From e9ac853a06108cfaa0e37be2e63af9fdcd1fe7b1 Mon Sep 17 00:00:00 2001 From: Federico Lolli <federico.lolli@skywarder.eu> Date: Tue, 25 Mar 2025 00:41:19 +0100 Subject: [PATCH] CHECKPOINT moved code for valve control window to own file --- src/ui/panes/valve_control.rs | 353 ++---------------- src/ui/panes/valve_control/ui.rs | 70 +--- .../panes/valve_control/ui/shortcut_widget.rs | 66 ++++ .../valve_control/ui/valve_control_window.rs | 297 +++++++++++++++ 4 files changed, 398 insertions(+), 388 deletions(-) create mode 100644 src/ui/panes/valve_control/ui/shortcut_widget.rs create mode 100644 src/ui/panes/valve_control/ui/valve_control_window.rs diff --git a/src/ui/panes/valve_control.rs b/src/ui/panes/valve_control.rs index 793d004..73289d7 100644 --- a/src/ui/panes/valve_control.rs +++ b/src/ui/panes/valve_control.rs @@ -9,9 +9,8 @@ use std::{ }; use egui::{ - Color32, DragValue, FontId, Frame, Grid, Key, KeyboardShortcut, Label, Modal, Modifiers, - Response, RichText, Sense, Stroke, TextFormat, Ui, UiBuilder, Vec2, Widget, Window, - text::LayoutJob, vec2, + Color32, DragValue, FontId, Frame, Grid, Key, Label, Modifiers, Response, RichText, Sense, + Stroke, TextFormat, Ui, UiBuilder, Vec2, Widget, Window, text::LayoutJob, vec2, }; use itertools::Itertools; use serde::{Deserialize, Serialize}; @@ -20,8 +19,7 @@ use skyward_mavlink::{ orion::{ACK_TM_DATA, NACK_TM_DATA, WACK_TM_DATA}, }; use strum::IntoEnumIterator; -use tracing::{info, trace, warn}; -use ui::ShortcutCard; +use tracing::info; use crate::{ mavlink::{MavMessage, TimedMessage}, @@ -33,8 +31,9 @@ use crate::{ use super::PaneBehavior; -use commands::{Command, CommandSM}; +use commands::CommandSM; use icons::Icon; +use ui::{ShortcutCard, ValveControlWindow, map_key_to_shortcut}; use valves::{Valve, ValveStateManager}; const DEFAULT_AUTO_REFRESH_RATE: Duration = Duration::from_secs(1); @@ -85,7 +84,7 @@ pub struct ValveControlPane { #[serde(skip)] valve_key_map: HashMap<Valve, Key>, #[serde(skip)] - valve_window_states: HashMap<Valve, ValveWindowState>, + valve_window: Option<ValveControlWindow>, } impl Default for ValveControlPane { @@ -94,9 +93,6 @@ impl Default for ValveControlPane { let valve_key_map = Valve::iter() .zip(symbols.into_iter().map(map_symbol_to_key)) .collect(); - let valve_window_states = Valve::iter() - .map(|v| (v, ValveWindowState::Closed)) - .collect(); Self { valves_state: ValveStateManager::default(), commands: vec![], @@ -105,7 +101,7 @@ impl Default for ValveControlPane { last_refresh: None, is_settings_window_open: false, valve_key_map, - valve_window_states, + valve_window: None, } } } @@ -134,22 +130,14 @@ impl PaneBehavior for ValveControlPane { } // capture actions from keyboard shortcuts - let action_to_pass = self.keyboard_actions(shortcut_handler); + let action = self.keyboard_actions(shortcut_handler); - match action_to_pass { + match action { // Open the valve control window if the action is to open it Some(PaneAction::OpenValveControl(valve)) => { - self.set_window_state(valve, ValveWindowState::Open); - } - // Close if the user requests so - Some(PaneAction::CloseValveControls) => { - warn!("closing all"); - for valve in Valve::iter() { - self.set_window_state(valve, ValveWindowState::Closed); - } + self.valve_window.replace(ValveControlWindow::new(valve)); } - // Ignore otherwise - _ => {} + None => {} } Window::new("Settings") @@ -160,20 +148,14 @@ impl PaneBehavior for ValveControlPane { .open(&mut self.is_settings_window_open) .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) - { - trace!( - "Valve control window for valve {} is open", - valve_window_open - ); - 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), - ); + if let Some(valve_window) = &mut self.valve_window { + if let Some(command) = valve_window.ui(ui, shortcut_handler) { + self.commands.push(command.into()); + } + + if valve_window.is_closed() { + self.valve_window = None; + } } pane_response @@ -259,7 +241,7 @@ impl ValveControlPane { if response.clicked() { info!("Clicked on valve: {:?}", valve); - self.set_window_state(valve, ValveWindowState::Open); + self.valve_window = Some(ValveControlWindow::new(valve)); } } ui.end_row(); @@ -399,8 +381,7 @@ impl ValveControlPane { let visuals = ui.style().interact(&response); let (fill_color, btn_fill_color, stroke) = if response.clicked() - || shortcut_key_is_down - && self.valve_window_states.values().all(|&v| v.is_closed()) + || shortcut_key_is_down && self.valve_window.is_none() { let visuals = ui.visuals().widgets.active; (visuals.bg_fill, visuals.bg_fill, visuals.bg_stroke) @@ -444,264 +425,18 @@ 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, - mut action: Option<PaneAction>, - ) -> impl FnOnce(&mut Ui) { - move |ui| { - profiling::function_scope!("valve_control_window_ui"); - let icon_size = Vec2::splat(25.); - let text_size = 16.; - - fn btn_ui<R>( - window_state: &ValveWindowState, - key: Key, - add_contents: impl FnOnce(&mut Ui) -> R, - ) -> impl FnOnce(&mut Ui) -> Response { - move |ui| { - let wiggle_btn = Frame::canvas(ui.style()) - .inner_margin(ui.spacing().menu_margin) - .corner_radius(ui.visuals().noninteractive().corner_radius); - - ui.scope_builder(UiBuilder::new().id_salt(key).sense(Sense::click()), |ui| { - let response = ui.response(); - - let clicked = response.clicked(); - let shortcut_down = ui.ctx().input(|input| input.key_down(key)); - - let visuals = ui.style().interact(&response); - let (fill_color, stroke) = - if clicked || shortcut_down && window_state.is_open() { - let visuals = ui.visuals().widgets.active; - (visuals.bg_fill, visuals.bg_stroke) - } else if response.hovered() { - (visuals.bg_fill, visuals.bg_stroke) - } else { - let stroke = Stroke::new(1., Color32::TRANSPARENT); - (visuals.bg_fill.gamma_multiply(0.3), stroke) - }; - - wiggle_btn - .fill(fill_color) - .stroke(stroke) - .stroke(stroke) - .show(ui, |ui| { - ui.set_width(200.); - ui.horizontal(|ui| add_contents(ui)) - }); - - if response.clicked() { - info!("Clicked!"); - } - }) - .response - } - } - - let window_state = &self.valve_window_states[&valve]; - let wiggle_btn_response = btn_ui(window_state, Self::WIGGLE_KEY, |ui| { - ShortcutCard::new(map_key_to_shortcut(Self::WIGGLE_KEY)) - .text_color(ui.visuals().text_color()) - .fill_color(ui.visuals().widgets.inactive.bg_fill) - .text_size(20.) - .ui(ui); - ui.add( - Icon::Wiggle - .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 = self.valves_state.get_aperture_for(valve).valid_or(0.5) * 100.; - let aperture_btn_response = btn_ui(window_state, Self::APERTURE_KEY, |ui| { - ShortcutCard::new(map_key_to_shortcut(Self::APERTURE_KEY)) - .text_color(ui.visuals().text_color()) - .fill_color(ui.visuals().widgets.inactive.bg_fill) - .text_size(20.) - .ui(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)); - let drag_value_id = ui.next_auto_id(); - ui.add( - DragValue::new(&mut aperture) - .speed(0.5) - .range(0.0..=100.0) - .fixed_decimals(0) - .update_while_editing(false) - .suffix("%"), - ); - if matches!(window_state, ValveWindowState::ApertureFocused) { - ui.ctx().memory_mut(|m| { - m.request_focus(drag_value_id); - }); - } - })(ui); - - let mut timing_ms = 0_u32; - let timing_btn_response = btn_ui(window_state, Self::TIMING_KEY, |ui| { - ShortcutCard::new(map_key_to_shortcut(Self::TIMING_KEY)) - .text_color(ui.visuals().text_color()) - .fill_color(ui.visuals().widgets.inactive.bg_fill) - .text_size(20.) - .ui(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)); - let drag_value_id = ui.next_auto_id(); - ui.add( - DragValue::new(&mut timing_ms) - .speed(1) - .range(1..=10000) - .fixed_decimals(0) - .update_while_editing(false) - .suffix(" [ms]"), - ); - if matches!(window_state, ValveWindowState::TimingFocused) { - ui.ctx().memory_mut(|m| { - m.request_focus(drag_value_id); - }); - } - })(ui); - - // consider that action may be different that null if a keyboard shortcut was captured - if wiggle_btn_response.clicked() { - action = Some(PaneAction::Wiggle); - } else if aperture_btn_response.clicked() { - action = Some(PaneAction::SetAperture); - } else if timing_btn_response.clicked() { - action = Some(PaneAction::SetTiming); - } - - match action { - Some(PaneAction::Wiggle) => { - info!("Issued command to Wiggle valve: {:?}", valve); - self.commands.push(Command::wiggle(valve).into()); - } - Some(PaneAction::SetTiming) => { - info!( - "Issued command to set timing for valve {:?} to {} ms", - valve, timing_ms - ); - self.commands - .push(Command::set_atomic_valve_timing(valve, timing_ms).into()); - self.set_window_state(valve, ValveWindowState::Open); - } - Some(PaneAction::SetAperture) => { - info!( - "Issued command to set aperture for valve {:?} to {}%", - valve, aperture - ); - self.commands - .push(Command::set_valve_maximum_aperture(valve, aperture / 100.).into()); - self.set_window_state(valve, ValveWindowState::Open); - } - Some(PaneAction::FocusOnTiming) => { - self.set_window_state(valve, ValveWindowState::TimingFocused); - } - Some(PaneAction::FocusOnAperture) => { - self.set_window_state(valve, ValveWindowState::ApertureFocused); - } - _ => {} - } - } - } - #[profiling::function] 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 (&valve, &key) in self.valve_key_map.iter() { - key_action_pairs.push(( - Modifiers::NONE, - key, - PaneAction::OpenValveControl(valve), - )); - } - shortcut_handler - .consume_if_mode_is(ShortcutMode::composition(), &key_action_pairs[..]) - } + shortcut_handler.deactivate_mode(ShortcutMode::valve_control()); + // No window is open, so we can map the keys to open the valve control windows + for (&valve, &key) in self.valve_key_map.iter() { + key_action_pairs.push((Modifiers::NONE, key, PaneAction::OpenValveControl(valve))); } + shortcut_handler.consume_if_mode_is(ShortcutMode::composition(), &key_action_pairs[..]) } } -#[inline] -fn map_key_to_shortcut(key: Key) -> KeyboardShortcut { - KeyboardShortcut::new(Modifiers::NONE, key) -} - // ┌───────────────────────────┐ // │ UTILS METHODS │ // └───────────────────────────┘ @@ -724,45 +459,9 @@ impl ValveControlPane { self.last_refresh = Some(Instant::now()); self.manual_refresh = false; } - - #[inline] - fn set_window_state(&mut self, valve: Valve, state: ValveWindowState) { - self.valve_window_states.insert(valve, state); - } } #[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/ui.rs b/src/ui/panes/valve_control/ui.rs index 9bbc056..2c5a1d3 100644 --- a/src/ui/panes/valve_control/ui.rs +++ b/src/ui/panes/valve_control/ui.rs @@ -1,66 +1,14 @@ -use egui::{ - Color32, FontId, Frame, KeyboardShortcut, Label, Margin, ModifierNames, RichText, Stroke, - Widget, -}; +mod shortcut_widget; +mod valve_control_window; -pub struct ShortcutCard { - shortcut: KeyboardShortcut, - text_size: f32, - text_color: Option<Color32>, - fill_color: Option<Color32>, -} - -impl Widget for ShortcutCard { - fn ui(self, ui: &mut egui::Ui) -> egui::Response { - #[cfg(target_os = "macos")] - let is_mac = true; - #[cfg(not(target_os = "macos"))] - let is_mac = false; - - let shortcut_fmt = self.shortcut.format(&ModifierNames::SYMBOLS, is_mac); - let default_style = ui.style().noninteractive(); - let text_color = self.text_color.unwrap_or(default_style.text_color()); - let fill_color = self.fill_color.unwrap_or(default_style.bg_fill); - let corner_radius = default_style.corner_radius; - - let number = RichText::new(shortcut_fmt) - .color(text_color) - .font(FontId::monospace(self.text_size)); - - Frame::canvas(ui.style()) - .fill(fill_color) - .stroke(Stroke::NONE) - .inner_margin(Margin::same(5)) - .corner_radius(corner_radius) - .show(ui, |ui| { - Label::new(number).selectable(false).ui(ui); - }) - .response - } -} - -impl ShortcutCard { - pub fn new(shortcut: KeyboardShortcut) -> Self { - Self { - shortcut, - text_size: 20., - text_color: None, - fill_color: None, - } - } +use egui::{Key, KeyboardShortcut, Modifiers}; - pub fn text_size(mut self, text_size: f32) -> Self { - self.text_size = text_size; - self - } +// Re-export the modules for the UI modules +use super::{commands, icons, valves}; - pub fn text_color(mut self, text_color: Color32) -> Self { - self.text_color = Some(text_color); - self - } +pub use {shortcut_widget::ShortcutCard, valve_control_window::ValveControlWindow}; - pub fn fill_color(mut self, fill_color: Color32) -> Self { - self.fill_color = Some(fill_color); - self - } +#[inline] +pub fn map_key_to_shortcut(key: Key) -> KeyboardShortcut { + KeyboardShortcut::new(Modifiers::NONE, key) } diff --git a/src/ui/panes/valve_control/ui/shortcut_widget.rs b/src/ui/panes/valve_control/ui/shortcut_widget.rs new file mode 100644 index 0000000..9bbc056 --- /dev/null +++ b/src/ui/panes/valve_control/ui/shortcut_widget.rs @@ -0,0 +1,66 @@ +use egui::{ + Color32, FontId, Frame, KeyboardShortcut, Label, Margin, ModifierNames, RichText, Stroke, + Widget, +}; + +pub struct ShortcutCard { + shortcut: KeyboardShortcut, + text_size: f32, + text_color: Option<Color32>, + fill_color: Option<Color32>, +} + +impl Widget for ShortcutCard { + fn ui(self, ui: &mut egui::Ui) -> egui::Response { + #[cfg(target_os = "macos")] + let is_mac = true; + #[cfg(not(target_os = "macos"))] + let is_mac = false; + + let shortcut_fmt = self.shortcut.format(&ModifierNames::SYMBOLS, is_mac); + let default_style = ui.style().noninteractive(); + let text_color = self.text_color.unwrap_or(default_style.text_color()); + let fill_color = self.fill_color.unwrap_or(default_style.bg_fill); + let corner_radius = default_style.corner_radius; + + let number = RichText::new(shortcut_fmt) + .color(text_color) + .font(FontId::monospace(self.text_size)); + + Frame::canvas(ui.style()) + .fill(fill_color) + .stroke(Stroke::NONE) + .inner_margin(Margin::same(5)) + .corner_radius(corner_radius) + .show(ui, |ui| { + Label::new(number).selectable(false).ui(ui); + }) + .response + } +} + +impl ShortcutCard { + pub fn new(shortcut: KeyboardShortcut) -> Self { + Self { + shortcut, + text_size: 20., + text_color: None, + fill_color: None, + } + } + + pub fn text_size(mut self, text_size: f32) -> Self { + self.text_size = text_size; + self + } + + pub fn text_color(mut self, text_color: Color32) -> Self { + self.text_color = Some(text_color); + self + } + + pub fn fill_color(mut self, fill_color: Color32) -> Self { + self.fill_color = Some(fill_color); + self + } +} diff --git a/src/ui/panes/valve_control/ui/valve_control_window.rs b/src/ui/panes/valve_control/ui/valve_control_window.rs new file mode 100644 index 0000000..13a2642 --- /dev/null +++ b/src/ui/panes/valve_control/ui/valve_control_window.rs @@ -0,0 +1,297 @@ +use egui::{ + Color32, DragValue, Frame, Key, Label, Modal, Modifiers, Response, RichText, Sense, Stroke, Ui, + UiBuilder, Vec2, Widget, +}; +use tracing::info; + +use crate::ui::shortcuts::{ShortcutHandler, ShortcutMode}; + +use super::{ + commands::Command, icons::Icon, map_key_to_shortcut, shortcut_widget::ShortcutCard, + valves::Valve, +}; + +const WIGGLE_KEY: Key = Key::Minus; +const TIMING_KEY: Key = Key::Slash; +const APERTURE_KEY: Key = Key::Period; + +#[derive(Debug, Clone, PartialEq)] +pub struct ValveControlWindow { + valve: Valve, + state: ValveWindowState, + timing_ms: u32, + aperture_perc: f32, +} + +impl ValveControlWindow { + pub fn new(valve: Valve) -> ValveControlWindow { + ValveControlWindow { + valve, + state: ValveWindowState::Open, + timing_ms: 0, + aperture_perc: 0.0, + } + } + + pub fn is_closed(&self) -> bool { + matches!(self.state, ValveWindowState::Closed) + } + + #[profiling::function] + pub fn ui(&mut self, ui: &mut Ui, shortcut_handler: &mut ShortcutHandler) -> Option<Command> { + // Show only if the window is open + if self.is_closed() { + return None; + } + + // Capture the keyboard shortcuts + let mut action = self.keyboard_actions(shortcut_handler); + + // Draw the window UI + Modal::new(ui.auto_id_with("valve_control")) + .show(ui.ctx(), self.draw_window_ui(&mut action)); + + // Handle the actions + self.handle_actions(action) + } + + fn draw_window_ui(&mut self, action: &mut Option<WindowAction>) -> impl FnOnce(&mut Ui) { + |ui: &mut Ui| { + let icon_size = Vec2::splat(25.); + let text_size = 16.; + + fn btn_ui<R>( + window_state: &ValveWindowState, + key: Key, + add_contents: impl FnOnce(&mut Ui) -> R, + ) -> impl FnOnce(&mut Ui) -> Response { + move |ui| { + let wiggle_btn = Frame::canvas(ui.style()) + .inner_margin(ui.spacing().menu_margin) + .corner_radius(ui.visuals().noninteractive().corner_radius); + + ui.scope_builder(UiBuilder::new().id_salt(key).sense(Sense::click()), |ui| { + let response = ui.response(); + + let clicked = response.clicked(); + let shortcut_down = ui.ctx().input(|input| input.key_down(key)); + + let visuals = ui.style().interact(&response); + let (fill_color, stroke) = + if clicked || shortcut_down && window_state.is_open() { + let visuals = ui.visuals().widgets.active; + (visuals.bg_fill, visuals.bg_stroke) + } else if response.hovered() { + (visuals.bg_fill, visuals.bg_stroke) + } else { + let stroke = Stroke::new(1., Color32::TRANSPARENT); + (visuals.bg_fill.gamma_multiply(0.3), stroke) + }; + + wiggle_btn + .fill(fill_color) + .stroke(stroke) + .stroke(stroke) + .show(ui, |ui| { + ui.set_width(200.); + ui.horizontal(|ui| add_contents(ui)) + }); + + if response.clicked() { + info!("Clicked!"); + } + }) + .response + } + } + + let wiggle_btn_response = btn_ui(&self.state, WIGGLE_KEY, |ui| { + ShortcutCard::new(map_key_to_shortcut(WIGGLE_KEY)) + .text_color(ui.visuals().text_color()) + .fill_color(ui.visuals().widgets.inactive.bg_fill) + .text_size(20.) + .ui(ui); + ui.add( + Icon::Wiggle + .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 aperture_btn_response = btn_ui(&self.state, APERTURE_KEY, |ui| { + ShortcutCard::new(map_key_to_shortcut(APERTURE_KEY)) + .text_color(ui.visuals().text_color()) + .fill_color(ui.visuals().widgets.inactive.bg_fill) + .text_size(20.) + .ui(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)); + let drag_value_id = ui.next_auto_id(); + ui.add( + DragValue::new(&mut self.aperture_perc) + .speed(0.5) + .range(0.0..=100.0) + .fixed_decimals(0) + .update_while_editing(false) + .suffix("%"), + ); + if matches!(&self.state, ValveWindowState::ApertureFocused) { + ui.ctx().memory_mut(|m| { + m.request_focus(drag_value_id); + }); + } + })(ui); + + let timing_btn_response = btn_ui(&self.state, TIMING_KEY, |ui| { + ShortcutCard::new(map_key_to_shortcut(TIMING_KEY)) + .text_color(ui.visuals().text_color()) + .fill_color(ui.visuals().widgets.inactive.bg_fill) + .text_size(20.) + .ui(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)); + let drag_value_id = ui.next_auto_id(); + ui.add( + DragValue::new(&mut self.timing_ms) + .speed(1) + .range(1..=10000) + .fixed_decimals(0) + .update_while_editing(false) + .suffix(" [ms]"), + ); + if matches!(&self.state, ValveWindowState::TimingFocused) { + ui.ctx().memory_mut(|m| { + m.request_focus(drag_value_id); + }); + } + })(ui); + + // consider that action may be different that null if a keyboard shortcut was captured + if wiggle_btn_response.clicked() { + action.replace(WindowAction::Wiggle); + } else if aperture_btn_response.clicked() { + action.replace(WindowAction::SetAperture); + } else if timing_btn_response.clicked() { + action.replace(WindowAction::SetTiming); + } + } + } + + fn handle_actions(&mut self, action: Option<WindowAction>) -> Option<Command> { + match action { + // If the action close is called, close the window + Some(WindowAction::CloseWindow) => { + self.state = ValveWindowState::Closed; + None + } + Some(WindowAction::LooseFocus) => { + self.state = ValveWindowState::Open; + None + } + Some(WindowAction::Wiggle) => { + info!("Issued command to Wiggle valve: {:?}", self.valve); + Some(Command::wiggle(self.valve)) + } + Some(WindowAction::SetTiming) => { + info!( + "Issued command to set timing for valve {:?} to {} ms", + self.valve, self.timing_ms + ); + self.state = ValveWindowState::Open; + Some(Command::set_atomic_valve_timing(self.valve, self.timing_ms)) + } + Some(WindowAction::SetAperture) => { + info!( + "Issued command to set aperture for valve {:?} to {}%", + self.valve, self.aperture_perc + ); + self.state = ValveWindowState::Open; + Some(Command::set_valve_maximum_aperture( + self.valve, + self.aperture_perc / 100., + )) + } + Some(WindowAction::FocusOnTiming) => { + self.state = ValveWindowState::TimingFocused; + None + } + Some(WindowAction::FocusOnAperture) => { + self.state = ValveWindowState::ApertureFocused; + None + } + _ => None, + } + } +} + +impl ValveControlWindow { + #[profiling::function] + fn keyboard_actions(&self, shortcut_handler: &mut ShortcutHandler) -> Option<WindowAction> { + let mut key_action_pairs = Vec::new(); + + shortcut_handler.activate_mode(ShortcutMode::valve_control()); + match self.state { + ValveWindowState::Open => { + // A window is open, so we can map the keys to control the valve + key_action_pairs.push((Modifiers::NONE, WIGGLE_KEY, WindowAction::Wiggle)); + key_action_pairs.push((Modifiers::NONE, TIMING_KEY, WindowAction::FocusOnTiming)); + key_action_pairs.push(( + Modifiers::NONE, + APERTURE_KEY, + WindowAction::FocusOnAperture, + )); + key_action_pairs.push((Modifiers::NONE, Key::Escape, WindowAction::CloseWindow)); + } + 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, WindowAction::SetTiming)); + key_action_pairs.push((Modifiers::NONE, Key::Escape, WindowAction::LooseFocus)); + } + 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, WindowAction::SetAperture)); + key_action_pairs.push((Modifiers::NONE, Key::Escape, WindowAction::LooseFocus)); + } + ValveWindowState::Closed => {} + } + shortcut_handler.consume_if_mode_is(ShortcutMode::valve_control(), &key_action_pairs[..]) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ValveWindowState { + Closed, + Open, + TimingFocused, + ApertureFocused, +} + +impl ValveWindowState { + #[inline] + fn is_open(&self) -> bool { + matches!(self, Self::Open) + } +} + +#[derive(Debug, Clone, Copy)] +enum WindowAction { + // window actions + CloseWindow, + LooseFocus, + // commands + Wiggle, + SetTiming, + SetAperture, + // UI focus + FocusOnTiming, + FocusOnAperture, +} -- GitLab