diff --git a/src/ui/app.rs b/src/ui/app.rs index 801b580c3585203a1b8eadc74a277380cae24b74..b83784b0a4fef6e196444432cc2065559999c63e 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -4,7 +4,9 @@ use egui_tiles::{Behavior, Container, Linear, LinearDir, Tile, TileId, Tiles, Tr use serde::{Deserialize, Serialize}; use std::{ fs, + ops::DerefMut, path::{Path, PathBuf}, + sync::{Arc, Mutex}, time::{Duration, Instant}, }; use tracing::{debug, error, trace}; @@ -18,7 +20,7 @@ use crate::{ use super::{ panes::{Pane, PaneBehavior, PaneKind}, persistency::LayoutManager, - shortcuts, + shortcuts::{ShortcutHandler, ShortcutMode}, utils::maximized_pane_ui, widget_gallery::WidgetGallery, widgets::reception_led::ReceptionLed, @@ -34,6 +36,8 @@ pub struct App { // == Message handling == message_broker: MessageBroker, message_bundle: MessageBundle, + // Shortcut handling + shortcut_handler: Arc<Mutex<ShortcutHandler>>, // == Windows == widget_gallery: WidgetGallery, sources_window: ConnectionsWindow, @@ -59,17 +63,19 @@ impl eframe::App for App { if let Some(hovered_tile) = hovered_pane { // Capture any pane action generated by keyboard shortcuts let key_action_pairs = [ - ((Modifiers::NONE, Key::V), PaneAction::SplitV), - ((Modifiers::NONE, Key::H), PaneAction::SplitH), - ((Modifiers::NONE, Key::C), PaneAction::Close), - ((Modifiers::NONE, Key::R), PaneAction::ReplaceThroughGallery), - ((Modifiers::SHIFT, Key::Escape), PaneAction::Maximize), - ((Modifiers::NONE, Key::Escape), PaneAction::Exit), + (Modifiers::NONE, Key::V, PaneAction::SplitV), + (Modifiers::NONE, Key::H, PaneAction::SplitH), + (Modifiers::NONE, Key::C, PaneAction::Close), + (Modifiers::NONE, Key::R, PaneAction::ReplaceThroughGallery), + (Modifiers::SHIFT, Key::Escape, PaneAction::Maximize), + (Modifiers::NONE, Key::Escape, PaneAction::Exit), ]; - pane_action = - pane_action - .or(shortcuts::map_to_action(ctx, &key_action_pairs[..]) - .map(|a| (hovered_tile, a))); + pane_action = pane_action.or(self + .shortcut_handler + .lock() + .log_unwrap() + .consume_if_mode_is(ShortcutMode::composition(), &key_action_pairs[..]) + .map(|a| (hovered_tile, a))); } // If an action was triggered, we consume it @@ -220,9 +226,13 @@ impl eframe::App for App { egui::CentralPanel::default().show(ctx, |ui| { if let Some(maximized_pane) = self.maximized_pane { if let Some(Tile::Pane(pane)) = panes_tree.tiles.get_mut(maximized_pane) { - maximized_pane_ui(ui, pane); + maximized_pane_ui( + ui, + pane, + self.shortcut_handler.lock().log_unwrap().deref_mut(), + ); } else { - panic!("Maximized pane not found in tree!"); + unreachable!("Maximized pane not found in tree!"); } } else { panes_tree.ui(&mut self.behavior, ui); @@ -276,14 +286,16 @@ impl App { }); } + let shortcut_handler = Arc::new(Mutex::new(ShortcutHandler::new(ctx.egui_ctx.clone()))); Self { state, layout_manager, message_broker: MessageBroker::new(ctx.egui_ctx.clone()), widget_gallery: WidgetGallery::default(), - behavior: AppBehavior::default(), + behavior: AppBehavior::new(Arc::clone(&shortcut_handler)), maximized_pane: None, message_bundle: MessageBundle::default(), + shortcut_handler, sources_window: ConnectionsWindow::default(), layout_manager_window: LayoutManagerWindow::default(), } @@ -393,12 +405,22 @@ impl AppState { } /// Behavior for the tree of panes in the app -#[derive(Default)] pub struct AppBehavior { + pub shortcut_handler: Arc<Mutex<ShortcutHandler>>, pub action: Option<(TileId, PaneAction)>, pub tile_id_hovered: Option<TileId>, } +impl AppBehavior { + fn new(shortcut_handler: Arc<Mutex<ShortcutHandler>>) -> Self { + Self { + shortcut_handler, + action: None, + tile_id_hovered: None, + } + } +} + impl Behavior<Pane> for AppBehavior { fn pane_ui( &mut self, @@ -406,7 +428,7 @@ impl Behavior<Pane> for AppBehavior { tile_id: TileId, pane: &mut Pane, ) -> egui_tiles::UiResponse { - let res = ui.scope(|ui| pane.ui(ui)); + let res = ui.scope(|ui| pane.ui(ui, self.shortcut_handler.lock().log_unwrap().deref_mut())); let PaneResponse { action_called, drag_response, diff --git a/src/ui/panes.rs b/src/ui/panes.rs index 9e0ea2f10a89d5f26cfee111faf8934b5649ecb0..3fa948cf62bacc3dca440f5c3a739ba576a64aaa 100644 --- a/src/ui/panes.rs +++ b/src/ui/panes.rs @@ -11,7 +11,7 @@ use strum_macros::{self, EnumIter, EnumMessage}; use crate::mavlink::{MavMessage, TimedMessage}; -use super::app::PaneResponse; +use super::{app::PaneResponse, shortcuts::ShortcutHandler}; #[derive(Clone, PartialEq, Default, Serialize, Deserialize, Debug)] pub struct Pane { @@ -27,7 +27,7 @@ impl Pane { #[enum_dispatch(PaneKind)] pub trait PaneBehavior { /// Renders the UI of the pane. - fn ui(&mut self, ui: &mut Ui) -> PaneResponse; + fn ui(&mut self, ui: &mut Ui, shortcut_handler: &mut ShortcutHandler) -> PaneResponse; /// Updates the pane state. This method is called before `ui` to allow the /// pane to update its state based on the messages received. @@ -50,8 +50,8 @@ pub trait PaneBehavior { } impl PaneBehavior for Pane { - fn ui(&mut self, ui: &mut Ui) -> PaneResponse { - self.pane.ui(ui) + fn ui(&mut self, ui: &mut Ui, shortcut_handler: &mut ShortcutHandler) -> PaneResponse { + self.pane.ui(ui, shortcut_handler) } fn update(&mut self, messages: &[&TimedMessage]) { diff --git a/src/ui/panes/default.rs b/src/ui/panes/default.rs index e0419cae5aa3aef587fe6c8753e662db24b5ad97..dd2406f6c54c63c157ac4789dcd69bfdccd58443 100644 --- a/src/ui/panes/default.rs +++ b/src/ui/panes/default.rs @@ -7,6 +7,7 @@ use crate::{ mavlink::TimedMessage, ui::{ app::{PaneAction, PaneResponse}, + shortcuts::ShortcutHandler, utils::{SizingMemo, vertically_centered}, }, }; @@ -27,7 +28,7 @@ impl PartialEq for DefaultPane { impl PaneBehavior for DefaultPane { #[profiling::function] - fn ui(&mut self, ui: &mut Ui) -> PaneResponse { + fn ui(&mut self, ui: &mut Ui, _shortcut_handler: &mut ShortcutHandler) -> PaneResponse { let mut response = PaneResponse::default(); let parent = vertically_centered(ui, &mut self.centering_memo, |ui| { @@ -49,11 +50,7 @@ impl PaneBehavior for DefaultPane { self.contains_pointer = parent.contains_pointer(); - if parent - .interact(egui::Sense::click_and_drag()) - .on_hover_cursor(egui::CursorIcon::Grab) - .dragged() - { + if parent.interact(egui::Sense::click_and_drag()).dragged() { response.set_drag_started(); }; diff --git a/src/ui/panes/messages_viewer.rs b/src/ui/panes/messages_viewer.rs index cfee6c67ab5601d72b53852581f51c76d07c21da..eb964669f6b83b39e090b230501d222eac7f8c45 100644 --- a/src/ui/panes/messages_viewer.rs +++ b/src/ui/panes/messages_viewer.rs @@ -1,7 +1,7 @@ use egui::{Label, Ui}; use serde::{Deserialize, Serialize}; -use crate::ui::app::PaneResponse; +use crate::ui::{app::PaneResponse, shortcuts::ShortcutHandler}; use super::PaneBehavior; @@ -10,7 +10,7 @@ pub struct MessagesViewerPane; impl PaneBehavior for MessagesViewerPane { #[profiling::function] - fn ui(&mut self, ui: &mut Ui) -> PaneResponse { + fn ui(&mut self, ui: &mut Ui, _shortcut_handler: &mut ShortcutHandler) -> PaneResponse { let mut response = PaneResponse::default(); let label = ui.add_sized(ui.available_size(), Label::new("This is a label")); if label.drag_started() { diff --git a/src/ui/panes/pid_drawing_tool.rs b/src/ui/panes/pid_drawing_tool.rs index 05160cc8a16e697f7d417667b7e4bb5839881741..9257cf44ec076c1efda702e8f2c1e46b3199720c 100644 --- a/src/ui/panes/pid_drawing_tool.rs +++ b/src/ui/panes/pid_drawing_tool.rs @@ -19,7 +19,9 @@ use crate::{ MAVLINK_PROFILE, error::ErrInstrument, mavlink::{GSE_TM_DATA, MessageData, TimedMessage, reflection::MessageLike}, - ui::{app::PaneResponse, cache::ChangeTracker, utils::egui_to_glam}, + ui::{ + app::PaneResponse, cache::ChangeTracker, shortcuts::ShortcutHandler, utils::egui_to_glam, + }, }; use super::PaneBehavior; @@ -79,7 +81,7 @@ impl PartialEq for PidPane { } impl PaneBehavior for PidPane { - fn ui(&mut self, ui: &mut Ui) -> PaneResponse { + fn ui(&mut self, ui: &mut Ui, _shortcut_handler: &mut ShortcutHandler) -> PaneResponse { let mut pane_response = PaneResponse::default(); let theme = PidPane::find_theme(ui.ctx()); diff --git a/src/ui/panes/plot.rs b/src/ui/panes/plot.rs index af835401f3ca7b9dda7e81a664b8bae940461818..e4616d1532f060623372344818eeb051332ab105 100644 --- a/src/ui/panes/plot.rs +++ b/src/ui/panes/plot.rs @@ -8,7 +8,7 @@ use crate::{ MessageData, ROCKET_FLIGHT_TM_DATA, TimedMessage, reflection::{FieldLike, IndexedField}, }, - ui::app::PaneResponse, + ui::{app::PaneResponse, shortcuts::ShortcutHandler}, utils::units::UnitOfMeasure, }; use egui::{Color32, Ui, Vec2, Vec2b}; @@ -41,7 +41,7 @@ impl PartialEq for Plot2DPane { impl PaneBehavior for Plot2DPane { #[profiling::function] - fn ui(&mut self, ui: &mut Ui) -> PaneResponse { + fn ui(&mut self, ui: &mut Ui, _shortcut_handler: &mut ShortcutHandler) -> PaneResponse { let mut response = PaneResponse::default(); let data_settings_digest = self.settings.data_digest(); diff --git a/src/ui/panes/valve_control.rs b/src/ui/panes/valve_control.rs index 8488f607490121ab2f7c33c3091ca4333a9903be..6564ae30f46ad6b08b88c2bd327cd94e68e005e3 100644 --- a/src/ui/panes/valve_control.rs +++ b/src/ui/panes/valve_control.rs @@ -3,17 +3,15 @@ mod icons; mod valves; use std::{ - fmt::format, + collections::HashMap, time::{Duration, Instant}, }; use egui::{ - Color32, DragValue, FontId, Frame, Grid, Label, Layout, Margin, Rect, Response, RichText, - Sense, Stroke, TextFormat, Ui, UiBuilder, Vec2, Widget, WidgetText, - text::{Fonts, LayoutJob}, + Color32, DragValue, FontId, Frame, Grid, Key, Label, Margin, Modal, Modifiers, Response, + RichText, Sense, Stroke, TextFormat, Ui, UiBuilder, Vec2, Widget, Window, text::LayoutJob, vec2, }; -use egui_extras::{Size, StripBuilder}; use itertools::Itertools; use serde::{Deserialize, Serialize}; use skyward_mavlink::{ @@ -21,22 +19,26 @@ use skyward_mavlink::{ orion::{ACK_TM_DATA, NACK_TM_DATA, WACK_TM_DATA}, }; use strum::IntoEnumIterator; -use tracing::{info, warn}; +use tracing::{error, info, warn}; use crate::{ mavlink::{MavMessage, TimedMessage}, - ui::app::PaneResponse, + ui::{ + app::PaneResponse, + shortcuts::{ShortcutHandler, ShortcutMode}, + }, }; use super::PaneBehavior; -use commands::CommandSM; +use commands::{Command, CommandSM}; use icons::Icon; use valves::{Valve, ValveStateManager}; const DEFAULT_AUTO_REFRESH_RATE: Duration = Duration::from_secs(1); +const SYMBOL_LIST: &str = "123456789-/."; -#[derive(Clone, PartialEq, Default, Serialize, Deserialize, Debug)] +#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)] pub struct ValveControlPane { // INTERNAL #[serde(skip)] @@ -58,12 +60,39 @@ pub struct ValveControlPane { // UI SETTINGS #[serde(skip)] is_settings_window_open: bool, + #[serde(skip)] + valve_symbol_map: HashMap<char, Valve>, + #[serde(skip)] + valve_window_states: HashMap<Valve, ValveWindowState>, +} + +impl Default for ValveControlPane { + fn default() -> Self { + let symbols: Vec<char> = SYMBOL_LIST.chars().collect(); + let valve_symbol = symbols.into_iter().zip(Valve::iter()).collect(); + let valve_window_states = Valve::iter() + .map(|v| (v, ValveWindowState::Closed)) + .collect(); + Self { + valves_state: ValveStateManager::default(), + commands: vec![], + auto_refresh: None, + manual_refresh: false, + last_refresh: None, + is_settings_window_open: false, + valve_symbol_map: valve_symbol, + valve_window_states, + } + } } impl PaneBehavior for ValveControlPane { - fn ui(&mut self, ui: &mut Ui) -> PaneResponse { + fn ui(&mut self, ui: &mut Ui, shortcut_handler: &mut ShortcutHandler) -> PaneResponse { let mut pane_response = PaneResponse::default(); + // Set this to at least double the maximum icon size used + Icon::init_cache(ui.ctx(), (100, 100)); + let res = ui .scope_builder(UiBuilder::new().sense(Sense::click_and_drag()), |ui| { self.pane_ui()(ui); @@ -79,13 +108,46 @@ impl PaneBehavior for ValveControlPane { pane_response.set_drag_started(); } - egui::Window::new("Settings") + // capture actions from keyboard shortcuts + let action_to_pass = self.keyboard_actions(shortcut_handler); + + match action_to_pass { + // Open the valve control window if the action is to open it + Some(PaneAction::OpenValveControl(valve)) => { + self.valve_window_states + .insert(valve, ValveWindowState::Open); + } + // Close if the user requests so + Some(PaneAction::CloseValveControls) => { + warn!("closing all"); + for valve in Valve::iter() { + self.valve_window_states + .insert(valve, ValveWindowState::Closed); + } + } + // Ignore otherwise + _ => {} + } + + Window::new("Settings") .id(ui.auto_id_with("settings")) .auto_sized() .collapsible(true) .movable(true) .open(&mut self.is_settings_window_open) - .show(ui.ctx(), Self::window_ui(&mut self.auto_refresh)); + .show(ui.ctx(), Self::settings_window_ui(&mut self.auto_refresh)); + + if let Some(valve_window_open) = self + .valve_window_states + .iter() + .find(|&(_, state)| !state.is_closed()) + .map(|(&v, _)| v) + { + Modal::new(ui.auto_id_with(format!("valve_control {}", valve_window_open))).show( + ui.ctx(), + self.valve_control_window_ui(valve_window_open, action_to_pass), + ); + } pane_response } @@ -118,7 +180,7 @@ impl PaneBehavior for ValveControlPane { // intercept all ACK/NACK/WACK messages cmd.capture_response(&message.message); // If a response was captured, consume the command and update the valve state - if let Some((valve, parameter)) = cmd.consume_response() { + if let Some((valve, Some(parameter))) = cmd.consume_response() { self.valves_state.set_parameter_of(valve, parameter); } } @@ -152,15 +214,14 @@ impl ValveControlPane { fn pane_ui(&mut self) -> impl FnOnce(&mut Ui) { |ui| { ui.set_min_width(BTN_MAX_WIDTH); - let n = ((ui.max_rect().width() / BTN_MAX_WIDTH) as usize).max(1); - let symbols: Vec<char> = "123456789-/*".chars().collect(); - let valve_chunks = Valve::iter().zip(symbols).chunks(n); + let n = (ui.max_rect().width() / BTN_MAX_WIDTH) as usize; + let valve_chunks = SYMBOL_LIST.chars().zip(Valve::iter()).chunks(n.max(1)); Grid::new("valves_grid") .num_columns(n) .spacing(Vec2::splat(5.)) .show(ui, |ui| { for chunk in &valve_chunks { - for (valve, symbol) in chunk { + for (symbol, valve) in chunk { ui.scope(self.valve_frame_ui(valve, symbol)); } ui.end_row(); @@ -182,7 +243,7 @@ impl ValveControlPane { } } - fn window_ui(auto_refresh_setting: &mut Option<Duration>) -> impl FnOnce(&mut Ui) { + fn settings_window_ui(auto_refresh_setting: &mut Option<Duration>) -> impl FnOnce(&mut Ui) { |ui| { // Display auto refresh setting let mut auto_refresh = auto_refresh_setting.is_some(); @@ -287,9 +348,12 @@ impl ValveControlPane { ui.vertical(|ui| { ui.set_min_width(80.); ui.horizontal_top(|ui| { - let rect = Rect::from_min_size(ui.cursor().min, icon_size); - Icon::Timing.paint(ui, rect); - ui.allocate_rect(rect, Sense::hover()); + ui.add( + Icon::Timing + .as_image(ui.ctx().theme()) + .fit_to_exact_size(icon_size) + .sense(Sense::hover()), + ); ui.allocate_ui(vec2(20., 10.), |ui| { let layout_job = LayoutJob::single_section(timing_str.clone(), text_format.clone()); @@ -298,9 +362,12 @@ impl ValveControlPane { }); }); ui.horizontal_top(|ui| { - let rect = Rect::from_min_size(ui.cursor().min, icon_size); - Icon::Aperture.paint(ui, rect); - ui.allocate_rect(rect, Sense::hover()); + ui.add( + Icon::Aperture + .as_image(ui.ctx().theme()) + .fit_to_exact_size(icon_size) + .sense(Sense::hover()), + ); let layout_job = LayoutJob::single_section(aperture_str.clone(), text_format); let galley = ui.fonts(|fonts| fonts.layout_job(layout_job)); @@ -364,6 +431,200 @@ impl ValveControlPane { ); } } + + const WIGGLE_KEY: Key = Key::Minus; + const TIMING_KEY: Key = Key::Slash; + const APERTURE_KEY: Key = Key::Period; + + fn valve_control_window_ui( + &mut self, + valve: Valve, + action: Option<PaneAction>, + ) -> impl FnOnce(&mut Ui) { + move |ui| { + let icon_size = Vec2::splat(25.); + let text_size = 16.; + + fn btn_ui<R>( + valve: Valve, + add_contents: impl FnOnce(&mut Ui) -> R, + ) -> impl FnOnce(&mut Ui) -> Response { + move |ui| { + let mut wiggle_btn = Frame::canvas(ui.style()) + .inner_margin(ui.spacing().menu_margin) + .corner_radius(ui.visuals().noninteractive().corner_radius); + + wiggle_btn = ui.ctx().input(|input| { + if input.key_down(ValveControlPane::WIGGLE_KEY) { + wiggle_btn + .fill(ui.visuals().widgets.active.bg_fill) + .stroke(ui.visuals().widgets.active.fg_stroke) + } else { + wiggle_btn + .fill(ui.visuals().widgets.inactive.bg_fill.gamma_multiply(0.3)) + .stroke(Stroke::new(2.0, Color32::TRANSPARENT)) + } + }); + + ui.scope_builder( + UiBuilder::new() + .id_salt(format!("valve_control_window_{}_wiggle", valve)) + .sense(Sense::click()), + |ui| { + wiggle_btn.show(ui, |ui| ui.horizontal(|ui| add_contents(ui))); + }, + ) + .response + } + } + + let wiggle_btn_response = btn_ui(valve, |ui| { + ui.add( + Icon::Aperture + .as_image(ui.ctx().theme()) + .fit_to_exact_size(icon_size), + ); + ui.add(Label::new(RichText::new("Wiggle").size(text_size)).selectable(false)); + })(ui); + + let mut aperture = 0_u32; + let aperture_btn_response = btn_ui(valve, |ui| { + ui.add( + Icon::Aperture + .as_image(ui.ctx().theme()) + .fit_to_exact_size(icon_size), + ); + ui.add(Label::new(RichText::new("Aperture: ").size(text_size)).selectable(false)); + ui.add( + DragValue::new(&mut aperture) + .speed(0.5) + .range(0.0..=100.0) + .fixed_decimals(1) + .update_while_editing(false) + .suffix("%"), + ); + })(ui); + + let mut timing_ms = 0_u32; + let timing_btn_response = btn_ui(valve, |ui| { + ui.add( + Icon::Timing + .as_image(ui.ctx().theme()) + .fit_to_exact_size(icon_size), + ); + ui.add(Label::new(RichText::new("Timing: ").size(text_size)).selectable(false)); + ui.add( + DragValue::new(&mut timing_ms) + .speed(1) + .range(1..=10000) + .fixed_decimals(0) + .update_while_editing(false) + .suffix(" [ms]"), + ); + })(ui); + + if wiggle_btn_response.clicked() || matches!(action, Some(PaneAction::Wiggle)) { + info!("Wiggle valve: {:?}", valve); + self.commands.push(Command::wiggle(valve).into()); + } + // self.valve_window_states + // .insert(valve, ValveWindowState::Closed); + } + } + + fn keyboard_actions(&self, shortcut_handler: &mut ShortcutHandler) -> Option<PaneAction> { + let mut key_action_pairs = Vec::new(); + match self + .valve_window_states + .iter() + .find(|&(_, open)| !open.is_closed()) + { + Some((&valve, state)) => { + shortcut_handler.activate_mode(ShortcutMode::valve_control()); + match state { + ValveWindowState::Open => { + // A window is open, so we can map the keys to control the valve + key_action_pairs.push(( + Modifiers::NONE, + Self::WIGGLE_KEY, + PaneAction::Wiggle, + )); + key_action_pairs.push(( + Modifiers::NONE, + Self::TIMING_KEY, + PaneAction::FocusOnTiming, + )); + key_action_pairs.push(( + Modifiers::NONE, + Self::APERTURE_KEY, + PaneAction::FocusOnAperture, + )); + key_action_pairs.push(( + Modifiers::NONE, + Key::Escape, + PaneAction::CloseValveControls, + )); + } + ValveWindowState::TimingFocused => { + // The timing field is focused, so we can map the keys to control the timing + key_action_pairs.push((Modifiers::NONE, Key::Enter, PaneAction::SetTiming)); + key_action_pairs.push(( + Modifiers::NONE, + Key::Escape, + PaneAction::OpenValveControl(valve), + )); + } + ValveWindowState::ApertureFocused => { + // The aperture field is focused, so we can map the keys to control the aperture + key_action_pairs.push(( + Modifiers::NONE, + Key::Enter, + PaneAction::SetAperture, + )); + key_action_pairs.push(( + Modifiers::NONE, + Key::Escape, + PaneAction::OpenValveControl(valve), + )); + } + ValveWindowState::Closed => unreachable!(), + } + shortcut_handler + .consume_if_mode_is(ShortcutMode::valve_control(), &key_action_pairs[..]) + } + None => { + shortcut_handler.deactivate_mode(ShortcutMode::valve_control()); + // No window is open, so we can map the keys to open the valve control windows + for &symbol in self.valve_symbol_map.keys() { + let key = match symbol { + '1' => Key::Num1, + '2' => Key::Num2, + '3' => Key::Num3, + '4' => Key::Num4, + '5' => Key::Num5, + '6' => Key::Num6, + '7' => Key::Num7, + '8' => Key::Num8, + '9' => Key::Num9, + '-' => Key::Minus, + '/' => Key::Slash, + '.' => Key::Period, + _ => { + error!("Invalid symbol: {}", symbol); + panic!("Invalid symbol: {}", symbol); + } + }; + key_action_pairs.push(( + Modifiers::NONE, + key, + PaneAction::OpenValveControl(self.valve_symbol_map[&symbol]), + )); + } + shortcut_handler + .consume_if_mode_is(ShortcutMode::composition(), &key_action_pairs[..]) + } + } + } } // ┌───────────────────────────┐ @@ -389,3 +650,39 @@ impl ValveControlPane { self.manual_refresh = false; } } + +#[derive(Debug, Clone, Copy)] +enum PaneAction { + OpenValveControl(Valve), + CloseValveControls, + Wiggle, + SetTiming, + SetAperture, + FocusOnTiming, + FocusOnAperture, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ValveWindowState { + Closed, + Open, + TimingFocused, + ApertureFocused, +} + +impl ValveWindowState { + #[inline] + fn is_open(&self) -> bool { + matches!(self, Self::Open) + } + + #[inline] + fn is_closed(&self) -> bool { + matches!(self, Self::Closed) + } + + #[inline] + fn is_focused(&self) -> bool { + matches!(self, Self::TimingFocused | Self::ApertureFocused) + } +} diff --git a/src/ui/panes/valve_control/commands.rs b/src/ui/panes/valve_control/commands.rs index 8593fd9be94a5e594de9cae0dc8a635bc81e1209..ed4d282dfe6ed98650e67ebaa07a50d80ba0639a 100644 --- a/src/ui/panes/valve_control/commands.rs +++ b/src/ui/panes/valve_control/commands.rs @@ -1,3 +1,7 @@ +use std::time::{Duration, Instant}; + +use skyward_mavlink::orion::WIGGLE_SERVO_TC_DATA; + use crate::mavlink::{ ACK_TM_DATA, MavMessage, MessageData, NACK_TM_DATA, SET_ATOMIC_VALVE_TIMING_TC_DATA, SET_VALVE_MAXIMUM_APERTURE_TC_DATA, WACK_TM_DATA, @@ -8,25 +12,35 @@ use super::valves::{ParameterValue, Valve, ValveParameter}; #[derive(Debug, Clone, PartialEq)] pub enum CommandSM { Request(Command), - WaitingForResponse(Command), - Response((Valve, ValveParameter)), + WaitingForResponse((Instant, Command)), + Response((Valve, Option<ValveParameter>)), Consumed, } impl CommandSM { pub fn pack_and_wait(&mut self) -> Option<MavMessage> { match self { - CommandSM::Request(command) => { + Self::Request(command) => { let message = MavMessage::from(command.clone()); - *self = CommandSM::WaitingForResponse(command.clone()); + *self = CommandSM::WaitingForResponse((Instant::now(), command.clone())); Some(message) } _ => None, } } + pub fn cancel_expired(&mut self, timeout: Duration) { + if let Self::WaitingForResponse((instant, cmd)) = self { + if instant.elapsed() > timeout { + let Command { kind, valve } = cmd; + // *self = Self::Response(valve, kind.to_invalid_parameter(error)); + todo!() // TODO + } + } + } + pub fn capture_response(&mut self, message: &MavMessage) { - if let CommandSM::WaitingForResponse(Command { kind, valve }) = self { + if let Self::WaitingForResponse((_, Command { kind, valve })) = self { let id = kind.message_id() as u8; match message { MavMessage::ACK_TM(ACK_TM_DATA { recv_msgid, .. }) if *recv_msgid == id => { @@ -47,9 +61,9 @@ impl CommandSM { } } - pub fn consume_response(&mut self) -> Option<(Valve, ValveParameter)> { + pub fn consume_response(&mut self) -> Option<(Valve, Option<ValveParameter>)> { match self { - CommandSM::Response((valve, parameter)) => { + Self::Response((valve, parameter)) => { let res = Some((*valve, parameter.clone())); *self = CommandSM::Consumed; res @@ -59,17 +73,17 @@ impl CommandSM { } pub fn is_waiting_for_response(&self) -> bool { - matches!(self, CommandSM::WaitingForResponse(_)) + matches!(self, Self::WaitingForResponse(_)) } pub fn is_consumed(&self) -> bool { - matches!(self, CommandSM::Consumed) + matches!(self, Self::Consumed) } } impl From<Command> for CommandSM { fn from(value: Command) -> Self { - CommandSM::Request(value) + Self::Request(value) } } @@ -100,17 +114,37 @@ pub struct Command { valve: Valve, } +impl Command { + pub fn wiggle(valve: Valve) -> Self { + Self { + kind: CommandKind::Wiggle, + valve, + } + } + + pub fn set_atomic_valve_timing(valve: Valve, timing: u32) -> Self { + valve.set_atomic_valve_timing(timing) + } + + pub fn set_valve_maximum_aperture(valve: Valve, aperture: f32) -> Self { + valve.set_valve_maximum_aperture(aperture) + } +} + impl From<Command> for MavMessage { fn from(value: Command) -> Self { match value.kind { + CommandKind::Wiggle => Self::WIGGLE_SERVO_TC(WIGGLE_SERVO_TC_DATA { + servo_id: value.valve.into(), + }), CommandKind::SetAtomicValveTiming(timing) => { - MavMessage::SET_ATOMIC_VALVE_TIMING_TC(SET_ATOMIC_VALVE_TIMING_TC_DATA { + Self::SET_ATOMIC_VALVE_TIMING_TC(SET_ATOMIC_VALVE_TIMING_TC_DATA { servo_id: value.valve.into(), maximum_timing: timing, }) } CommandKind::SetValveMaximumAperture(aperture) => { - MavMessage::SET_VALVE_MAXIMUM_APERTURE_TC(SET_VALVE_MAXIMUM_APERTURE_TC_DATA { + Self::SET_VALVE_MAXIMUM_APERTURE_TC(SET_VALVE_MAXIMUM_APERTURE_TC_DATA { servo_id: value.valve.into(), maximum_aperture: aperture, }) @@ -121,6 +155,7 @@ impl From<Command> for MavMessage { #[derive(Debug, Clone, Copy, PartialEq)] enum CommandKind { + Wiggle, SetAtomicValveTiming(u32), SetValveMaximumAperture(f32), } @@ -128,35 +163,40 @@ enum CommandKind { impl CommandKind { fn message_id(&self) -> u32 { match self { - CommandKind::SetAtomicValveTiming(_) => SET_ATOMIC_VALVE_TIMING_TC_DATA::ID, - CommandKind::SetValveMaximumAperture(_) => SET_VALVE_MAXIMUM_APERTURE_TC_DATA::ID, + Self::Wiggle => WIGGLE_SERVO_TC_DATA::ID, + Self::SetAtomicValveTiming(_) => SET_ATOMIC_VALVE_TIMING_TC_DATA::ID, + Self::SetValveMaximumAperture(_) => SET_VALVE_MAXIMUM_APERTURE_TC_DATA::ID, } } - fn to_valid_parameter(&self) -> ValveParameter { - (*self).into() + fn to_valid_parameter(&self) -> Option<ValveParameter> { + (*self).try_into().ok() } - fn to_invalid_parameter(&self, error: u16) -> ValveParameter { + fn to_invalid_parameter(&self, error: u16) -> Option<ValveParameter> { match self { - CommandKind::SetAtomicValveTiming(_) => { - ValveParameter::AtomicValveTiming(ParameterValue::Invalid(error)) - } - CommandKind::SetValveMaximumAperture(_) => { - ValveParameter::ValveMaximumAperture(ParameterValue::Invalid(error)) - } + Self::Wiggle => None, + Self::SetAtomicValveTiming(_) => Some(ValveParameter::AtomicValveTiming( + ParameterValue::Invalid(error), + )), + Self::SetValveMaximumAperture(_) => Some(ValveParameter::ValveMaximumAperture( + ParameterValue::Invalid(error), + )), } } } -impl From<CommandKind> for ValveParameter { - fn from(value: CommandKind) -> Self { +impl TryFrom<CommandKind> for ValveParameter { + type Error = (); + + fn try_from(value: CommandKind) -> Result<Self, Self::Error> { match value { + CommandKind::Wiggle => Err(()), CommandKind::SetAtomicValveTiming(timing) => { - ValveParameter::AtomicValveTiming(ParameterValue::Valid(timing)) + Ok(Self::AtomicValveTiming(ParameterValue::Valid(timing))) } CommandKind::SetValveMaximumAperture(aperture) => { - ValveParameter::ValveMaximumAperture(ParameterValue::Valid(aperture)) + Ok(Self::ValveMaximumAperture(ParameterValue::Valid(aperture))) } } } diff --git a/src/ui/panes/valve_control/icons.rs b/src/ui/panes/valve_control/icons.rs index 064fee550c449c9a1a8e7ff479068f79f942d0b5..f8d70a96c88d77ec728d7b4bcf3648cef2b179b4 100644 --- a/src/ui/panes/valve_control/icons.rs +++ b/src/ui/panes/valve_control/icons.rs @@ -1,13 +1,18 @@ -use egui::{ImageSource, Rect, Theme, Ui}; +use egui::{Context, Image, ImageSource, SizeHint, TextureOptions, Theme, Ui}; +use strum::IntoEnumIterator; +use strum_macros::EnumIter; +use tracing::error; -#[derive(Debug, Clone, Copy)] +use crate::error::ErrInstrument; + +#[derive(Debug, Clone, Copy, EnumIter)] pub enum Icon { Aperture, Timing, } impl Icon { - fn get_image(&self, theme: Theme) -> ImageSource { + fn as_image_source(&self, theme: Theme) -> ImageSource { match (&self, theme) { (Icon::Aperture, Theme::Light) => { egui::include_image!(concat!( @@ -35,11 +40,25 @@ impl Icon { } } } -} -impl Icon { - pub fn paint(&mut self, ui: &mut Ui, image_rect: Rect) { - let theme = ui.ctx().theme(); - egui::Image::new(self.get_image(theme)).paint_at(ui, image_rect); + pub fn init_cache(ctx: &Context, size_hint: (u32, u32)) { + let size_hint = SizeHint::Size(size_hint.0, size_hint.1); + for icon in Self::iter() { + if let Err(e) = + icon.as_image_source(ctx.theme()) + .load(ctx, TextureOptions::LINEAR, size_hint) + { + error!("Error loading icons: {}", e); + } + } + } + + pub fn as_image(&self, theme: Theme) -> Image { + Image::new(self.as_image_source(theme)) + } + + pub fn reset_cache(&self, ui: &mut Ui) { + let img: Image = self.as_image(ui.ctx().theme()); + ui.ctx().forget_image(img.uri().log_unwrap()); } } diff --git a/src/ui/panes/valve_control/valves.rs b/src/ui/panes/valve_control/valves.rs index 9580dc169d8000ea01aaa283c596c18e68c12231..cf13fcb154b487b914fccbe941c72aa5d1735f2f 100644 --- a/src/ui/panes/valve_control/valves.rs +++ b/src/ui/panes/valve_control/valves.rs @@ -63,7 +63,7 @@ impl ValveStateManager { } #[allow(non_camel_case_types)] -#[derive(Clone, Copy, Debug, PartialEq, Eq, EnumIter)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, EnumIter, Hash)] pub enum Valve { OxFilling, OxRelease, @@ -131,9 +131,9 @@ pub enum ParameterValue<T, E> { impl<T: Display, E: Display> Display for ParameterValue<T, E> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - ParameterValue::Valid(value) => write!(f, "{}", value), - ParameterValue::Missing => write!(f, "MISSING"), - ParameterValue::Invalid(error) => write!(f, "INVALID: {}", error), + Self::Valid(value) => write!(f, "{}", value), + Self::Missing => write!(f, "MISSING"), + Self::Invalid(error) => write!(f, "INVALID: {}", error), } } } diff --git a/src/ui/shortcuts.rs b/src/ui/shortcuts.rs index 7efa69f2ac6fc1c01bf85380dc7b44437bb55e5a..d95bcaf8d63d2c12872ce2fe24354aa7721c21fa 100644 --- a/src/ui/shortcuts.rs +++ b/src/ui/shortcuts.rs @@ -1,10 +1,165 @@ +use std::collections::HashSet; + use egui::{Context, Key, KeyboardShortcut, Modifiers}; -pub fn map_to_action<A: Clone>(ctx: &Context, pairs: &[((Modifiers, Key), A)]) -> Option<A> { - ctx.input_mut(|i| { - pairs.iter().find_map(|((modifier, key), action)| { - i.consume_shortcut(&KeyboardShortcut::new(*modifier, *key)) - .then_some(action.to_owned()) - }) - }) +/// Contains all keyboard shortcuts added by the UI. +/// +/// [`ShortcutHandler`] is used to register shortcuts and consume them, while +/// keeping tracks of all enabled shortcuts and filter active shortcut based on +/// UI views and modes (see [`ShortcutModeStack`]). +#[derive(Debug, Clone)] +pub struct ShortcutHandler { + /// The egui context. Needed to consume shortcuts. + ctx: Context, + + /// Set of all enabled shortcuts. + enabled_shortcuts: HashSet<KeyboardShortcut>, + + /// Stack layers of keyboard shortcuts. Controls which shortcuts are active at any given time. + mode_stack: ShortcutModeStack, +} + +impl ShortcutHandler { + pub fn new(ctx: Context) -> Self { + Self { + ctx, + enabled_shortcuts: Default::default(), + mode_stack: Default::default(), + } + } + + fn add_shortcut_action_pair<A>( + &mut self, + modifier: Modifiers, + key: Key, + action: A, + mode: ShortcutMode, + ) -> Option<A> { + let shortcut = KeyboardShortcut::new(modifier, key); + if self.mode_stack.is_active(mode) { + let action = self + .ctx + .input_mut(|i| i.consume_shortcut(&shortcut).then_some(action)); + self.enabled_shortcuts.insert(shortcut); + action + } else { + None + } + } + + /// Consume the keyboard shortcut provided and return the action associated + /// with it if the active mode is the provided one. + pub fn consume_if_mode_is<A: Clone>( + &mut self, + mode: ShortcutMode, + shortcuts: &[(Modifiers, Key, A)], + ) -> Option<A> { + for (modifier, key, action) in shortcuts { + if let Some(action) = self.add_shortcut_action_pair(*modifier, *key, action, mode) { + return Some(action.clone()); + }; + } + None + } + + /// Activate a mode (see [`ShortcutModeStack`] for more). + #[inline] + pub fn activate_mode(&mut self, mode: ShortcutMode) { + if !self.mode_stack.is_active(mode) { + self.mode_stack.activate(mode); + self.enabled_shortcuts.clear(); + } + } + + /// Deactivate a mode, switching back to the previous layer (if any). + #[inline] + pub fn deactivate_mode(&mut self, mode: ShortcutMode) { + if self.mode_stack.is_active(mode) { + self.mode_stack.deactivate(mode); + self.enabled_shortcuts.clear(); + } + } +} + +/// Stack layers of keyboard shortcuts. Controls which shortcuts are active at any given time. +/// +/// The first layer is the default layer, which is active when the user is in the main view. +/// The second layer is active when the user is in a modal/dialog/window that needs full keyboard control. +/// When the modal/dialog/window is closed the second layer is removed and the first layer is active again. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +struct ShortcutModeStack { + first: FirstLayerModes, + second: Option<SecondLayerModes>, +} + +impl ShortcutModeStack { + fn is_active(&self, mode: ShortcutMode) -> bool { + match mode { + ShortcutMode::FirstLayer(first) => self.first == first && self.second.is_none(), + ShortcutMode::SecondLayer(second) => self.second == Some(second), + } + } + + fn activate(&mut self, mode: ShortcutMode) { + match mode { + ShortcutMode::FirstLayer(first) => { + self.first = first; + self.second = None; + } + ShortcutMode::SecondLayer(second) => self.second = Some(second), + } + } + + fn deactivate(&mut self, mode: ShortcutMode) { + match mode { + ShortcutMode::FirstLayer(first) => { + if self.first == first { + self.first = FirstLayerModes::default(); + } + } + ShortcutMode::SecondLayer(second) => { + if self.second == Some(second) { + self.second = None; + } + } + } + } +} + +/// Layers of keyboard shortcuts. See [`ShortcutModeStack`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ShortcutMode { + FirstLayer(FirstLayerModes), + SecondLayer(SecondLayerModes), +} + +impl ShortcutMode { + #[inline] + pub fn composition() -> Self { + Self::FirstLayer(FirstLayerModes::Composition) + } + + #[inline] + pub fn valve_control() -> Self { + Self::SecondLayer(SecondLayerModes::ValveControl) + } +} + +/// First layer of keyboard shortcuts. +/// +/// Active when the user is on the main view choosing how to customize their layout. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum FirstLayerModes { + /// Shortcuts that are active when the user is in the main menu. + #[default] + Composition, +} + +/// Second layer of keyboard shortcuts, sits on top of the first layer. +/// +/// Active when the user is in a modal, dialog or window that needs full keyboard control. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SecondLayerModes { + /// Shortcuts that are active when the user is in the main menu. + ValveControl, } diff --git a/src/ui/utils.rs b/src/ui/utils.rs index 2469c54aa8b79743d4bc3f9f081593ffe26d71be..7d1b19344d0c505be71491a1e4976e58f6912fb2 100644 --- a/src/ui/utils.rs +++ b/src/ui/utils.rs @@ -1,16 +1,19 @@ use egui::containers::Frame; use egui::{Response, Shadow, Stroke, Style, Ui}; -use super::panes::{Pane, PaneBehavior}; +use super::{ + panes::{Pane, PaneBehavior}, + shortcuts::ShortcutHandler, +}; /// This function wraps a ui into a popup frame intended for the pane that needs /// to be maximized on screen. -pub fn maximized_pane_ui(ui: &mut Ui, pane: &mut Pane) { +pub fn maximized_pane_ui(ui: &mut Ui, pane: &mut Pane, shortcut_handler: &mut ShortcutHandler) { Frame::popup(&Style::default()) .fill(egui::Color32::TRANSPARENT) .shadow(Shadow::NONE) .stroke(Stroke::NONE) - .show(ui, |ui| pane.ui(ui)); + .show(ui, |ui| pane.ui(ui, shortcut_handler)); } #[derive(Debug, Default, Clone)]