diff --git a/Cargo.toml b/Cargo.toml index 0a16e82a..f9570b4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -152,6 +152,7 @@ enumset = "1.1.3" # esp32c6-hal = { path = "../../esp-hal/esp32c6-hal" } # esp-hal-common = { path = "../../esp-hal/esp-hal-common" } # esp-hal-procmacros = { path = "../../esp-hal/esp-hal-procmacros" } +embedded-menu = { git = "https://github.com/bugadani/embedded-menu.git" } [patch.'https://github.com/esp-rs/esp-wifi.git'] # esp-wifi = { path = "../esp-wifi/esp-wifi" } diff --git a/gui/src/screens/mod.rs b/gui/src/screens/mod.rs index 57270f5f..83388161 100644 --- a/gui/src/screens/mod.rs +++ b/gui/src/screens/mod.rs @@ -25,10 +25,9 @@ pub mod qr; pub mod wifi_ap; pub const fn menu_style( -) -> MenuStyle { +) -> MenuStyle { MenuStyle::new(BinaryColor::On) .with_animated_selection_indicator(10) - .with_details_delay(300) .with_selection_indicator(AnimatedTriangle::new(200)) .with_input_adapter(SingleTouch { debounce_time: 1, @@ -40,7 +39,7 @@ pub const fn menu_style( pub fn create_menu, R>( title: T, -) -> MenuBuilder { +) -> MenuBuilder { Menu::with_style(title, menu_style()) } diff --git a/gui/src/screens/wifi_ap.rs b/gui/src/screens/wifi_ap.rs index 60f1f092..def23ab5 100644 --- a/gui/src/screens/wifi_ap.rs +++ b/gui/src/screens/wifi_ap.rs @@ -1,6 +1,8 @@ use embedded_graphics::{pixelcolor::BinaryColor, prelude::DrawTarget, Drawable}; +use embedded_layout::{chain, object_chain::Chain}; use embedded_menu::{ interaction::single_touch::SingleTouch, + items::MenuItem, selection_indicator::{style::animated_triangle::AnimatedTriangle, AnimatedPosition}, Menu, }; @@ -17,18 +19,16 @@ pub enum ApMenuEvents { Exit, } -#[derive(Clone, Copy, PartialEq, Eq, Menu)] -#[menu( - title = "WiFi Config", - navigation(events = ApMenuEvents), - items = [ - navigation(label = "Exit", event = ApMenuEvents::Exit) - ] -)] -pub struct ApMenu {} - pub struct WifiApScreen { - pub menu: ApMenuMenuWrapper, + pub menu: Menu< + &'static str, + SingleTouch, + chain! { MenuItem<&'static str, ApMenuEvents, (), true> }, + ApMenuEvents, + AnimatedPosition, + AnimatedTriangle, + BinaryColor, + >, pub state: WifiAccessPointState, pub timeout: Option, } @@ -36,7 +36,9 @@ pub struct WifiApScreen { impl WifiApScreen { pub fn new() -> Self { Self { - menu: ApMenu {}.create_menu_with_style(menu_style()), + menu: Menu::with_style("WiFi Config", menu_style()) + .add_item("Exit", (), |_| ApMenuEvents::Exit) + .build(), state: WifiAccessPointState::NotConnected, timeout: None, } diff --git a/gui/src/widgets/battery_small.rs b/gui/src/widgets/battery_small.rs index 3893364f..978973fb 100644 --- a/gui/src/widgets/battery_small.rs +++ b/gui/src/widgets/battery_small.rs @@ -9,7 +9,7 @@ use embedded_graphics::{ }; use embedded_io_async::{Read, Write}; use embedded_layout::prelude::*; -use embedded_menu::items::select::SelectValue; +use embedded_menu::items::menu_item::SelectValue; use norfs::storable::{LoadError, Loadable, Storable}; use ufmt::uwrite; @@ -298,15 +298,16 @@ impl Drawable for Battery { } impl SelectValue for BatteryStyle { - fn next(&self) -> Self { - match self { + fn next(&mut self) { + *self = match self { Self::MilliVolts => Self::Percentage, Self::Percentage => Self::Icon, Self::Icon => Self::LowIndicator, Self::LowIndicator => Self::MilliVolts, - } + }; } - fn name(&self) -> &'static str { + + fn marker(&self) -> &str { match self { Self::MilliVolts => "MilliVolts", Self::Percentage => "Percentage", diff --git a/src/states/menu/about.rs b/src/states/menu/about.rs index 9aae6a15..c2b39697 100644 --- a/src/states/menu/about.rs +++ b/src/states/menu/about.rs @@ -5,7 +5,7 @@ use crate::{ uformat, AppState, SerialNumber, }; -use embedded_menu::items::NavigationItem; +use embedded_menu::items::menu_item::MenuItem; use gui::screens::create_menu; #[derive(Clone, Copy)] @@ -31,16 +31,15 @@ impl MenuScreen for AboutAppMenu { type Result = AppState; async fn menu(&mut self, context: &mut Context) -> impl AppMenuBuilder { - let list_item = |label| NavigationItem::new(label, AboutMenuEvents::None); + let list_item = + |label| MenuItem::new(label, "").with_value_converter(|_| AboutMenuEvents::None); let mut items = heapless::Vec::<_, 5>::new(); items.extend([ list_item(uformat!(20, "{}", env!("FW_VERSION_MENU_ITEM"))), list_item(uformat!(20, "{}", env!("HW_VERSION_MENU_ITEM"))), - NavigationItem::new( - uformat!(20, "Serial {}", SerialNumber), - AboutMenuEvents::ToSerial, - ), + list_item(uformat!(20, "Serial {}", SerialNumber)) + .with_value_converter(|_| AboutMenuEvents::ToSerial), list_item(match context.frontend.device_id() { Some(id) => uformat!(20, "ADC {:?}", LeftPadAny(16, id)), None => uformat!(20, "ADC Unknown"), @@ -51,15 +50,15 @@ impl MenuScreen for AboutAppMenu { { unwrap!(items .push( - NavigationItem::new(uformat!(20, "Fuel gauge"), AboutMenuEvents::ToBatteryInfo) - .with_marker("MAX17055") + MenuItem::new(uformat!(20, "Fuel gauge"), "MAX17055") + .with_value_converter(|_| AboutMenuEvents::ToBatteryInfo) ) .ok()); } create_menu("Device info") - .add_items(items) - .add_item(NavigationItem::new("Back", AboutMenuEvents::Back)) + .add_menu_items(items) + .add_item("Back", "<-", |_| AboutMenuEvents::Back) } async fn handle_event( diff --git a/src/states/menu/battery_info.rs b/src/states/menu/battery_info.rs index 5b1f999a..8472b32c 100644 --- a/src/states/menu/battery_info.rs +++ b/src/states/menu/battery_info.rs @@ -5,7 +5,7 @@ use crate::{ uformat, AppState, }; use embassy_time::Duration; -use embedded_menu::items::NavigationItem; +use embedded_menu::items::menu_item::MenuItem; use gui::screens::create_menu; #[derive(Clone, Copy)] @@ -33,7 +33,7 @@ impl MenuScreen for BatteryInfoMenu { let mut list_item = |label| { unwrap!(items - .push(NavigationItem::new(label, BatteryEvents::None)) + .push(MenuItem::new(label, "").with_value_converter(|_| BatteryEvents::None)) .ok()) }; @@ -72,8 +72,8 @@ impl MenuScreen for BatteryInfoMenu { } create_menu("Battery info") - .add_items(items) - .add_item(NavigationItem::new("Back", BatteryEvents::Back)) + .add_menu_items(items) + .add_item("Back", "<-", |_| BatteryEvents::Back) } async fn handle_event( diff --git a/src/states/menu/display.rs b/src/states/menu/display.rs index 302c8fc7..890f0cfe 100644 --- a/src/states/menu/display.rs +++ b/src/states/menu/display.rs @@ -6,7 +6,6 @@ use crate::{ states::menu::{AppMenu, MenuScreen}, AppState, }; -use embedded_menu::items::{NavigationItem, Select}; use gui::{screens::create_menu, widgets::battery_small::BatteryStyle}; pub async fn display_menu(context: &mut Context) -> AppState { @@ -37,18 +36,21 @@ impl MenuScreen for DisplayMenu { async fn menu(&mut self, context: &mut Context) -> impl super::AppMenuBuilder { create_menu("Display") .add_item( - Select::new("Brightness", context.config.display_brightness) - .with_value_converter(DisplayMenuEvents::ChangeBrigtness), + "Brightness", + context.config.display_brightness, + DisplayMenuEvents::ChangeBrigtness, ) .add_item( - Select::new("Battery", context.config.battery_display_style) - .with_value_converter(DisplayMenuEvents::ChangeBatteryStyle), + "Battery", + context.config.battery_display_style, + DisplayMenuEvents::ChangeBatteryStyle, ) .add_item( - Select::new("EKG Filter", context.config.filter_strength) - .with_value_converter(DisplayMenuEvents::ChangeFilterStrength), + "EKG Filter", + context.config.filter_strength, + DisplayMenuEvents::ChangeFilterStrength, ) - .add_item(NavigationItem::new("Back", DisplayMenuEvents::Back)) + .add_item("Back", "<-", |_| DisplayMenuEvents::Back) } async fn handle_event( diff --git a/src/states/menu/main.rs b/src/states/menu/main.rs index 5b93e0fb..f05347b9 100644 --- a/src/states/menu/main.rs +++ b/src/states/menu/main.rs @@ -4,12 +4,12 @@ use crate::{ states::menu::{AppMenu, MenuScreen}, AppState, }; -use embedded_menu::items::NavigationItem; +use embedded_menu::items::menu_item::{MenuItem, SelectValue}; use gui::screens::create_menu; use super::AppMenuBuilder; -#[derive(Clone, Copy)] +#[derive(Clone, Copy, PartialEq)] pub enum MainMenuEvents { Measure, Display, @@ -22,6 +22,12 @@ pub enum MainMenuEvents { Shutdown, } +impl SelectValue for MainMenuEvents { + fn marker(&self) -> &'static str { + "" + } +} + pub async fn main_menu(context: &mut Context) -> AppState { info!("Free heap: {} bytes", ALLOCATOR.free()); @@ -40,16 +46,18 @@ impl MenuScreen for MainMenu { async fn menu(&mut self, context: &mut Context) -> impl AppMenuBuilder { let mut optional_items = heapless::Vec::<_, 4>::new(); - let mut optional_item = - |label, event| unwrap!(optional_items.push(NavigationItem::new(label, event)).ok()); - if context.can_enable_wifi() { - optional_item("Wifi setup", MainMenuEvents::WifiSetup); - optional_item("Wifi networks", MainMenuEvents::WifiListVisible); - + let mut optional_item = |label, event| { + unwrap!(optional_items + .push(MenuItem::new(label, event).with_value_converter(|evt| evt)) + .ok()) + }; let network_configured = !context.config.backend_url.is_empty() && !context.config.known_networks.is_empty(); + optional_item("Wifi setup", MainMenuEvents::WifiSetup); + optional_item("Wifi networks", MainMenuEvents::WifiListVisible); + if network_configured { optional_item("Firmware update", MainMenuEvents::FirmwareUpdate); optional_item("Speed test", MainMenuEvents::Throughput); @@ -57,12 +65,12 @@ impl MenuScreen for MainMenu { } create_menu("Main menu") - .add_item(NavigationItem::new("Measure", MainMenuEvents::Measure)) - .add_item(NavigationItem::new("Display", MainMenuEvents::Display)) - .add_item(NavigationItem::new("Storage", MainMenuEvents::Storage)) - .add_item(NavigationItem::new("Device info", MainMenuEvents::About)) - .add_items(optional_items) - .add_item(NavigationItem::new("Shutdown", MainMenuEvents::Shutdown)) + .add_item("Measure", MainMenuEvents::Measure, |evt| evt) + .add_item("Display", MainMenuEvents::Display, |evt| evt) + .add_item("Storage", MainMenuEvents::Storage, |evt| evt) + .add_item("Device info", MainMenuEvents::About, |evt| evt) + .add_menu_items(optional_items) + .add_item("Shutdown", MainMenuEvents::Shutdown, |evt| evt) } async fn handle_event( diff --git a/src/states/menu/mod.rs b/src/states/menu/mod.rs index 77396596..3817c84a 100644 --- a/src/states/menu/mod.rs +++ b/src/states/menu/mod.rs @@ -49,7 +49,7 @@ pub trait AppMenuBuilder { } impl AppMenuBuilder - for MenuBuilder + for MenuBuilder where T: AsRef, VG: ViewGroup + MenuItemCollection, @@ -75,7 +75,7 @@ pub trait AppMenuT: Drawable { } impl AppMenuT - for Menu + for Menu where T: AsRef, VG: ViewGroup + MenuItemCollection, diff --git a/src/states/menu/storage.rs b/src/states/menu/storage.rs index f6f22f70..35a066aa 100644 --- a/src/states/menu/storage.rs +++ b/src/states/menu/storage.rs @@ -8,7 +8,7 @@ use crate::{ states::menu::{AppMenu, MenuScreen}, uformat, AppState, }; -use embedded_menu::items::{NavigationItem, Select}; +use embedded_menu::items::menu_item::{MenuItem, SelectValue}; use gui::screens::create_menu; use super::AppMenuBuilder; @@ -33,6 +33,15 @@ pub async fn storage_menu(context: &mut Context) -> AppState { result } +#[derive(Clone, PartialEq)] +struct UsedStorage(heapless::String<32>); + +impl SelectValue for UsedStorage { + fn marker(&self) -> &str { + self.0.as_str() + } +} + struct StorageMenu; impl MenuScreen for StorageMenu { @@ -40,56 +49,50 @@ impl MenuScreen for StorageMenu { type Result = AppState; async fn menu(&mut self, context: &mut Context) -> impl AppMenuBuilder { - // needs to be separate because the item is of a different type - let mut used_item = heapless::Vec::<_, 1>::new(); + let mut used_item = heapless::Vec::<_, 2>::new(); + let mut items = heapless::Vec::<_, 2>::new(); + if let Some(storage) = context.storage.as_mut() { if let Ok(used) = storage.used_bytes().await { - let used_str = uformat!( + let used_str = UsedStorage(uformat!( 32, "{}/{}", BinarySize(used), BinarySize(storage.capacity()) - ); + )); unwrap!(used_item .push( - NavigationItem::new("Used", StorageMenuEvents::Nothing) - .with_marker(used_str) + MenuItem::new("Used", used_str) + .with_value_converter(|_| StorageMenuEvents::Nothing) ) .ok()); } - } - let mut items = heapless::Vec::<_, 2>::new(); - unwrap!(items - .push(NavigationItem::new( - "Format storage", - StorageMenuEvents::Format, - )) - .ok()); - - if context.can_enable_wifi() - && !context.config.known_networks.is_empty() - && !context.config.backend_url.is_empty() - && context.sta_has_work().await - { - unwrap!(items - .push(NavigationItem::new( - "Upload data", - StorageMenuEvents::Upload - )) - .ok()); + if context.can_enable_wifi() + && !context.config.known_networks.is_empty() + && !context.config.backend_url.is_empty() + && context.sta_has_work().await + { + unwrap!(items + .push( + MenuItem::new("Upload data", "->") + .with_value_converter(|_| StorageMenuEvents::Upload) + ) + .ok()); + } } create_menu("Storage") .add_item( - Select::new("New EKG", context.config.measurement_action) - .with_value_converter(StorageMenuEvents::ChangeMeasurementAction) - .with_detail_text("What to do with new measurements"), + "New EKG", + context.config.measurement_action, + StorageMenuEvents::ChangeMeasurementAction, ) - .add_items(used_item) - .add_items(items) - .add_item(NavigationItem::new("Back", StorageMenuEvents::Back)) + .add_menu_items(used_item) + .add_menu_items(items) + .add_item("Format storage", "->", |_| StorageMenuEvents::Format) + .add_item("Back", "<-", |_| StorageMenuEvents::Back) } async fn handle_event( diff --git a/src/states/menu/wifi_sta.rs b/src/states/menu/wifi_sta.rs index 6986d138..974d7bc5 100644 --- a/src/states/menu/wifi_sta.rs +++ b/src/states/menu/wifi_sta.rs @@ -4,7 +4,7 @@ use alloc::{string::String, vec}; use embassy_futures::select::{select, Either}; use embassy_time::{Duration, Instant, Ticker, Timer}; use embedded_graphics::Drawable; -use embedded_menu::items::NavigationItem; +use embedded_menu::items::menu_item::MenuItem; use gui::screens::create_menu; use crate::{ @@ -36,8 +36,9 @@ pub async fn wifi_sta(context: &mut Context) -> AppState { let mut ticker = Ticker::every(MIN_FRAME_TIME); let mut menu_state = Default::default(); - let list_item = - |label: &str| NavigationItem::new(String::from(label), WifiStaMenuEvents::None); + let list_item = |label: &str| { + MenuItem::new(String::from(label), "").with_value_converter(|_| WifiStaMenuEvents::None) + }; // Initial placeholder let mut ssids = vec![list_item("Scanning...")]; @@ -72,8 +73,8 @@ pub async fn wifi_sta(context: &mut Context) -> AppState { } let mut menu_screen = create_menu("Access points") - .add_items(&mut ssids) - .add_item(NavigationItem::new("Back", WifiStaMenuEvents::Back)) + .add_menu_items(&mut ssids) + .add_item("Back", "<-", |_| WifiStaMenuEvents::Back) .build_with_state(menu_state); if let Some(WifiStaMenuEvents::Back) = menu_screen.interact(is_touched) { diff --git a/src/states/upload_or_store_measurement.rs b/src/states/upload_or_store_measurement.rs index 7d681e85..cbbecc57 100644 --- a/src/states/upload_or_store_measurement.rs +++ b/src/states/upload_or_store_measurement.rs @@ -5,7 +5,7 @@ use core::{ use alloc::{boxed::Box, vec::Vec}; use embassy_time::Duration; -use embedded_menu::items::NavigationItem; +use embedded_menu::items::menu_item::{MenuItem, SelectValue}; use embedded_nal_async::{Dns, TcpConnect}; use gui::screens::create_menu; use norfs::{ @@ -122,6 +122,14 @@ async fn ask_for_measurement_action(context: &mut Context) -> (bool, bool) { struct AskForMeasurementActionMenu; +#[derive(Clone, Copy, PartialEq)] +struct UploadOrStore(bool, bool); +impl SelectValue for UploadOrStore { + fn marker(&self) -> &'static str { + "" + } +} + impl MenuScreen for AskForMeasurementActionMenu { type Event = (bool, bool); type Result = (bool, bool); @@ -129,8 +137,13 @@ impl MenuScreen for AskForMeasurementActionMenu { async fn menu(&mut self, context: &mut Context) -> impl AppMenuBuilder { let mut items = heapless::Vec::<_, 3>::new(); - let mut add_item = |label, value| { - unwrap!(items.push(NavigationItem::new(label, value)).ok()); + let mut add_item = |label, can_upload, can_store| { + unwrap!(items + .push( + MenuItem::new(label, UploadOrStore(can_upload, can_store)) + .with_value_converter(|x| (x.0, x.1)) + ) + .ok()); }; let network_configured = @@ -140,18 +153,20 @@ impl MenuScreen for AskForMeasurementActionMenu { if network_configured { if can_store { - add_item("Upload or store", (true, true)); + add_item("Upload or store", true, true); } - add_item("Upload", (true, false)); + add_item("Upload", true, false); } if can_store { - add_item("Store", (false, true)); + add_item("Store", false, true); } - create_menu("EKG action") - .add_items(items) - .add_item(NavigationItem::new("Discard", (false, false))) + create_menu("EKG action").add_menu_items(items).add_item( + "Discard", + UploadOrStore(false, false), + |x| (x.0, x.1), + ) } async fn handle_event(