From dd7fa0ecced0824ac5958f114425d1873564d4cd Mon Sep 17 00:00:00 2001 From: Frank Villaro-Dixon Date: Thu, 16 May 2024 19:49:49 +0200 Subject: [PATCH] have sliding submenus Signed-off-by: Frank Villaro-Dixon --- src/apps.rs | 185 ++++++++++++++++++-------------- src/hid.rs | 1 - src/main.rs | 11 +- src/roundy_math.rs | 10 ++ src/ui.rs | 256 +++++++++++++++++++++++++++++++++++++-------- 5 files changed, 338 insertions(+), 125 deletions(-) diff --git a/src/apps.rs b/src/apps.rs index ad92285..f780079 100644 --- a/src/apps.rs +++ b/src/apps.rs @@ -2,32 +2,9 @@ pub struct BeoApps { pub apps: Vec>, } -pub struct MainMenu { - // Can either be the default, or the current - pub selected_id: usize, - pub names: Vec, -} - -impl MainMenu { - pub fn selected_name(&self) -> &str { - &self.names[self.selected_id] - } - - pub fn go_left(&mut self) { - if self.selected_id > 0 { - self.selected_id -= 1; - } - } - pub fn go_right(&mut self) { - if self.selected_id < self.names.len() - 1 { - self.selected_id += 1; - } - } -} - pub struct AppBase { name: String, - pub main_menu: MainMenu, + pub main_menu: Menu, } impl AppBase { @@ -36,6 +13,37 @@ impl AppBase { } } +#[derive(Debug, Clone)] +pub struct Menu { + pub submenus: Vec>, + current_submenu_id: usize, + pub selected_submenu: Option>, + pub name: String, + //pub image: Option, +} + +impl Menu { + pub fn go_left(&mut self) { + if self.current_submenu_id > 0 { + self.current_submenu_id -= 1; + } + } + pub fn go_right(&mut self) { + if self.current_submenu_id < self.submenus.len() - 1 { + self.current_submenu_id += 1; + } + } + fn select_submenu(&mut self) { + self.selected_submenu = Some(self.submenus[self.current_submenu_id].clone()); + } + + pub fn get_deepest_selected_submenu(&self) -> &Menu { + match &self.selected_submenu { + Some(selected_submenu) => selected_submenu.get_deepest_selected_submenu(), + None => self, + } + } +} pub trait App { fn base(&self) -> &AppBase; fn base_mut(&mut self) -> &mut AppBase; @@ -48,20 +56,58 @@ struct Spotify { impl Spotify { fn new() -> Self { + let spotify_menus = vec![ + Box::new(Menu { + submenus: vec![], + current_submenu_id: 0, + selected_submenu: None, + name: "Playlists".to_string(), + }), + Box::new(Menu { + submenus: vec![], + current_submenu_id: 0, + selected_submenu: None, + name: "Artists".to_string(), + }), + Box::new(Menu { + submenus: vec![], + current_submenu_id: 0, + selected_submenu: None, + name: "Albums".to_string(), + }), + Box::new(Menu { + submenus: vec![], + current_submenu_id: 0, + selected_submenu: None, + name: "Songs".to_string(), + }), + Box::new(Menu { + submenus: vec![], + current_submenu_id: 0, + selected_submenu: None, + name: "Genres".to_string(), + }), + Box::new(Menu { + submenus: vec![], + current_submenu_id: 0, + selected_submenu: None, + name: "New Releases".to_string(), + }), + Box::new(Menu { + submenus: vec![], + current_submenu_id: 0, + selected_submenu: None, + name: "Charts".to_string(), + }), + ]; Spotify { base: AppBase { name: "Spotify".to_string(), - main_menu: MainMenu { - selected_id: 0, - names: vec![ - "Playlists".to_string(), - "Artists".to_string(), - "Albums".to_string(), - "Songs".to_string(), - "Genres".to_string(), - "New Releases".to_string(), - "Charts".to_string(), - ], + main_menu: Menu { + submenus: spotify_menus, + current_submenu_id: 0, + selected_submenu: None, + name: "XXX first one unused".to_string(), }, }, } @@ -84,16 +130,34 @@ struct Radio { impl Radio { fn new() -> Self { + let radio_menus = vec![ + Box::new(Menu { + submenus: vec![], + current_submenu_id: 0, + selected_submenu: None, + name: "Favorites".to_string(), + }), + Box::new(Menu { + submenus: vec![], + current_submenu_id: 0, + selected_submenu: None, + name: "Local".to_string(), + }), + Box::new(Menu { + submenus: vec![], + current_submenu_id: 0, + selected_submenu: None, + name: "Global".to_string(), + }), + ]; Radio { base: AppBase { name: "Radio".to_string(), - main_menu: MainMenu { - selected_id: 0, - names: vec![ - "Favorites".to_string(), - "Local".to_string(), - "Global".to_string(), - ], + main_menu: Menu { + submenus: radio_menus, + current_submenu_id: 0, + selected_submenu: None, + name: "XXX first one unused".to_string(), }, }, } @@ -109,42 +173,7 @@ impl App for Radio { } } -struct Settings { - base: AppBase, -} - -impl Settings { - fn new() -> Self { - Settings { - base: AppBase { - name: "Settings".to_string(), - main_menu: MainMenu { - selected_id: 0, - names: vec![ - "Display".to_string(), - "Sound".to_string(), - "Network".to_string(), - ], - }, - }, - } - } -} - -impl App for Settings { - fn base(&self) -> &AppBase { - &self.base - } - fn base_mut(&mut self) -> &mut AppBase { - &mut self.base - } -} - pub fn get_beo_apps() -> BeoApps { - let apps: Vec> = vec![ - Box::new(Spotify::new()), - Box::new(Radio::new()), - Box::new(Settings::new()), - ]; + let apps: Vec> = vec![Box::new(Spotify::new()), Box::new(Radio::new())]; BeoApps { apps } } diff --git a/src/hid.rs b/src/hid.rs index aeb3d14..0b84dae 100644 --- a/src/hid.rs +++ b/src/hid.rs @@ -78,7 +78,6 @@ impl Beo5Device { match evts { Ok(evts) => { - println!("Will iterate!"); for ev in evts { match ev.kind() { evdev::InputEventKind::Synchronization(_) => {} diff --git a/src/main.rs b/src/main.rs index 32c8e68..ab5a457 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,6 +20,7 @@ struct Fonts { app_title: Paint, app_menu: Paint, app_menu_selected: Paint, + menu_wheel: Paint, } fn main() { @@ -69,12 +70,19 @@ fn run( app_menu_selected.set_text_align(Align::Center); app_menu_selected.set_font_size(14.); + let mut menu_wheel = Paint::color(Color::hex("F0F0FF")); + menu_wheel.set_font(&[font_bold]); + menu_wheel.set_text_baseline(Baseline::Top); + menu_wheel.set_text_align(Align::Right); + menu_wheel.set_font_size(14.); + let fonts = Fonts { sans: font_sans, bold: font_bold, app_title, app_menu, app_menu_selected, + menu_wheel, }; let apps = get_beo_apps(); @@ -106,7 +114,6 @@ fn run( let hw_event = beo_device.get_event_nonblocking(); match hw_event { Some(ev) => { - println!("HW Event: {:?}", ev); beo.accept_event(ev); } None => {} @@ -177,6 +184,8 @@ fn run( } }, Event::RedrawRequested(_) => { + beo.dt_update(1.0 / 60.0); + let dpi_factor = window.scale_factor(); let size = window.inner_size(); canvas.set_size(size.width, size.height, dpi_factor as f32); diff --git a/src/roundy_math.rs b/src/roundy_math.rs index cc29c6d..d1c69d1 100644 --- a/src/roundy_math.rs +++ b/src/roundy_math.rs @@ -58,4 +58,14 @@ impl VirtualCircle { self.center.x - (self.radius.powf(2.) - (y - self.center.y).powf(2.)).sqrt() } + + pub fn get_point_from_angle_bcircle(&self, angle_deg: f32) -> Point { + // The beocircle has 0 at middle left, -90 at the top, 90 at the bottom + let angle_deg = angle_deg - 90.; + let angle_rad = angle_deg.to_radians(); + + let x = angle_rad.sin() * self.radius + self.center.x; + let y = -angle_rad.cos() * self.radius + self.center.y; + Point { x, y } + } } diff --git a/src/ui.rs b/src/ui.rs index 1f6d393..bb4cd7c 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -14,12 +14,21 @@ pub struct BeoUi { // XXX for items in menu //displayed_menu_item_id: Option, laser_pct: f32, // XXX this could be removed as only used when laser moved, could be given to fn directly + + // Stores the "angle shift" as defined by the infinite wheel. + // The quadrant is an "inverted" unit circle: 0 at the left, -90 at the top, 90 at the bottom + angle_shift: f32, + time_without_wheel_spin: f32, } const CANVAS_HEIGHT: f32 = 768.; const CANVAS_WIDTH: f32 = 1024.; const LASER_EPS_MATCH: f32 = 15.0; const LASER_MENU_CIRCLE_RADIUS: f32 = CANVAS_WIDTH - 30.; +const MENU_CIRCLE_RADIUS: f32 = 200.; +const ANGLE_DEG_BETWEEN_MENU_ITEMS: f32 = 40.; +const K: f32 = 0.04; +const DO_MAGNET_AFTER_SECS: f32 = 0.2; fn laser_pct_to_y_pos(pct: f32) -> f32 { // This is only y pos in the main menu circle, which we suppose is a const @@ -30,8 +39,81 @@ impl BeoUi { pub fn new(apps: BeoApps) -> BeoUi { BeoUi { beo_apps: apps, - current_app_id: None, + current_app_id: Some(0), laser_pct: 0.0, + angle_shift: 0.0, + time_without_wheel_spin: 0.0, + } + } + + pub fn dt_update(&mut self, dt: f32) { + // We do things that are time dependent here + + self.time_without_wheel_spin += dt; + //For example, round the angle to the nearest ANGLE_DEG + + self.adjust_wheel_angle(); + } + + fn adjust_wheel_angle(&mut self) { + // This function is called when the wheel is not spinning + // It will adjust the angle to the nearest multiple of ANGLE_DEG_BETWEEN_MENU_ITEMS + let angle_reminder = self.angle_shift % ANGLE_DEG_BETWEEN_MENU_ITEMS; + + let sign = if angle_reminder > 0. { 1. } else { -1. }; + let angle_abs = angle_reminder.abs(); + if self.time_without_wheel_spin > DO_MAGNET_AFTER_SECS { + if angle_abs > 0.1 { + // XXX TODO :angle abs: make it symmetrical + if angle_abs < ANGLE_DEG_BETWEEN_MENU_ITEMS / 2. { + //println!("1. Adjusting angle: {} * {}", sign, angle_abs * K); + self.angle_shift -= sign * angle_abs * K; + } else { + //println!("2. Adjusting angle: {} * {}", sign, angle_abs * K); + self.angle_shift += sign * angle_abs * K; + } + } else { + self.angle_shift -= angle_abs * sign; + } + } + + if self.angle_shift < -10. { + // Let the magnet bring it back to 0 + self.angle_shift = -10.; + } + + if let Some(selected_app) = self.current_app() { + let selected_menu = selected_app.base().main_menu.get_deepest_selected_submenu(); + let menu_count = selected_menu.submenus.len(); + let max_angle = (menu_count - 1) as f32 * ANGLE_DEG_BETWEEN_MENU_ITEMS + 10.; + + if self.angle_shift > max_angle { + self.angle_shift = max_angle; + } + } + + self.choose_app_wheel_angle(); + } + + fn choose_app_wheel_angle(&mut self) { + if let Some(selected_app) = self.current_app() { + let selected_menu = selected_app.base().main_menu.get_deepest_selected_submenu(); + let menu_count = selected_menu.submenus.len(); + let max_angle = (menu_count - 1) as f32 * -ANGLE_DEG_BETWEEN_MENU_ITEMS - 10.; + + let angle = self.angle_shift; + println!("Angle before: {}", angle); + let angle = angle.max(max_angle); + let angle = angle.min(0.); + + println!("Angle: {}", angle); + let menu_id_unrounded = (angle - max_angle) / ANGLE_DEG_BETWEEN_MENU_ITEMS; + println!("Menu id unrounded: {}", menu_id_unrounded); + let menu_id_unrounded = angle / ANGLE_DEG_BETWEEN_MENU_ITEMS; + println!("Menu id unrounded bis : {}", menu_id_unrounded); + let menu_id = (angle / ANGLE_DEG_BETWEEN_MENU_ITEMS).round() as usize; + println!("Menu id: {}", menu_id); + //selected_app.base_mut().main_menu.selected_id = menu_id; } } @@ -53,6 +135,11 @@ impl BeoUi { selected_app.base_mut().main_menu.go_right(); } } + Beo5Event::SelectionWheelRel(rel_angle_eps) => { + self.time_without_wheel_spin = 0.0; + self.angle_shift += rel_angle_eps as f32; + self.adjust_wheel_angle(); + } _ => { // TODO: pass event to current app } @@ -149,62 +236,141 @@ impl BeoUi { } fn draw_app(&self, canvas: &mut Canvas, fonts: &Fonts, app: &Box) { - let num_menu_elements_before = 1; - let num_menu_elements_after = 2; - // That means max 1 + >1< + 2 = 4 elements in total - // Minimum aronud 3: 0 + >1< + 2 + let current_app_deepest_menu = &app.base().main_menu.get_deepest_selected_submenu(); - let current_menu_id = app.base().main_menu.selected_id; - let menu_elements = &app.base().main_menu.names; + let menu_circle = roundy_math::VirtualCircle { + center: roundy_math::Point { + x: canvas.width() as f32, + y: canvas.height() as f32 / 2.0, + }, + radius: MENU_CIRCLE_RADIUS, + }; - // Calculate indices for slices - let start_before = - (current_menu_id as isize - num_menu_elements_before as isize).max(0) as usize; - let end_before = current_menu_id; - let start_after = current_menu_id + 1; - let end_after = - ((current_menu_id + 1 + num_menu_elements_after).min(menu_elements.len())) as usize; - - // Safely getting slices using clamping - let menu_elems_before = &menu_elements[start_before..end_before]; - let menu_elem_selected = &menu_elements[current_menu_id]; - let menu_elems_after = &menu_elements[start_after..end_after]; - - //println!("menu_elems_before: {:?}", menu_elems_before); - //println!("current_menu: {:?}", menu_elem_selected); - //println!("menu_elems_after: {:?}", menu_elems_after); - - let mut toti = 0.; - for i in 0..num_menu_elements_before { - // This is a special case: we reserve some space - //so that the current menu is always at the same place - if i < menu_elems_before.len() { - let _ = canvas.fill_text( - toti * 100. + 50., - CANVAS_HEIGHT - 80., - &menu_elems_before[i], - &fonts.app_menu, + for i in 0..current_app_deepest_menu.submenus.len() { + let angle = i as f32 * -ANGLE_DEG_BETWEEN_MENU_ITEMS + self.angle_shift; + if angle > -90. && angle < 90. { + let pos = menu_circle.get_point_from_angle_bcircle(angle); + let n = format!( + "{}: {} ({}°)", + i, current_app_deepest_menu.submenus[i].name, angle ); + let _ = canvas.fill_text( + pos.x, + pos.y, + &n, //current_app_deepest_menu.submenus[i].name, + &fonts.menu_wheel, + ); + continue; } - toti += 1.; } - let _ = canvas.fill_text( - toti * 100. + 50., - CANVAS_HEIGHT - 80., - &menu_elem_selected, - &fonts.app_menu_selected, - ); - toti += 1.; + /* + let menus_count = current_app_deepest_menu.submenus.len(); - for i in 0..menu_elems_after.len() { + let menu_circle = roundy_math::VirtualCircle { + center: roundy_math::Point { + x: canvas_width as f32, + y: canvas_height as f32 / 2.0, + }, + radius: MENU_CIRCLE_RADIUS, + }; + + let canvas_size = roundy_math::Point { + x: canvas_width as f32, + y: canvas_height as f32, + }; + + // draw the main apps in the circle + let apps = &self.beo_apps.apps; + let pts = laser_menu_circle.get_equidistant_points(apps.len(), canvas_size); + + // XXX To be taken from global struct + let mut paint_normal = Paint::color(Color::hex("B7410E")); + paint_normal.set_font(&[fonts.sans]); + paint_normal.set_text_baseline(Baseline::Top); + + let mut paint_selected = Paint::color(Color::hex("D7612E")); + paint_selected.set_font(&[fonts.bold]); + paint_selected.set_text_baseline(Baseline::Top); + + for appid in 0..apps.len() { + let paint; + if self.current_app_id == Some(appid) { + paint = &paint_selected; + } else { + paint = &paint_normal; + } + + let app = &apps[appid]; + let _ = canvas.fill_text(pts[appid].x, pts[appid].y, app.base().name(), paint); + } + + // draw the laser + let ellipse_color = Paint::color(Color::hex("5C89D188")); + let mut path = Path::new(); + let ey = laser_pct_to_y_pos(self.laser_pct); + let ex = laser_menu_circle.get_x_on_circle(ey); + + path.ellipse(ex + 15., ey, 30., 10.); + canvas.fill_path(&path, &ellipse_color); + */ + } + /* + + + + let num_menu_elements_before = 1; + let num_menu_elements_after = 2; + // That means max 1 + >1< + 2 = 4 elements in total + // Minimum aronud 3: 0 + >1< + 2 + + let current_menu_id = app.base().main_menu.selected_id; + let menu_elements = &app.base().main_menu.names; + + // Calculate indices for slices + let start_before = + (current_menu_id as isize - num_menu_elements_before as isize).max(0) as usize; + let end_before = current_menu_id; + let start_after = current_menu_id + 1; + let end_after = + ((current_menu_id + 1 + num_menu_elements_after).min(menu_elements.len())) as usize; + + // Safely getting slices using clamping + let menu_elems_before = &menu_elements[start_before..end_before]; + let menu_elem_selected = &menu_elements[current_menu_id]; + let menu_elems_after = &menu_elements[start_after..end_after]; + + let mut toti = 0.; + for i in 0..num_menu_elements_before { + // This is a special case: we reserve some space + //so that the current menu is always at the same place + if i < menu_elems_before.len() { let _ = canvas.fill_text( toti * 100. + 50., CANVAS_HEIGHT - 80., - &menu_elems_after[i], + &menu_elems_before[i], &fonts.app_menu, ); - toti += 1.; } + toti += 1.; } + + let _ = canvas.fill_text( + toti * 100. + 50., + CANVAS_HEIGHT - 80., + &menu_elem_selected, + &fonts.app_menu_selected, + ); + toti += 1.; + + for i in 0..menu_elems_after.len() { + let _ = canvas.fill_text( + toti * 100. + 50., + CANVAS_HEIGHT - 80., + &menu_elems_after[i], + &fonts.app_menu, + ); + toti += 1.; + } + */ }