From 56cc306d8b08e5ed8149ae153bfd676dba632f26 Mon Sep 17 00:00:00 2001 From: Federico Lolli <federico.lolli@skywarder.eu> Date: Thu, 20 Mar 2025 19:04:21 +0100 Subject: [PATCH 01/24] Fixed Pid name --- src/ui/panes.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/panes.rs b/src/ui/panes.rs index 977fbda..33f01ce 100644 --- a/src/ui/panes.rs +++ b/src/ui/panes.rs @@ -90,7 +90,7 @@ pub enum PaneKind { Plot2D(plot::Plot2DPane), #[strum(message = "Pid")] - PidOld(pid_drawing_tool::PidPane), + Pid(pid_drawing_tool::PidPane), } impl Default for PaneKind { -- GitLab From cb68e832beed79eb11ba3908eafaae5329646732 Mon Sep 17 00:00:00 2001 From: Federico Lolli <federico.lolli@skywarder.eu> Date: Thu, 20 Mar 2025 19:04:27 +0100 Subject: [PATCH 02/24] Fixed Pid pane draggability --- src/ui/panes/pid_drawing_tool.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/ui/panes/pid_drawing_tool.rs b/src/ui/panes/pid_drawing_tool.rs index f4e980e..57c49ca 100644 --- a/src/ui/panes/pid_drawing_tool.rs +++ b/src/ui/panes/pid_drawing_tool.rs @@ -53,6 +53,8 @@ pub struct PidPane { editable: bool, #[serde(skip)] is_subs_window_visible: bool, + #[serde(skip)] + contains_pointer: bool, } impl Default for PidPane { @@ -66,6 +68,7 @@ impl Default for PidPane { action: None, editable: false, is_subs_window_visible: false, + contains_pointer: false, } } } @@ -81,6 +84,8 @@ impl PartialEq for PidPane { impl PaneBehavior for PidPane { fn ui(&mut self, ui: &mut egui::Ui, _: TileId) -> PaneResponse { + let mut pane_response = PaneResponse::default(); + let theme = PidPane::find_theme(ui.ctx()); if self.center_content && !self.editable { @@ -132,11 +137,18 @@ impl PaneBehavior for PidPane { self.reset_subscriptions(); } - PaneResponse::default() + // Check if the user is draqging the pane + self.contains_pointer = response.contains_pointer(); + let ctrl_pressed = ui.input(|i| i.modifiers.ctrl); + if response.dragged() && (ctrl_pressed || !self.editable) { + pane_response.set_drag_started(); + } + + pane_response } fn contains_pointer(&self) -> bool { - false + self.contains_pointer } fn update(&mut self, messages: &[TimedMessage]) { -- GitLab From 74a26643abb015b1483f0fb74dee5541b4b7d40d Mon Sep 17 00:00:00 2001 From: Federico Lolli <federico.lolli@skywarder.eu> Date: Thu, 20 Mar 2025 20:01:58 +0100 Subject: [PATCH 03/24] CHECKPOINT --- src/ui/panes.rs | 6 ++- src/ui/panes/plot.rs | 2 +- src/ui/panes/valve_control.rs | 25 +++++++++++ src/ui/panes/valve_control/enums.rs | 68 +++++++++++++++++++++++++++++ 4 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 src/ui/panes/valve_control.rs create mode 100644 src/ui/panes/valve_control/enums.rs diff --git a/src/ui/panes.rs b/src/ui/panes.rs index 33f01ce..9b27514 100644 --- a/src/ui/panes.rs +++ b/src/ui/panes.rs @@ -1,7 +1,8 @@ mod default; mod messages_viewer; mod pid_drawing_tool; -pub mod plot; +mod plot; +mod valve_control; use egui_tiles::TileId; use enum_dispatch::enum_dispatch; @@ -91,6 +92,9 @@ pub enum PaneKind { #[strum(message = "Pid")] Pid(pid_drawing_tool::PidPane), + + #[strum(message = "Valve Control")] + ValveControl(valve_control::ValveControlPane), } impl Default for PaneKind { diff --git a/src/ui/panes/plot.rs b/src/ui/panes/plot.rs index 4a98372..684f221 100644 --- a/src/ui/panes/plot.rs +++ b/src/ui/panes/plot.rs @@ -33,7 +33,7 @@ pub struct Plot2DPane { #[serde(skip)] settings_visible: bool, #[serde(skip)] - pub contains_pointer: bool, + contains_pointer: bool, } impl PartialEq for Plot2DPane { diff --git a/src/ui/panes/valve_control.rs b/src/ui/panes/valve_control.rs new file mode 100644 index 0000000..a46ff41 --- /dev/null +++ b/src/ui/panes/valve_control.rs @@ -0,0 +1,25 @@ +use egui_tiles::TileId; +use serde::{Deserialize, Serialize}; + +use crate::ui::app::PaneResponse; + +use super::PaneBehavior; + +mod enums; + +#[derive(Clone, PartialEq, Default, Serialize, Deserialize, Debug)] +pub struct ValveControlPane { + // Temporary Internal state + #[serde(skip)] + contains_pointer: bool, +} + +impl PaneBehavior for ValveControlPane { + fn ui(&mut self, ui: &mut egui::Ui, tile_id: TileId) -> PaneResponse { + todo!() + } + + fn contains_pointer(&self) -> bool { + self.contains_pointer + } +} diff --git a/src/ui/panes/valve_control/enums.rs b/src/ui/panes/valve_control/enums.rs new file mode 100644 index 0000000..a1bf8fc --- /dev/null +++ b/src/ui/panes/valve_control/enums.rs @@ -0,0 +1,68 @@ +use std::fmt::Display; + +use strum_macros::EnumIter; + +use crate::mavlink::{ + MessageData, SET_ATOMIC_VALVE_TIMING_TC_DATA, SET_VALVE_MAXIMUM_APERTURE_TC_DATA, Servoslist, +}; + +#[allow(non_camel_case_types)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, EnumIter)] +pub enum Valve { + OxFilling, + OxRelease, + OxVenting, + N2Filling, + N2Release, + N2Quenching, + N23Way, + Main, + Nitrogen, +} + +impl From<Valve> for Servoslist { + fn from(valve: Valve) -> Servoslist { + match valve { + Valve::OxFilling => Servoslist::OX_FILLING_VALVE, + Valve::OxRelease => Servoslist::OX_RELEASE_VALVE, + Valve::OxVenting => Servoslist::OX_VENTING_VALVE, + Valve::N2Filling => Servoslist::N2_FILLING_VALVE, + Valve::N2Release => Servoslist::N2_RELEASE_VALVE, + Valve::N2Quenching => Servoslist::N2_QUENCHING_VALVE, + Valve::N23Way => Servoslist::N2_3WAY_VALVE, + Valve::Main => Servoslist::MAIN_VALVE, + Valve::Nitrogen => Servoslist::NITROGEN_VALVE, + } + } +} + +impl Display for Valve { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Valve::OxFilling => write!(f, "Oxidizer Filling"), + Valve::OxRelease => write!(f, "Oxidizer Release"), + Valve::OxVenting => write!(f, "Oxidizer Venting"), + Valve::N2Filling => write!(f, "Nitrogen Filling"), + Valve::N2Release => write!(f, "Nitrogen Release"), + Valve::N2Quenching => write!(f, "Nitrogen Quenching"), + Valve::N23Way => write!(f, "Nitrogen 3-Way"), + Valve::Main => write!(f, "Main"), + Valve::Nitrogen => write!(f, "Nitrogen"), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, EnumIter)] +pub enum ValveCommands { + AtomicValveTiming, + ValveMaximumAperture, +} + +impl From<ValveCommands> for u32 { + fn from(command: ValveCommands) -> u32 { + match command { + ValveCommands::AtomicValveTiming => SET_ATOMIC_VALVE_TIMING_TC_DATA::ID, + ValveCommands::ValveMaximumAperture => SET_VALVE_MAXIMUM_APERTURE_TC_DATA::ID, + } + } +} -- GitLab From aced424cad06c57e47d58a73a831187978855b50 Mon Sep 17 00:00:00 2001 From: Federico Lolli <federico.lolli@skywarder.eu> Date: Fri, 21 Mar 2025 09:54:39 +0100 Subject: [PATCH 04/24] Added support for multiple message id subscriptions --- src/mavlink.rs | 4 ++++ src/message_broker.rs | 19 +++++++-------- src/message_broker/message_bundle.rs | 36 +++++++--------------------- src/ui/app.rs | 8 +++---- src/ui/panes.rs | 12 +++++----- src/ui/panes/default.rs | 15 +++++++----- src/ui/panes/pid_drawing_tool.rs | 6 ++--- src/ui/panes/plot.rs | 6 ++--- 8 files changed, 45 insertions(+), 61 deletions(-) diff --git a/src/mavlink.rs b/src/mavlink.rs index f1517cc..9a05aa8 100644 --- a/src/mavlink.rs +++ b/src/mavlink.rs @@ -34,4 +34,8 @@ impl TimedMessage { time: Instant::now(), } } + + pub fn id(&self) -> u32 { + self.message.message_id() + } } diff --git a/src/message_broker.rs b/src/message_broker.rs index 7cf10e7..ddf1c17 100644 --- a/src/message_broker.rs +++ b/src/message_broker.rs @@ -11,7 +11,6 @@ pub use message_bundle::MessageBundle; use reception_queue::ReceptionQueue; use std::{ - collections::HashMap, sync::{Arc, Mutex}, time::Duration, }; @@ -21,7 +20,7 @@ use tracing::error; use crate::{ communication::{Connection, ConnectionError, TransceiverConfigExt}, error::ErrInstrument, - mavlink::{MavFrame, MavHeader, MavMessage, MavlinkVersion, Message, TimedMessage}, + mavlink::{MavFrame, MavHeader, MavMessage, MavlinkVersion, TimedMessage}, }; const RECEPTION_QUEUE_INTERVAL: Duration = Duration::from_secs(1); @@ -34,7 +33,7 @@ const SEGS_COMPONENT_ID: u8 = 1; /// dispatching them to the views that are interested in them. pub struct MessageBroker { /// A map of all messages received so far, indexed by message ID - messages: HashMap<u32, Vec<TimedMessage>>, + messages: Vec<TimedMessage>, /// instant queue used for frequency calculation and reception time last_receptions: Arc<Mutex<ReceptionQueue>>, /// Connection to the Mavlink listener @@ -47,7 +46,7 @@ impl MessageBroker { /// Creates a new `MessageBroker` with the given channel size and Egui context. pub fn new(ctx: egui::Context) -> Self { Self { - messages: HashMap::new(), + messages: Vec::new(), // TODO: make this configurable last_receptions: Arc::new(Mutex::new(ReceptionQueue::new(RECEPTION_QUEUE_INTERVAL))), connection: None, @@ -88,8 +87,11 @@ impl MessageBroker { self.last_receptions.lock().log_unwrap().frequency() } - pub fn get(&self, id: u32) -> &[TimedMessage] { - self.messages.get(&id).map_or(&[], |v| v.as_slice()) + pub fn get(&self, ids: &[u32]) -> Vec<&TimedMessage> { + self.messages + .iter() + .filter(|msg| ids.contains(&msg.id())) + .collect() } /// Processes incoming network messages. New messages are added to the @@ -108,10 +110,7 @@ impl MessageBroker { self.last_receptions.lock().log_unwrap().push(message.time); // Store the message in the broker - self.messages - .entry(message.message.message_id()) - .or_default() - .push(message); + self.messages.push(message); } self.ctx.request_repaint(); } diff --git a/src/message_broker/message_bundle.rs b/src/message_broker/message_bundle.rs index 52568b9..eaa06df 100644 --- a/src/message_broker/message_bundle.rs +++ b/src/message_broker/message_bundle.rs @@ -1,4 +1,4 @@ -use crate::mavlink::{Message, TimedMessage}; +use crate::mavlink::TimedMessage; /// A bundle of messages, indexed by their ID. /// Allows for efficient storage and retrieval of messages by ID. @@ -10,36 +10,22 @@ use crate::mavlink::{Message, TimedMessage}; /// method to clear the content of the bundle and prepare it for reuse. #[derive(Default)] pub struct MessageBundle { - storage: Vec<(u32, Vec<TimedMessage>)>, + storage: Vec<TimedMessage>, count: u32, } impl MessageBundle { /// Returns all messages of the given ID contained in the bundle. - pub fn get(&self, id: u32) -> &[TimedMessage] { + pub fn get(&self, ids: &[u32]) -> Vec<&TimedMessage> { self.storage .iter() - .find(|&&(queue_id, _)| queue_id == id) - .map_or(&[], |(_, messages)| messages.as_slice()) + .filter(|msg| ids.contains(&msg.id())) + .collect() } /// Inserts a new message into the bundle. pub fn insert(&mut self, message: TimedMessage) { - let message_id = message.message.message_id(); - - // Retrieve the queue for the ID, if it exists - let maybe_queue = self - .storage - .iter_mut() - .find(|&&mut (queue_id, _)| queue_id == message_id) - .map(|(_, queue)| queue); - - if let Some(queue) = maybe_queue { - queue.push(message); - } else { - self.storage.push((message_id, vec![message])); - } - + self.storage.push(message); self.count += 1; } @@ -49,15 +35,9 @@ impl MessageBundle { } /// Resets the content of the bundle, preparing it to be efficiently reused. - /// Effectively, it clears the content of the bundle, but with lower - /// allocation cost the next time the bundle is reused. + /// Effectively, it clears the content of the bundle. pub fn reset(&mut self) { - // Clear the individual queues instead of the full storage, to avoid - // the allocation cost of the already used per-id queues. - for (_, queue) in &mut self.storage { - queue.clear(); - } - + self.storage.clear(); self.count = 0; } } diff --git a/src/ui/app.rs b/src/ui/app.rs index ad8fa9f..902e586 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -318,14 +318,12 @@ impl App { // Skip non-pane tiles let Tile::Pane(pane) = tile else { continue }; // Skip panes that do not have a subscription - let Some(sub_id) = pane.get_message_subscription() else { - continue; - }; + let sub_ids: Vec<u32> = pane.get_message_subscriptions().collect(); if pane.should_send_message_history() { - pane.update(self.message_broker.get(sub_id)); + pane.update(self.message_broker.get(&sub_ids[..]).as_slice()); } else { - pane.update(self.message_bundle.get(sub_id)); + pane.update(self.message_bundle.get(&sub_ids[..]).as_slice()); } } diff --git a/src/ui/panes.rs b/src/ui/panes.rs index 9b27514..5d031f3 100644 --- a/src/ui/panes.rs +++ b/src/ui/panes.rs @@ -34,11 +34,11 @@ pub trait PaneBehavior { /// Updates the pane state. This method is called before `ui` to allow the /// pane to update its state based on the messages received. - fn update(&mut self, _messages: &[TimedMessage]) {} + fn update(&mut self, _messages: &[&TimedMessage]) {} /// Returns the ID of the messages this pane is interested in, if any. - fn get_message_subscription(&self) -> Option<u32> { - None + fn get_message_subscriptions(&self) -> Box<dyn Iterator<Item = u32>> { + Box::new(None.into_iter()) } /// Checks whether the full message history should be sent to the pane. @@ -61,12 +61,12 @@ impl PaneBehavior for Pane { self.pane.contains_pointer() } - fn update(&mut self, messages: &[TimedMessage]) { + fn update(&mut self, messages: &[&TimedMessage]) { self.pane.update(messages) } - fn get_message_subscription(&self) -> Option<u32> { - self.pane.get_message_subscription() + fn get_message_subscriptions(&self) -> Box<dyn Iterator<Item = u32>> { + self.pane.get_message_subscriptions() } fn should_send_message_history(&self) -> bool { diff --git a/src/ui/panes/default.rs b/src/ui/panes/default.rs index c3f7979..e4d1bef 100644 --- a/src/ui/panes/default.rs +++ b/src/ui/panes/default.rs @@ -2,9 +2,12 @@ use super::PaneBehavior; use serde::{Deserialize, Serialize}; use tracing::debug; -use crate::ui::{ - app::{PaneAction, PaneResponse}, - utils::{SizingMemo, vertically_centered}, +use crate::{ + mavlink::TimedMessage, + ui::{ + app::{PaneAction, PaneResponse}, + utils::{SizingMemo, vertically_centered}, + }, }; #[derive(Clone, Debug, Default, Serialize, Deserialize)] @@ -60,10 +63,10 @@ impl PaneBehavior for DefaultPane { self.contains_pointer } - fn update(&mut self, _messages: &[crate::mavlink::TimedMessage]) {} + fn update(&mut self, _messages: &[&TimedMessage]) {} - fn get_message_subscription(&self) -> Option<u32> { - None + fn get_message_subscriptions(&self) -> Box<dyn Iterator<Item = u32>> { + Box::new(None.into_iter()) } fn should_send_message_history(&self) -> bool { diff --git a/src/ui/panes/pid_drawing_tool.rs b/src/ui/panes/pid_drawing_tool.rs index 57c49ca..6f7695e 100644 --- a/src/ui/panes/pid_drawing_tool.rs +++ b/src/ui/panes/pid_drawing_tool.rs @@ -151,7 +151,7 @@ impl PaneBehavior for PidPane { self.contains_pointer } - fn update(&mut self, messages: &[TimedMessage]) { + fn update(&mut self, messages: &[&TimedMessage]) { if let Some(msg) = messages.last() { for element in &mut self.elements { element.update(&msg.message, self.message_subscription_id); @@ -159,8 +159,8 @@ impl PaneBehavior for PidPane { } } - fn get_message_subscription(&self) -> Option<u32> { - Some(self.message_subscription_id) + fn get_message_subscriptions(&self) -> Box<dyn Iterator<Item = u32>> { + Box::new(Some(self.message_subscription_id).into_iter()) } } diff --git a/src/ui/panes/plot.rs b/src/ui/panes/plot.rs index 684f221..42444ab 100644 --- a/src/ui/panes/plot.rs +++ b/src/ui/panes/plot.rs @@ -186,7 +186,7 @@ impl PaneBehavior for Plot2DPane { } #[profiling::function] - fn update(&mut self, messages: &[TimedMessage]) { + fn update(&mut self, messages: &[&TimedMessage]) { if !self.state_valid { self.line_data.clear(); } @@ -226,8 +226,8 @@ impl PaneBehavior for Plot2DPane { self.state_valid = true; } - fn get_message_subscription(&self) -> Option<u32> { - Some(self.settings.plot_message_id) + fn get_message_subscriptions(&self) -> Box<dyn Iterator<Item = u32>> { + Box::new(Some(self.settings.plot_message_id).into_iter()) } fn should_send_message_history(&self) -> bool { -- GitLab From f26c5392effa9994c9fd6c2afa66573efad65d77 Mon Sep 17 00:00:00 2001 From: Federico Lolli <federico.lolli@skywarder.eu> Date: Fri, 21 Mar 2025 10:11:49 +0100 Subject: [PATCH 05/24] Simplified ui method of PaneBehavior Removed TileId reference --- src/ui/app.rs | 31 +++++++++++++++---------------- src/ui/panes.rs | 8 ++++---- src/ui/panes/default.rs | 5 +++-- src/ui/panes/messages_viewer.rs | 2 +- src/ui/panes/pid_drawing_tool.rs | 3 +-- src/ui/panes/plot.rs | 7 +++---- src/ui/panes/valve_control.rs | 3 +-- src/ui/utils.rs | 5 ++--- src/ui/widget_gallery.rs | 4 ++-- 9 files changed, 32 insertions(+), 36 deletions(-) diff --git a/src/ui/app.rs b/src/ui/app.rs index 902e586..199d195 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -66,18 +66,18 @@ impl eframe::App for App { ((Modifiers::NONE, Key::V), PaneAction::SplitV), ((Modifiers::NONE, Key::H), PaneAction::SplitH), ((Modifiers::NONE, Key::C), PaneAction::Close), - ( - (Modifiers::NONE, Key::R), - PaneAction::ReplaceThroughGallery(Some(hovered_tile)), - ), + ((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[..])); + pane_action = + pane_action + .or(shortcuts::map_to_action(ctx, &key_action_pairs[..]) + .map(|a| (hovered_tile, a))); } // If an action was triggered, we consume it - if let Some(action) = pane_action.take() { + if let Some((tile_id, action)) = pane_action.take() { match action { PaneAction::SplitH => { if let Some(hovered_tile) = hovered_pane { @@ -132,15 +132,15 @@ impl eframe::App for App { } } } - PaneAction::Replace(tile_id, new_pane) => { + PaneAction::Replace(new_pane) => { debug!( "Called Replace on tile {:?} with pane {:?}", tile_id, new_pane ); panes_tree.tiles.insert(tile_id, Tile::Pane(*new_pane)); } - PaneAction::ReplaceThroughGallery(Some(source_tile)) => { - self.widget_gallery.replace_tile(source_tile); + PaneAction::ReplaceThroughGallery => { + self.widget_gallery.replace_tile(tile_id); } PaneAction::Maximize => { // This is a toggle: if there is not currently a maximized pane, @@ -170,7 +170,6 @@ impl eframe::App for App { self.maximized_pane = None; } } - _ => panic!("Unable to handle action"), } } @@ -225,7 +224,7 @@ 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, maximized_pane, pane); + maximized_pane_ui(ui, pane); } else { panic!("Maximized pane not found in tree!"); } @@ -400,7 +399,7 @@ impl AppState { /// Behavior for the tree of panes in the app #[derive(Default)] pub struct AppBehavior { - pub action: Option<PaneAction>, + pub action: Option<(TileId, PaneAction)>, } impl Behavior<Pane> for AppBehavior { @@ -413,10 +412,10 @@ impl Behavior<Pane> for AppBehavior { let PaneResponse { action_called, drag_response, - } = pane.ui(ui, tile_id); + } = pane.ui(ui); // Capture the action and store it to be consumed in the update function if let Some(action_called) = action_called { - self.action = Some(action_called); + self.action = Some((tile_id, action_called)); } drag_response } @@ -456,8 +455,8 @@ pub enum PaneAction { SplitH, SplitV, Close, - Replace(TileId, Box<Pane>), - ReplaceThroughGallery(Option<TileId>), + Replace(Box<Pane>), + ReplaceThroughGallery, Maximize, Exit, } diff --git a/src/ui/panes.rs b/src/ui/panes.rs index 5d031f3..1bec4df 100644 --- a/src/ui/panes.rs +++ b/src/ui/panes.rs @@ -4,7 +4,7 @@ mod pid_drawing_tool; mod plot; mod valve_control; -use egui_tiles::TileId; +use egui::Ui; use enum_dispatch::enum_dispatch; use serde::{Deserialize, Serialize}; use strum_macros::{self, EnumIter, EnumMessage}; @@ -27,7 +27,7 @@ impl Pane { #[enum_dispatch(PaneKind)] pub trait PaneBehavior { /// Renders the UI of the pane. - fn ui(&mut self, ui: &mut egui::Ui, tile_id: TileId) -> PaneResponse; + fn ui(&mut self, ui: &mut Ui) -> PaneResponse; /// Whether the pane contains the pointer. fn contains_pointer(&self) -> bool; @@ -53,8 +53,8 @@ pub trait PaneBehavior { } impl PaneBehavior for Pane { - fn ui(&mut self, ui: &mut egui::Ui, tile_id: TileId) -> PaneResponse { - self.pane.ui(ui, tile_id) + fn ui(&mut self, ui: &mut Ui) -> PaneResponse { + self.pane.ui(ui) } fn contains_pointer(&self) -> bool { diff --git a/src/ui/panes/default.rs b/src/ui/panes/default.rs index e4d1bef..63eb9d7 100644 --- a/src/ui/panes/default.rs +++ b/src/ui/panes/default.rs @@ -1,4 +1,5 @@ use super::PaneBehavior; +use egui::Ui; use serde::{Deserialize, Serialize}; use tracing::debug; @@ -26,7 +27,7 @@ impl PartialEq for DefaultPane { impl PaneBehavior for DefaultPane { #[profiling::function] - fn ui(&mut self, ui: &mut egui::Ui, tile_id: egui_tiles::TileId) -> PaneResponse { + fn ui(&mut self, ui: &mut Ui) -> PaneResponse { let mut response = PaneResponse::default(); let parent = vertically_centered(ui, &mut self.centering_memo, |ui| { @@ -40,7 +41,7 @@ impl PaneBehavior for DefaultPane { debug!("Horizontal Split button clicked"); } if ui.button("Widget Gallery").clicked() { - response.set_action(PaneAction::ReplaceThroughGallery(Some(tile_id))); + response.set_action(PaneAction::ReplaceThroughGallery); } }) .response diff --git a/src/ui/panes/messages_viewer.rs b/src/ui/panes/messages_viewer.rs index 2c8a54f..628f2d1 100644 --- a/src/ui/panes/messages_viewer.rs +++ b/src/ui/panes/messages_viewer.rs @@ -19,7 +19,7 @@ impl PartialEq for MessagesViewerPane { impl PaneBehavior for MessagesViewerPane { #[profiling::function] - fn ui(&mut self, ui: &mut egui::Ui, _tile_id: egui_tiles::TileId) -> PaneResponse { + fn ui(&mut self, ui: &mut egui::Ui) -> PaneResponse { let mut response = PaneResponse::default(); let label = ui.add_sized(ui.available_size(), Label::new("This is a label")); self.contains_pointer = label.contains_pointer(); diff --git a/src/ui/panes/pid_drawing_tool.rs b/src/ui/panes/pid_drawing_tool.rs index 6f7695e..1a7ec73 100644 --- a/src/ui/panes/pid_drawing_tool.rs +++ b/src/ui/panes/pid_drawing_tool.rs @@ -8,7 +8,6 @@ use core::f32; use egui::{ Button, Color32, Context, CursorIcon, PointerButton, Response, Sense, Theme, Ui, Widget, }; -use egui_tiles::TileId; use elements::Element; use glam::Vec2; use grid::GridInfo; @@ -83,7 +82,7 @@ impl PartialEq for PidPane { } impl PaneBehavior for PidPane { - fn ui(&mut self, ui: &mut egui::Ui, _: TileId) -> PaneResponse { + fn ui(&mut self, ui: &mut Ui) -> 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 42444ab..0d67e3d 100644 --- a/src/ui/panes/plot.rs +++ b/src/ui/panes/plot.rs @@ -11,9 +11,8 @@ use crate::{ ui::app::PaneResponse, utils::units::UnitOfMeasure, }; -use egui::{Color32, Vec2, Vec2b}; +use egui::{Color32, Ui, Vec2, Vec2b}; use egui_plot::{AxisHints, HPlacement, Legend, Line, PlotPoint, log_grid_spacer}; -use egui_tiles::TileId; use serde::{self, Deserialize, Serialize}; use source_window::sources_window; use std::{ @@ -44,7 +43,7 @@ impl PartialEq for Plot2DPane { impl PaneBehavior for Plot2DPane { #[profiling::function] - fn ui(&mut self, ui: &mut egui::Ui, _: TileId) -> PaneResponse { + fn ui(&mut self, ui: &mut Ui) -> PaneResponse { let mut response = PaneResponse::default(); let data_settings_digest = self.settings.data_digest(); @@ -235,7 +234,7 @@ impl PaneBehavior for Plot2DPane { } } -fn show_menu(ui: &mut egui::Ui, settings_visible: &mut bool, settings: &mut PlotSettings) { +fn show_menu(ui: &mut Ui, settings_visible: &mut bool, settings: &mut PlotSettings) { ui.set_max_width(200.0); // To make sure we wrap long text if ui.button("Source Data Settings…").clicked() { diff --git a/src/ui/panes/valve_control.rs b/src/ui/panes/valve_control.rs index a46ff41..f2e83cc 100644 --- a/src/ui/panes/valve_control.rs +++ b/src/ui/panes/valve_control.rs @@ -1,4 +1,3 @@ -use egui_tiles::TileId; use serde::{Deserialize, Serialize}; use crate::ui::app::PaneResponse; @@ -15,7 +14,7 @@ pub struct ValveControlPane { } impl PaneBehavior for ValveControlPane { - fn ui(&mut self, ui: &mut egui::Ui, tile_id: TileId) -> PaneResponse { + fn ui(&mut self, ui: &mut egui::Ui) -> PaneResponse { todo!() } diff --git a/src/ui/utils.rs b/src/ui/utils.rs index 0c2ae3f..2469c54 100644 --- a/src/ui/utils.rs +++ b/src/ui/utils.rs @@ -1,17 +1,16 @@ use egui::containers::Frame; use egui::{Response, Shadow, Stroke, Style, Ui}; -use egui_tiles::TileId; use super::panes::{Pane, PaneBehavior}; /// 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, tile_id: TileId, pane: &mut Pane) { +pub fn maximized_pane_ui(ui: &mut Ui, pane: &mut Pane) { Frame::popup(&Style::default()) .fill(egui::Color32::TRANSPARENT) .shadow(Shadow::NONE) .stroke(Stroke::NONE) - .show(ui, |ui| pane.ui(ui, tile_id)); + .show(ui, |ui| pane.ui(ui)); } #[derive(Debug, Default, Clone)] diff --git a/src/ui/widget_gallery.rs b/src/ui/widget_gallery.rs index ad8796d..80ef01e 100644 --- a/src/ui/widget_gallery.rs +++ b/src/ui/widget_gallery.rs @@ -19,7 +19,7 @@ impl WidgetGallery { self.open = true; } - pub fn show(&mut self, ctx: &Context) -> Option<PaneAction> { + pub fn show(&mut self, ctx: &Context) -> Option<(TileId, PaneAction)> { let mut window_visible = self.open; let resp = egui::Window::new("Widget Gallery") .collapsible(false) @@ -31,7 +31,7 @@ impl WidgetGallery { } else if let Some(message) = pane.get_message() { if ui.button(message).clicked() { if let Some(tile_id) = self.tile_id { - return Some(PaneAction::Replace(tile_id, Pane::boxed(pane))); + return Some((tile_id, PaneAction::Replace(Pane::boxed(pane)))); } } } -- GitLab From bc759b2f4074462010eee0649e29b4c60a4693ee Mon Sep 17 00:00:00 2001 From: Federico Lolli <federico.lolli@skywarder.eu> Date: Fri, 21 Mar 2025 10:19:32 +0100 Subject: [PATCH 06/24] Simplified PaneBehavior by removing "contains_pointer" method --- src/ui/app.rs | 16 ++++++++++------ src/ui/panes.rs | 7 ------- src/ui/panes/default.rs | 4 ---- src/ui/panes/messages_viewer.rs | 8 ++------ src/ui/panes/pid_drawing_tool.rs | 8 -------- src/ui/panes/plot.rs | 7 ------- src/ui/panes/valve_control.rs | 13 +++---------- 7 files changed, 15 insertions(+), 48 deletions(-) diff --git a/src/ui/app.rs b/src/ui/app.rs index 199d195..801b580 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -49,11 +49,7 @@ impl eframe::App for App { let panes_tree = &mut self.state.panes_tree; // Get the id of the hovered pane, in order to apply actions to it - let hovered_pane = panes_tree - .tiles - .iter() - .find(|(_, tile)| matches!(tile, Tile::Pane(pane) if pane.contains_pointer())) - .map(|(id, _)| *id); + let hovered_pane = self.behavior.tile_id_hovered; trace!("Hovered pane: {:?}", hovered_pane); // Capture any pane action generated by pane children @@ -400,6 +396,7 @@ impl AppState { #[derive(Default)] pub struct AppBehavior { pub action: Option<(TileId, PaneAction)>, + pub tile_id_hovered: Option<TileId>, } impl Behavior<Pane> for AppBehavior { @@ -409,10 +406,17 @@ impl Behavior<Pane> for AppBehavior { tile_id: TileId, pane: &mut Pane, ) -> egui_tiles::UiResponse { + let res = ui.scope(|ui| pane.ui(ui)); let PaneResponse { action_called, drag_response, - } = pane.ui(ui); + } = res.inner; + + // Check if the pointer is hovering over the pane + if res.response.contains_pointer() { + self.tile_id_hovered = Some(tile_id); + } + // Capture the action and store it to be consumed in the update function if let Some(action_called) = action_called { self.action = Some((tile_id, action_called)); diff --git a/src/ui/panes.rs b/src/ui/panes.rs index 1bec4df..9e0ea2f 100644 --- a/src/ui/panes.rs +++ b/src/ui/panes.rs @@ -29,9 +29,6 @@ pub trait PaneBehavior { /// Renders the UI of the pane. fn ui(&mut self, ui: &mut Ui) -> PaneResponse; - /// Whether the pane contains the pointer. - fn contains_pointer(&self) -> bool; - /// Updates the pane state. This method is called before `ui` to allow the /// pane to update its state based on the messages received. fn update(&mut self, _messages: &[&TimedMessage]) {} @@ -57,10 +54,6 @@ impl PaneBehavior for Pane { self.pane.ui(ui) } - fn contains_pointer(&self) -> bool { - self.pane.contains_pointer() - } - fn update(&mut self, messages: &[&TimedMessage]) { self.pane.update(messages) } diff --git a/src/ui/panes/default.rs b/src/ui/panes/default.rs index 63eb9d7..e0419ca 100644 --- a/src/ui/panes/default.rs +++ b/src/ui/panes/default.rs @@ -60,10 +60,6 @@ impl PaneBehavior for DefaultPane { response } - fn contains_pointer(&self) -> bool { - self.contains_pointer - } - fn update(&mut self, _messages: &[&TimedMessage]) {} fn get_message_subscriptions(&self) -> Box<dyn Iterator<Item = u32>> { diff --git a/src/ui/panes/messages_viewer.rs b/src/ui/panes/messages_viewer.rs index 628f2d1..3d85aef 100644 --- a/src/ui/panes/messages_viewer.rs +++ b/src/ui/panes/messages_viewer.rs @@ -1,4 +1,4 @@ -use egui::Label; +use egui::{Label, Ui}; use serde::{Deserialize, Serialize}; use crate::ui::app::PaneResponse; @@ -19,7 +19,7 @@ impl PartialEq for MessagesViewerPane { impl PaneBehavior for MessagesViewerPane { #[profiling::function] - fn ui(&mut self, ui: &mut egui::Ui) -> PaneResponse { + fn ui(&mut self, ui: &mut Ui) -> PaneResponse { let mut response = PaneResponse::default(); let label = ui.add_sized(ui.available_size(), Label::new("This is a label")); self.contains_pointer = label.contains_pointer(); @@ -28,8 +28,4 @@ impl PaneBehavior for MessagesViewerPane { } response } - - fn contains_pointer(&self) -> bool { - self.contains_pointer - } } diff --git a/src/ui/panes/pid_drawing_tool.rs b/src/ui/panes/pid_drawing_tool.rs index 1a7ec73..05160cc 100644 --- a/src/ui/panes/pid_drawing_tool.rs +++ b/src/ui/panes/pid_drawing_tool.rs @@ -52,8 +52,6 @@ pub struct PidPane { editable: bool, #[serde(skip)] is_subs_window_visible: bool, - #[serde(skip)] - contains_pointer: bool, } impl Default for PidPane { @@ -67,7 +65,6 @@ impl Default for PidPane { action: None, editable: false, is_subs_window_visible: false, - contains_pointer: false, } } } @@ -137,7 +134,6 @@ impl PaneBehavior for PidPane { } // Check if the user is draqging the pane - self.contains_pointer = response.contains_pointer(); let ctrl_pressed = ui.input(|i| i.modifiers.ctrl); if response.dragged() && (ctrl_pressed || !self.editable) { pane_response.set_drag_started(); @@ -146,10 +142,6 @@ impl PaneBehavior for PidPane { pane_response } - fn contains_pointer(&self) -> bool { - self.contains_pointer - } - fn update(&mut self, messages: &[&TimedMessage]) { if let Some(msg) = messages.last() { for element in &mut self.elements { diff --git a/src/ui/panes/plot.rs b/src/ui/panes/plot.rs index 0d67e3d..af83540 100644 --- a/src/ui/panes/plot.rs +++ b/src/ui/panes/plot.rs @@ -31,8 +31,6 @@ pub struct Plot2DPane { state_valid: bool, #[serde(skip)] settings_visible: bool, - #[serde(skip)] - contains_pointer: bool, } impl PartialEq for Plot2DPane { @@ -145,7 +143,6 @@ impl PaneBehavior for Plot2DPane { } plot.show(ui, |plot_ui| { - self.contains_pointer = plot_ui.response().contains_pointer(); if plot_ui.response().dragged() && ctrl_pressed { response.set_drag_started(); } @@ -180,10 +177,6 @@ impl PaneBehavior for Plot2DPane { response } - fn contains_pointer(&self) -> bool { - self.contains_pointer - } - #[profiling::function] fn update(&mut self, messages: &[&TimedMessage]) { if !self.state_valid { diff --git a/src/ui/panes/valve_control.rs b/src/ui/panes/valve_control.rs index f2e83cc..4fdf47d 100644 --- a/src/ui/panes/valve_control.rs +++ b/src/ui/panes/valve_control.rs @@ -1,3 +1,4 @@ +use egui::Ui; use serde::{Deserialize, Serialize}; use crate::ui::app::PaneResponse; @@ -7,18 +8,10 @@ use super::PaneBehavior; mod enums; #[derive(Clone, PartialEq, Default, Serialize, Deserialize, Debug)] -pub struct ValveControlPane { - // Temporary Internal state - #[serde(skip)] - contains_pointer: bool, -} +pub struct ValveControlPane {} impl PaneBehavior for ValveControlPane { - fn ui(&mut self, ui: &mut egui::Ui) -> PaneResponse { + fn ui(&mut self, ui: &mut Ui) -> PaneResponse { todo!() } - - fn contains_pointer(&self) -> bool { - self.contains_pointer - } } -- GitLab From 0092f152c5da4e06c2cb21034b85a06e35a7e2bd Mon Sep 17 00:00:00 2001 From: Federico Lolli <federico.lolli@skywarder.eu> Date: Fri, 21 Mar 2025 17:49:45 +0100 Subject: [PATCH 07/24] CHECKPOINT --- Cargo.toml | 2 +- icons/valve_control/dark/aperture.svg | 7 + icons/valve_control/dark/timing.svg | 6 + icons/valve_control/light/aperture.svg | 7 + icons/valve_control/light/timing.svg | 6 + src/ui/panes/messages_viewer.rs | 14 +- src/ui/panes/plot/source_window.rs | 1 + src/ui/panes/valve_control.rs | 316 ++++++++++++++++++++++++- src/ui/panes/valve_control/commands.rs | 163 +++++++++++++ src/ui/panes/valve_control/enums.rs | 68 ------ src/ui/panes/valve_control/icons.rs | 45 ++++ src/ui/panes/valve_control/valves.rs | 139 +++++++++++ 12 files changed, 688 insertions(+), 86 deletions(-) create mode 100644 icons/valve_control/dark/aperture.svg create mode 100644 icons/valve_control/dark/timing.svg create mode 100644 icons/valve_control/light/aperture.svg create mode 100644 icons/valve_control/light/timing.svg create mode 100644 src/ui/panes/valve_control/commands.rs delete mode 100644 src/ui/panes/valve_control/enums.rs create mode 100644 src/ui/panes/valve_control/icons.rs create mode 100644 src/ui/panes/valve_control/valves.rs diff --git a/Cargo.toml b/Cargo.toml index 8c2de1b..5698828 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ authors = [ "Niccolò Betto <niccolo.betto@skywarder.eu>", ] edition = "2024" -description = "Skyward Enhanced Ground Software" +edescription = "Skyward Enhanced Ground Software" license = "MIT" [dependencies] diff --git a/icons/valve_control/dark/aperture.svg b/icons/valve_control/dark/aperture.svg new file mode 100644 index 0000000..75a1a4f --- /dev/null +++ b/icons/valve_control/dark/aperture.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> +<svg fill="#ffffff" width="800px" height="800px" viewBox="0 0 14 14" role="img" focusable="false" aria-hidden="true" + xmlns="http://www.w3.org/2000/svg"> + <path + d="m 9.8075842,8.125795 c -0.047226,-0.0818 -0.1653177,-0.0818 -0.2125819,-2e-5 l -2.7091686,4.69002 c -0.047101,0.0815 0.011348,0.18406 0.1055111,0.18419 0.00288,0 0.00577,0 0.00866,0 1.7426489,0 3.3117022,-0.74298 4.4078492,-1.92938 0.03656,-0.0396 0.04318,-0.0983 0.01627,-0.14492 L 9.807592,8.125795 Z m -2.4888999,1.68471 -5.4097573,0.005 c -0.094226,8e-5 -0.1536307,0.10217 -0.1064545,0.18373 0.8246515,1.42592 2.2192034,2.4809 3.8722553,2.85359 0.052573,0.0119 0.1068068,-0.0117 0.1337538,-0.0583 l 1.6166069,-2.80007 c 0.047277,-0.0819 -0.011863,-0.18422 -0.1064042,-0.18411 z m 4.8812207,-5.80581 c -0.825356,-1.42976 -2.2234427,-2.48728 -3.8807595,-2.85911 -0.052561,-0.0118 -0.1067061,0.0117 -0.1336405,0.0584 l -1.6174875,2.80157 c -0.047252,0.0819 0.011813,0.18415 0.1063413,0.18412 l 5.4189662,-0.001 c 0.0942,-2e-5 0.153693,-0.10205 0.10658,-0.18365 z m 0.413678,1.12713 -3.2344588,0 c -0.094503,0 -0.1535677,0.10233 -0.1062909,0.18415 l 2.7089927,4.6888 c 0.04735,0.0819 0.165368,0.0815 0.212808,-3.1e-4 C 12.706741,9.120925 13,8.094715 13,7.000015 c 0,-0.62043 -0.09423,-1.2188 -0.269055,-1.7817 -0.01595,-0.0514 -0.06352,-0.0865 -0.117362,-0.0865 z M 7.0021135,1.000015 c -7.045e-4,0 -0.00142,0 -0.00213,0 -1.7423595,0 -3.311199,0.74275 -4.4073209,1.92881 -0.036558,0.0395 -0.043188,0.0983 -0.016254,0.14494 l 1.6162797,2.79947 c 0.047264,0.0819 0.1654436,0.0818 0.2126575,-9e-5 l 2.7023877,-4.6891 c 0.04695,-0.0815 -0.011498,-0.18401 -0.1056242,-0.18403 z m -5.6144011,7.87237 3.2356039,0 c 0.094503,0 0.1535552,-0.10231 0.106291,-0.18416 L 2.0185518,3.994515 c -0.047327,-0.082 -0.1653304,-0.0816 -0.2127959,3e-4 C 1.2933853,4.878505 1,5.904985 1,7.000015 c 0,0.62198 0.094717,1.22181 0.2703885,1.78596 0.01599,0.0514 0.063518,0.0864 0.1173239,0.0864 z" /> +</svg> diff --git a/icons/valve_control/dark/timing.svg b/icons/valve_control/dark/timing.svg new file mode 100644 index 0000000..e267bde --- /dev/null +++ b/icons/valve_control/dark/timing.svg @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> +<svg fill="#ffffff" width="800px" height="800px" viewBox="0 0 56 56" xmlns="http://www.w3.org/2000/svg"> + <path + d="M 27.9999 51.9063 C 41.0546 51.9063 51.9063 41.0781 51.9063 28 C 51.9063 14.9453 41.0780 4.0937 28.0234 4.0937 C 26.7812 4.0937 26.1718 4.8437 26.1718 6.0625 L 26.1718 15.1563 C 26.1718 16.1641 26.8514 16.9844 27.8827 16.9844 C 28.9140 16.9844 29.6171 16.1641 29.6171 15.1563 L 29.6171 8.1484 C 39.9296 8.9688 47.8983 17.5 47.8983 28 C 47.8983 39.0625 39.0390 47.9219 27.9999 47.9219 C 16.9374 47.9219 8.0546 39.0625 8.0780 28 C 8.1014 23.0781 9.8593 18.6016 12.7890 15.1563 C 13.5155 14.2422 13.5624 13.1406 12.7890 12.3203 C 12.0155 11.4766 10.7030 11.5469 9.8593 12.6016 C 6.2733 16.7734 4.0937 22.1641 4.0937 28 C 4.0937 41.0781 14.9218 51.9063 27.9999 51.9063 Z M 31.7499 31.6094 C 33.6014 29.6875 33.2265 27.0625 30.9999 25.5156 L 18.6014 16.8672 C 17.4296 16.0469 16.2109 17.2656 17.0312 18.4375 L 25.6796 30.8359 C 27.2265 33.0625 29.8514 33.4609 31.7499 31.6094 Z" /> +</svg> diff --git a/icons/valve_control/light/aperture.svg b/icons/valve_control/light/aperture.svg new file mode 100644 index 0000000..7975283 --- /dev/null +++ b/icons/valve_control/light/aperture.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> +<svg fill="#000000" width="800px" height="800px" viewBox="0 0 14 14" role="img" focusable="false" aria-hidden="true" + xmlns="http://www.w3.org/2000/svg"> + <path + d="m 9.8075842,8.125795 c -0.047226,-0.0818 -0.1653177,-0.0818 -0.2125819,-2e-5 l -2.7091686,4.69002 c -0.047101,0.0815 0.011348,0.18406 0.1055111,0.18419 0.00288,0 0.00577,0 0.00866,0 1.7426489,0 3.3117022,-0.74298 4.4078492,-1.92938 0.03656,-0.0396 0.04318,-0.0983 0.01627,-0.14492 L 9.807592,8.125795 Z m -2.4888999,1.68471 -5.4097573,0.005 c -0.094226,8e-5 -0.1536307,0.10217 -0.1064545,0.18373 0.8246515,1.42592 2.2192034,2.4809 3.8722553,2.85359 0.052573,0.0119 0.1068068,-0.0117 0.1337538,-0.0583 l 1.6166069,-2.80007 c 0.047277,-0.0819 -0.011863,-0.18422 -0.1064042,-0.18411 z m 4.8812207,-5.80581 c -0.825356,-1.42976 -2.2234427,-2.48728 -3.8807595,-2.85911 -0.052561,-0.0118 -0.1067061,0.0117 -0.1336405,0.0584 l -1.6174875,2.80157 c -0.047252,0.0819 0.011813,0.18415 0.1063413,0.18412 l 5.4189662,-0.001 c 0.0942,-2e-5 0.153693,-0.10205 0.10658,-0.18365 z m 0.413678,1.12713 -3.2344588,0 c -0.094503,0 -0.1535677,0.10233 -0.1062909,0.18415 l 2.7089927,4.6888 c 0.04735,0.0819 0.165368,0.0815 0.212808,-3.1e-4 C 12.706741,9.120925 13,8.094715 13,7.000015 c 0,-0.62043 -0.09423,-1.2188 -0.269055,-1.7817 -0.01595,-0.0514 -0.06352,-0.0865 -0.117362,-0.0865 z M 7.0021135,1.000015 c -7.045e-4,0 -0.00142,0 -0.00213,0 -1.7423595,0 -3.311199,0.74275 -4.4073209,1.92881 -0.036558,0.0395 -0.043188,0.0983 -0.016254,0.14494 l 1.6162797,2.79947 c 0.047264,0.0819 0.1654436,0.0818 0.2126575,-9e-5 l 2.7023877,-4.6891 c 0.04695,-0.0815 -0.011498,-0.18401 -0.1056242,-0.18403 z m -5.6144011,7.87237 3.2356039,0 c 0.094503,0 0.1535552,-0.10231 0.106291,-0.18416 L 2.0185518,3.994515 c -0.047327,-0.082 -0.1653304,-0.0816 -0.2127959,3e-4 C 1.2933853,4.878505 1,5.904985 1,7.000015 c 0,0.62198 0.094717,1.22181 0.2703885,1.78596 0.01599,0.0514 0.063518,0.0864 0.1173239,0.0864 z" /> +</svg> diff --git a/icons/valve_control/light/timing.svg b/icons/valve_control/light/timing.svg new file mode 100644 index 0000000..dd2d1e1 --- /dev/null +++ b/icons/valve_control/light/timing.svg @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> +<svg fill="#000000" width="800px" height="800px" viewBox="0 0 56 56" xmlns="http://www.w3.org/2000/svg"> + <path + d="M 27.9999 51.9063 C 41.0546 51.9063 51.9063 41.0781 51.9063 28 C 51.9063 14.9453 41.0780 4.0937 28.0234 4.0937 C 26.7812 4.0937 26.1718 4.8437 26.1718 6.0625 L 26.1718 15.1563 C 26.1718 16.1641 26.8514 16.9844 27.8827 16.9844 C 28.9140 16.9844 29.6171 16.1641 29.6171 15.1563 L 29.6171 8.1484 C 39.9296 8.9688 47.8983 17.5 47.8983 28 C 47.8983 39.0625 39.0390 47.9219 27.9999 47.9219 C 16.9374 47.9219 8.0546 39.0625 8.0780 28 C 8.1014 23.0781 9.8593 18.6016 12.7890 15.1563 C 13.5155 14.2422 13.5624 13.1406 12.7890 12.3203 C 12.0155 11.4766 10.7030 11.5469 9.8593 12.6016 C 6.2733 16.7734 4.0937 22.1641 4.0937 28 C 4.0937 41.0781 14.9218 51.9063 27.9999 51.9063 Z M 31.7499 31.6094 C 33.6014 29.6875 33.2265 27.0625 30.9999 25.5156 L 18.6014 16.8672 C 17.4296 16.0469 16.2109 17.2656 17.0312 18.4375 L 25.6796 30.8359 C 27.2265 33.0625 29.8514 33.4609 31.7499 31.6094 Z" /> +</svg> diff --git a/src/ui/panes/messages_viewer.rs b/src/ui/panes/messages_viewer.rs index 3d85aef..cfee6c6 100644 --- a/src/ui/panes/messages_viewer.rs +++ b/src/ui/panes/messages_viewer.rs @@ -5,24 +5,14 @@ use crate::ui::app::PaneResponse; use super::PaneBehavior; -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -pub struct MessagesViewerPane { - #[serde(skip)] - contains_pointer: bool, -} - -impl PartialEq for MessagesViewerPane { - fn eq(&self, _other: &Self) -> bool { - true - } -} +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct MessagesViewerPane; impl PaneBehavior for MessagesViewerPane { #[profiling::function] fn ui(&mut self, ui: &mut Ui) -> PaneResponse { let mut response = PaneResponse::default(); let label = ui.add_sized(ui.available_size(), Label::new("This is a label")); - self.contains_pointer = label.contains_pointer(); if label.drag_started() { response.set_drag_started(); } diff --git a/src/ui/panes/plot/source_window.rs b/src/ui/panes/plot/source_window.rs index a62d1f9..c309878 100644 --- a/src/ui/panes/plot/source_window.rs +++ b/src/ui/panes/plot/source_window.rs @@ -14,6 +14,7 @@ pub fn sources_window(ui: &mut egui::Ui, plot_settings: &mut PlotSettings) { egui::DragValue::new(&mut points_lifespan_sec) .range(5..=1800) .speed(1) + .update_while_editing(false) .suffix(" seconds"), ); res1.union(res2) diff --git a/src/ui/panes/valve_control.rs b/src/ui/panes/valve_control.rs index 4fdf47d..1e781c9 100644 --- a/src/ui/panes/valve_control.rs +++ b/src/ui/panes/valve_control.rs @@ -1,17 +1,323 @@ -use egui::Ui; +mod commands; +mod icons; +mod valves; + +use std::time::{Duration, Instant}; + +use egui::{ + Color32, DragValue, Frame, Label, Rect, RichText, Sense, Stroke, Ui, UiBuilder, Vec2, Widget, + vec2, +}; +use egui_extras::{Size, StripBuilder}; +use itertools::Itertools; use serde::{Deserialize, Serialize}; +use skyward_mavlink::{ + mavlink::MessageData, + orion::{ACK_TM_DATA, NACK_TM_DATA, WACK_TM_DATA}, +}; +use strum::IntoEnumIterator; +use tracing::info; -use crate::ui::app::PaneResponse; +use crate::{ + mavlink::{MavMessage, TimedMessage}, + ui::app::PaneResponse, +}; use super::PaneBehavior; -mod enums; +use commands::CommandSM; +use icons::Icon; +use valves::{Valve, ValveStateManager}; + +const DEFAULT_AUTO_REFRESH_RATE: Duration = Duration::from_secs(1); #[derive(Clone, PartialEq, Default, Serialize, Deserialize, Debug)] -pub struct ValveControlPane {} +pub struct ValveControlPane { + // INTERNAL + #[serde(skip)] + valves_state: ValveStateManager, + + // VALVE COMMANDS LIST + #[serde(skip)] + commands: Vec<CommandSM>, + + // REFRESH SETTINGS + auto_refresh: Option<Duration>, + + #[serde(skip)] + manual_refresh: bool, + + #[serde(skip)] + last_refresh: Option<Instant>, + + // UI SETTINGS + #[serde(skip)] + is_settings_window_open: bool, +} impl PaneBehavior for ValveControlPane { fn ui(&mut self, ui: &mut Ui) -> PaneResponse { - todo!() + let mut pane_response = PaneResponse::default(); + + let res = ui + .scope_builder(UiBuilder::new().sense(Sense::click_and_drag()), |ui| { + self.pane_ui()(ui); + ui.allocate_space(ui.available_size()); + }) + .response; + + // Show the menu when the user right-clicks the pane + res.context_menu(self.menu_ui()); + + // Check if the user started dragging the pane + if res.drag_started() { + pane_response.set_drag_started(); + } + + egui::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)); + + pane_response + } + + fn get_message_subscriptions(&self) -> Box<dyn Iterator<Item = u32>> { + let mut subscriptions = vec![]; + if self.needs_refresh() { + // TODO + // subscriptions.push(); + } + + // Subscribe to ACK, NACK, WACK messages if any command is waiting for a response + if self.commands.iter().any(CommandSM::is_waiting_for_response) { + subscriptions.push(ACK_TM_DATA::ID); + subscriptions.push(NACK_TM_DATA::ID); + subscriptions.push(WACK_TM_DATA::ID); + } + + Box::new(subscriptions.into_iter()) + } + + fn update(&mut self, messages: &[&TimedMessage]) { + if self.needs_refresh() { + // TODO + } + + // Capture any ACK/NACK/WACK messages and update the valve state + for message in messages { + for cmd in self.commands.iter_mut() { + // 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() { + self.valves_state.set_parameter_of(valve, parameter); + } + } + + // Remove consumed commands + self.commands.retain(|cmd| !cmd.is_consumed()); + } + + self.reset_last_refresh(); + } + + fn drain_outgoing_messages(&mut self) -> Vec<MavMessage> { + let mut outgoing = vec![]; + + // Pack and send the next command + for cmd in self.commands.iter_mut() { + if let Some(message) = cmd.pack_and_wait() { + outgoing.push(message); + } + } + + outgoing + } +} + +// ┌────────────────────────┐ +// │ UI METHODS │ +// └────────────────────────┘ +impl ValveControlPane { + fn pane_ui(&mut self) -> impl FnOnce(&mut Ui) { + |ui| { + let valve_chunks = Valve::iter().chunks(3); + StripBuilder::new(ui) + .sizes(Size::remainder(), 3) + .vertical(|mut strip| { + for chunk in &valve_chunks { + strip.strip(|builder| { + builder.sizes(Size::remainder(), 3).horizontal(|mut strip| { + for valve in chunk { + strip.cell(self.valve_frame_ui(valve)); + } + }); + }); + } + }); + } + } + + fn menu_ui(&mut self) -> impl FnOnce(&mut Ui) { + |ui| { + if ui.button("Refresh now").clicked() { + self.manual_refresh = true; + ui.close_menu(); + } + if ui.button("Settings").clicked() { + self.is_settings_window_open = true; + ui.close_menu(); + } + } + } + + fn 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(); + ui.horizontal(|ui| { + ui.checkbox(&mut auto_refresh, "Auto Refresh"); + if auto_refresh { + let auto_refresh_duration = + auto_refresh_setting.get_or_insert(DEFAULT_AUTO_REFRESH_RATE); + let mut auto_refresh_rate = 1. / auto_refresh_duration.as_secs_f32(); + DragValue::new(&mut auto_refresh_rate) + .speed(0.1) + .range(0.1..=10.0) + .fixed_decimals(1) + .update_while_editing(false) + .suffix(" Hz") + .ui(ui); + *auto_refresh_duration = Duration::from_secs_f32(1. / auto_refresh_rate); + } else { + *auto_refresh_setting = None; + } + }); + } + } + + fn valve_frame_ui(&self, valve: Valve) -> impl FnOnce(&mut Ui) { + move |ui| { + let valve_str = valve.to_string(); + let timing = self.valves_state.get_timing_for(valve); + let aperture = self.valves_state.get_aperture_for(valve); + + let timing_str = match timing { + valves::ParameterValue::Valid(value) => { + format!("{} [ms]", value) + } + valves::ParameterValue::Missing => "N/A".to_owned(), + valves::ParameterValue::Invalid(err_id) => { + format!("ERROR: {}", err_id) + } + }; + let aperture_str = match aperture { + valves::ParameterValue::Valid(value) => { + format!("{}", value) + } + valves::ParameterValue::Missing => "N/A".to_owned(), + valves::ParameterValue::Invalid(err_id) => { + format!("ERROR: {}", err_id) + } + }; + + let inside_frame = |ui: &mut Ui| { + let response = ui.response(); + let visuals = ui.style().interact(&response); + let text_color = visuals.text_color(); + let icon_size = Vec2::splat(17.); + + StripBuilder::new(ui) + .size(Size::exact(20.)) + .size(Size::exact(20.)) + .vertical(|mut strip| { + strip.cell(|ui| { + Label::new( + RichText::new(valve_str.to_ascii_uppercase()) + .color(text_color) + .size(16.0), + ) + .selectable(false) + .ui(ui); + }); + strip.cell(|ui| { + ui.vertical(|ui| { + ui.horizontal(|ui| { + let rect = Rect::from_min_size(ui.cursor().min, icon_size); + Icon::Timing.paint(ui, rect); + ui.allocate_rect(rect, Sense::hover()); + Label::new(format!("Timing: {}", timing_str)) + .selectable(false) + .ui(ui); + }); + ui.horizontal(|ui| { + let rect = Rect::from_min_size(ui.cursor().min, icon_size); + Icon::Aperture.paint(ui, rect); + ui.allocate_rect(rect, Sense::hover()); + Label::new(format!("Aperture: {}", aperture_str)) + .selectable(false) + .ui(ui); + }); + }); + }); + }); + }; + + ui.scope_builder( + UiBuilder::new() + .id_salt("valve_".to_owned() + &valve_str) + .sense(Sense::click()), + |ui| { + ui.set_width(200.); + ui.set_height(60.); + let response = ui.response(); + let visuals = ui.style().interact(&response); + + let fill_color = if response.hovered() { + visuals.bg_fill + } else { + visuals.bg_fill.gamma_multiply(0.3) + }; + + Frame::canvas(ui.style()) + .fill(fill_color) + .stroke(Stroke::NONE) + .inner_margin(ui.spacing().menu_margin) + .show(ui, inside_frame); + + if response.clicked() { + info!("Clicked!"); + } + }, + ); + } + } +} + +// ┌───────────────────────────┐ +// │ UTILS METHODS │ +// └───────────────────────────┘ +impl ValveControlPane { + fn needs_refresh(&self) -> bool { + // manual trigger of refresh + let manual_triggered = self.manual_refresh; + // automatic trigger of refresh + let auto_triggered = if let Some(auto_refresh) = self.auto_refresh { + self.last_refresh + .is_none_or(|last| last.elapsed() >= auto_refresh) + } else { + false + }; + + manual_triggered || auto_triggered + } + + fn reset_last_refresh(&mut self) { + self.last_refresh = Some(Instant::now()); + self.manual_refresh = false; } } diff --git a/src/ui/panes/valve_control/commands.rs b/src/ui/panes/valve_control/commands.rs new file mode 100644 index 0000000..8593fd9 --- /dev/null +++ b/src/ui/panes/valve_control/commands.rs @@ -0,0 +1,163 @@ +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, +}; + +use super::valves::{ParameterValue, Valve, ValveParameter}; + +#[derive(Debug, Clone, PartialEq)] +pub enum CommandSM { + Request(Command), + WaitingForResponse(Command), + Response((Valve, ValveParameter)), + Consumed, +} + +impl CommandSM { + pub fn pack_and_wait(&mut self) -> Option<MavMessage> { + match self { + CommandSM::Request(command) => { + let message = MavMessage::from(command.clone()); + *self = CommandSM::WaitingForResponse(command.clone()); + Some(message) + } + _ => None, + } + } + + pub fn capture_response(&mut self, message: &MavMessage) { + if let CommandSM::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 => { + *self = CommandSM::Response((*valve, kind.to_valid_parameter())); + } + MavMessage::NACK_TM(NACK_TM_DATA { + err_id, recv_msgid, .. + }) if *recv_msgid == id => { + *self = CommandSM::Response((*valve, kind.to_invalid_parameter(*err_id))); + } + MavMessage::WACK_TM(WACK_TM_DATA { + err_id, recv_msgid, .. + }) if *recv_msgid == id => { + *self = CommandSM::Response((*valve, kind.to_invalid_parameter(*err_id))); + } + _ => {} + } + } + } + + pub fn consume_response(&mut self) -> Option<(Valve, ValveParameter)> { + match self { + CommandSM::Response((valve, parameter)) => { + let res = Some((*valve, parameter.clone())); + *self = CommandSM::Consumed; + res + } + _ => None, + } + } + + pub fn is_waiting_for_response(&self) -> bool { + matches!(self, CommandSM::WaitingForResponse(_)) + } + + pub fn is_consumed(&self) -> bool { + matches!(self, CommandSM::Consumed) + } +} + +impl From<Command> for CommandSM { + fn from(value: Command) -> Self { + CommandSM::Request(value) + } +} + +trait ControllableValves { + fn set_atomic_valve_timing(self, timing: u32) -> Command; + fn set_valve_maximum_aperture(self, aperture: f32) -> Command; +} + +impl ControllableValves for Valve { + fn set_atomic_valve_timing(self, timing: u32) -> Command { + Command { + kind: CommandKind::SetAtomicValveTiming(timing), + valve: self, + } + } + + fn set_valve_maximum_aperture(self, aperture: f32) -> Command { + Command { + kind: CommandKind::SetValveMaximumAperture(aperture), + valve: self, + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Command { + kind: CommandKind, + valve: Valve, +} + +impl From<Command> for MavMessage { + fn from(value: Command) -> Self { + match value.kind { + CommandKind::SetAtomicValveTiming(timing) => { + MavMessage::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 { + servo_id: value.valve.into(), + maximum_aperture: aperture, + }) + } + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +enum CommandKind { + SetAtomicValveTiming(u32), + SetValveMaximumAperture(f32), +} + +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, + } + } + + fn to_valid_parameter(&self) -> ValveParameter { + (*self).into() + } + + fn to_invalid_parameter(&self, error: u16) -> ValveParameter { + match self { + CommandKind::SetAtomicValveTiming(_) => { + ValveParameter::AtomicValveTiming(ParameterValue::Invalid(error)) + } + CommandKind::SetValveMaximumAperture(_) => { + ValveParameter::ValveMaximumAperture(ParameterValue::Invalid(error)) + } + } + } +} + +impl From<CommandKind> for ValveParameter { + fn from(value: CommandKind) -> Self { + match value { + CommandKind::SetAtomicValveTiming(timing) => { + ValveParameter::AtomicValveTiming(ParameterValue::Valid(timing)) + } + CommandKind::SetValveMaximumAperture(aperture) => { + ValveParameter::ValveMaximumAperture(ParameterValue::Valid(aperture)) + } + } + } +} diff --git a/src/ui/panes/valve_control/enums.rs b/src/ui/panes/valve_control/enums.rs deleted file mode 100644 index a1bf8fc..0000000 --- a/src/ui/panes/valve_control/enums.rs +++ /dev/null @@ -1,68 +0,0 @@ -use std::fmt::Display; - -use strum_macros::EnumIter; - -use crate::mavlink::{ - MessageData, SET_ATOMIC_VALVE_TIMING_TC_DATA, SET_VALVE_MAXIMUM_APERTURE_TC_DATA, Servoslist, -}; - -#[allow(non_camel_case_types)] -#[derive(Clone, Copy, Debug, PartialEq, Eq, EnumIter)] -pub enum Valve { - OxFilling, - OxRelease, - OxVenting, - N2Filling, - N2Release, - N2Quenching, - N23Way, - Main, - Nitrogen, -} - -impl From<Valve> for Servoslist { - fn from(valve: Valve) -> Servoslist { - match valve { - Valve::OxFilling => Servoslist::OX_FILLING_VALVE, - Valve::OxRelease => Servoslist::OX_RELEASE_VALVE, - Valve::OxVenting => Servoslist::OX_VENTING_VALVE, - Valve::N2Filling => Servoslist::N2_FILLING_VALVE, - Valve::N2Release => Servoslist::N2_RELEASE_VALVE, - Valve::N2Quenching => Servoslist::N2_QUENCHING_VALVE, - Valve::N23Way => Servoslist::N2_3WAY_VALVE, - Valve::Main => Servoslist::MAIN_VALVE, - Valve::Nitrogen => Servoslist::NITROGEN_VALVE, - } - } -} - -impl Display for Valve { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Valve::OxFilling => write!(f, "Oxidizer Filling"), - Valve::OxRelease => write!(f, "Oxidizer Release"), - Valve::OxVenting => write!(f, "Oxidizer Venting"), - Valve::N2Filling => write!(f, "Nitrogen Filling"), - Valve::N2Release => write!(f, "Nitrogen Release"), - Valve::N2Quenching => write!(f, "Nitrogen Quenching"), - Valve::N23Way => write!(f, "Nitrogen 3-Way"), - Valve::Main => write!(f, "Main"), - Valve::Nitrogen => write!(f, "Nitrogen"), - } - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, EnumIter)] -pub enum ValveCommands { - AtomicValveTiming, - ValveMaximumAperture, -} - -impl From<ValveCommands> for u32 { - fn from(command: ValveCommands) -> u32 { - match command { - ValveCommands::AtomicValveTiming => SET_ATOMIC_VALVE_TIMING_TC_DATA::ID, - ValveCommands::ValveMaximumAperture => SET_VALVE_MAXIMUM_APERTURE_TC_DATA::ID, - } - } -} diff --git a/src/ui/panes/valve_control/icons.rs b/src/ui/panes/valve_control/icons.rs new file mode 100644 index 0000000..064fee5 --- /dev/null +++ b/src/ui/panes/valve_control/icons.rs @@ -0,0 +1,45 @@ +use egui::{ImageSource, Rect, Theme, Ui}; + +#[derive(Debug, Clone, Copy)] +pub enum Icon { + Aperture, + Timing, +} + +impl Icon { + fn get_image(&self, theme: Theme) -> ImageSource { + match (&self, theme) { + (Icon::Aperture, Theme::Light) => { + egui::include_image!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/icons/valve_control/light/aperture.svg" + )) + } + (Icon::Aperture, Theme::Dark) => { + egui::include_image!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/icons/valve_control/dark/aperture.svg" + )) + } + (Icon::Timing, Theme::Light) => { + egui::include_image!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/icons/valve_control/light/timing.svg" + )) + } + (Icon::Timing, Theme::Dark) => { + egui::include_image!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/icons/valve_control/dark/timing.svg" + )) + } + } + } +} + +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); + } +} diff --git a/src/ui/panes/valve_control/valves.rs b/src/ui/panes/valve_control/valves.rs new file mode 100644 index 0000000..9580dc1 --- /dev/null +++ b/src/ui/panes/valve_control/valves.rs @@ -0,0 +1,139 @@ +//! Valve Control Pane +//! +//! NOTE: We assume that no more than one entity will sent messages to control valves at a time. + +use std::fmt::Display; + +use strum::IntoEnumIterator; +use strum_macros::EnumIter; + +use crate::{error::ErrInstrument, mavlink::Servoslist}; + +#[derive(Clone, Debug, PartialEq)] +pub struct ValveStateManager { + settings: Vec<(Valve, ValveParameter)>, +} + +impl Default for ValveStateManager { + fn default() -> Self { + Self::new() + } +} + +impl ValveStateManager { + pub fn new() -> Self { + let settings = Valve::iter() + .flat_map(|valve| ValveParameter::iter().map(move |parameter| (valve, parameter))) + .collect(); + Self { settings } + } + + pub fn set_parameter_of(&mut self, valve: Valve, parameter: ValveParameter) { + let (_, par) = self + .settings + .iter_mut() + .find(|(v, _)| *v == valve) + .log_unwrap(); + *par = parameter; + } + + pub fn get_timing_for(&self, valve: Valve) -> ParameterValue<u32, u16> { + for (_, par) in self.settings.iter().filter(|(v, _)| *v == valve) { + match par { + ValveParameter::AtomicValveTiming(parameter_value) => { + return parameter_value.clone(); + } + _ => continue, + }; + } + unreachable!() + } + + pub fn get_aperture_for(&self, valve: Valve) -> ParameterValue<f32, u16> { + for (_, par) in self.settings.iter().filter(|(v, _)| *v == valve) { + match par { + ValveParameter::ValveMaximumAperture(parameter_value) => { + return parameter_value.clone(); + } + _ => continue, + }; + } + unreachable!() + } +} + +#[allow(non_camel_case_types)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, EnumIter)] +pub enum Valve { + OxFilling, + OxRelease, + OxVenting, + N2Filling, + N2Release, + N2Quenching, + N23Way, + Main, + Nitrogen, +} + +impl From<Valve> for Servoslist { + fn from(valve: Valve) -> Servoslist { + match valve { + Valve::OxFilling => Servoslist::OX_FILLING_VALVE, + Valve::OxRelease => Servoslist::OX_RELEASE_VALVE, + Valve::OxVenting => Servoslist::OX_VENTING_VALVE, + Valve::N2Filling => Servoslist::N2_FILLING_VALVE, + Valve::N2Release => Servoslist::N2_RELEASE_VALVE, + Valve::N2Quenching => Servoslist::N2_QUENCHING_VALVE, + Valve::N23Way => Servoslist::N2_3WAY_VALVE, + Valve::Main => Servoslist::MAIN_VALVE, + Valve::Nitrogen => Servoslist::NITROGEN_VALVE, + } + } +} + +impl From<Valve> for u8 { + fn from(valve: Valve) -> u8 { + Servoslist::from(valve) as u8 + } +} + +impl Display for Valve { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Valve::OxFilling => write!(f, "Oxidizer Filling"), + Valve::OxRelease => write!(f, "Oxidizer Release"), + Valve::OxVenting => write!(f, "Oxidizer Venting"), + Valve::N2Filling => write!(f, "Nitrogen Filling"), + Valve::N2Release => write!(f, "Nitrogen Release"), + Valve::N2Quenching => write!(f, "Nitrogen Quenching"), + Valve::N23Way => write!(f, "Nitrogen 3-Way"), + Valve::Main => write!(f, "Main"), + Valve::Nitrogen => write!(f, "Nitrogen"), + } + } +} + +#[derive(Clone, Debug, PartialEq, EnumIter)] +pub enum ValveParameter { + AtomicValveTiming(ParameterValue<u32, u16>), + ValveMaximumAperture(ParameterValue<f32, u16>), +} + +#[derive(Clone, Debug, PartialEq, Default)] +pub enum ParameterValue<T, E> { + Valid(T), // T is the valid parameter value + #[default] + Missing, // The parameter is missing + Invalid(E), // E is the reason why the parameter is invalid +} + +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), + } + } +} -- GitLab From 4be15a73d6862ab6839e44cc81fee0f536edfa15 Mon Sep 17 00:00:00 2001 From: Federico Lolli <federico.lolli@skywarder.eu> Date: Fri, 21 Mar 2025 18:37:37 +0100 Subject: [PATCH 08/24] CHECKPOINT --- icons/valve_control/dark/aperture.svg | 2 +- icons/valve_control/dark/timing.svg | 2 +- icons/valve_control/light/aperture.svg | 2 +- icons/valve_control/light/timing.svg | 2 +- src/ui/panes/valve_control.rs | 34 ++++++++++++-------------- 5 files changed, 19 insertions(+), 23 deletions(-) diff --git a/icons/valve_control/dark/aperture.svg b/icons/valve_control/dark/aperture.svg index 75a1a4f..55195e2 100644 --- a/icons/valve_control/dark/aperture.svg +++ b/icons/valve_control/dark/aperture.svg @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> -<svg fill="#ffffff" width="800px" height="800px" viewBox="0 0 14 14" role="img" focusable="false" aria-hidden="true" +<svg fill="#8c8c8c" width="800px" height="800px" viewBox="0 0 14 14" role="img" focusable="false" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"> <path d="m 9.8075842,8.125795 c -0.047226,-0.0818 -0.1653177,-0.0818 -0.2125819,-2e-5 l -2.7091686,4.69002 c -0.047101,0.0815 0.011348,0.18406 0.1055111,0.18419 0.00288,0 0.00577,0 0.00866,0 1.7426489,0 3.3117022,-0.74298 4.4078492,-1.92938 0.03656,-0.0396 0.04318,-0.0983 0.01627,-0.14492 L 9.807592,8.125795 Z m -2.4888999,1.68471 -5.4097573,0.005 c -0.094226,8e-5 -0.1536307,0.10217 -0.1064545,0.18373 0.8246515,1.42592 2.2192034,2.4809 3.8722553,2.85359 0.052573,0.0119 0.1068068,-0.0117 0.1337538,-0.0583 l 1.6166069,-2.80007 c 0.047277,-0.0819 -0.011863,-0.18422 -0.1064042,-0.18411 z m 4.8812207,-5.80581 c -0.825356,-1.42976 -2.2234427,-2.48728 -3.8807595,-2.85911 -0.052561,-0.0118 -0.1067061,0.0117 -0.1336405,0.0584 l -1.6174875,2.80157 c -0.047252,0.0819 0.011813,0.18415 0.1063413,0.18412 l 5.4189662,-0.001 c 0.0942,-2e-5 0.153693,-0.10205 0.10658,-0.18365 z m 0.413678,1.12713 -3.2344588,0 c -0.094503,0 -0.1535677,0.10233 -0.1062909,0.18415 l 2.7089927,4.6888 c 0.04735,0.0819 0.165368,0.0815 0.212808,-3.1e-4 C 12.706741,9.120925 13,8.094715 13,7.000015 c 0,-0.62043 -0.09423,-1.2188 -0.269055,-1.7817 -0.01595,-0.0514 -0.06352,-0.0865 -0.117362,-0.0865 z M 7.0021135,1.000015 c -7.045e-4,0 -0.00142,0 -0.00213,0 -1.7423595,0 -3.311199,0.74275 -4.4073209,1.92881 -0.036558,0.0395 -0.043188,0.0983 -0.016254,0.14494 l 1.6162797,2.79947 c 0.047264,0.0819 0.1654436,0.0818 0.2126575,-9e-5 l 2.7023877,-4.6891 c 0.04695,-0.0815 -0.011498,-0.18401 -0.1056242,-0.18403 z m -5.6144011,7.87237 3.2356039,0 c 0.094503,0 0.1535552,-0.10231 0.106291,-0.18416 L 2.0185518,3.994515 c -0.047327,-0.082 -0.1653304,-0.0816 -0.2127959,3e-4 C 1.2933853,4.878505 1,5.904985 1,7.000015 c 0,0.62198 0.094717,1.22181 0.2703885,1.78596 0.01599,0.0514 0.063518,0.0864 0.1173239,0.0864 z" /> diff --git a/icons/valve_control/dark/timing.svg b/icons/valve_control/dark/timing.svg index e267bde..a9d7e36 100644 --- a/icons/valve_control/dark/timing.svg +++ b/icons/valve_control/dark/timing.svg @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> -<svg fill="#ffffff" width="800px" height="800px" viewBox="0 0 56 56" xmlns="http://www.w3.org/2000/svg"> +<svg fill="#8c8c8c" width="800px" height="800px" viewBox="0 0 56 56" xmlns="http://www.w3.org/2000/svg"> <path d="M 27.9999 51.9063 C 41.0546 51.9063 51.9063 41.0781 51.9063 28 C 51.9063 14.9453 41.0780 4.0937 28.0234 4.0937 C 26.7812 4.0937 26.1718 4.8437 26.1718 6.0625 L 26.1718 15.1563 C 26.1718 16.1641 26.8514 16.9844 27.8827 16.9844 C 28.9140 16.9844 29.6171 16.1641 29.6171 15.1563 L 29.6171 8.1484 C 39.9296 8.9688 47.8983 17.5 47.8983 28 C 47.8983 39.0625 39.0390 47.9219 27.9999 47.9219 C 16.9374 47.9219 8.0546 39.0625 8.0780 28 C 8.1014 23.0781 9.8593 18.6016 12.7890 15.1563 C 13.5155 14.2422 13.5624 13.1406 12.7890 12.3203 C 12.0155 11.4766 10.7030 11.5469 9.8593 12.6016 C 6.2733 16.7734 4.0937 22.1641 4.0937 28 C 4.0937 41.0781 14.9218 51.9063 27.9999 51.9063 Z M 31.7499 31.6094 C 33.6014 29.6875 33.2265 27.0625 30.9999 25.5156 L 18.6014 16.8672 C 17.4296 16.0469 16.2109 17.2656 17.0312 18.4375 L 25.6796 30.8359 C 27.2265 33.0625 29.8514 33.4609 31.7499 31.6094 Z" /> </svg> diff --git a/icons/valve_control/light/aperture.svg b/icons/valve_control/light/aperture.svg index 7975283..df46696 100644 --- a/icons/valve_control/light/aperture.svg +++ b/icons/valve_control/light/aperture.svg @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> -<svg fill="#000000" width="800px" height="800px" viewBox="0 0 14 14" role="img" focusable="false" aria-hidden="true" +<svg fill="#505050" width="800px" height="800px" viewBox="0 0 14 14" role="img" focusable="false" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"> <path d="m 9.8075842,8.125795 c -0.047226,-0.0818 -0.1653177,-0.0818 -0.2125819,-2e-5 l -2.7091686,4.69002 c -0.047101,0.0815 0.011348,0.18406 0.1055111,0.18419 0.00288,0 0.00577,0 0.00866,0 1.7426489,0 3.3117022,-0.74298 4.4078492,-1.92938 0.03656,-0.0396 0.04318,-0.0983 0.01627,-0.14492 L 9.807592,8.125795 Z m -2.4888999,1.68471 -5.4097573,0.005 c -0.094226,8e-5 -0.1536307,0.10217 -0.1064545,0.18373 0.8246515,1.42592 2.2192034,2.4809 3.8722553,2.85359 0.052573,0.0119 0.1068068,-0.0117 0.1337538,-0.0583 l 1.6166069,-2.80007 c 0.047277,-0.0819 -0.011863,-0.18422 -0.1064042,-0.18411 z m 4.8812207,-5.80581 c -0.825356,-1.42976 -2.2234427,-2.48728 -3.8807595,-2.85911 -0.052561,-0.0118 -0.1067061,0.0117 -0.1336405,0.0584 l -1.6174875,2.80157 c -0.047252,0.0819 0.011813,0.18415 0.1063413,0.18412 l 5.4189662,-0.001 c 0.0942,-2e-5 0.153693,-0.10205 0.10658,-0.18365 z m 0.413678,1.12713 -3.2344588,0 c -0.094503,0 -0.1535677,0.10233 -0.1062909,0.18415 l 2.7089927,4.6888 c 0.04735,0.0819 0.165368,0.0815 0.212808,-3.1e-4 C 12.706741,9.120925 13,8.094715 13,7.000015 c 0,-0.62043 -0.09423,-1.2188 -0.269055,-1.7817 -0.01595,-0.0514 -0.06352,-0.0865 -0.117362,-0.0865 z M 7.0021135,1.000015 c -7.045e-4,0 -0.00142,0 -0.00213,0 -1.7423595,0 -3.311199,0.74275 -4.4073209,1.92881 -0.036558,0.0395 -0.043188,0.0983 -0.016254,0.14494 l 1.6162797,2.79947 c 0.047264,0.0819 0.1654436,0.0818 0.2126575,-9e-5 l 2.7023877,-4.6891 c 0.04695,-0.0815 -0.011498,-0.18401 -0.1056242,-0.18403 z m -5.6144011,7.87237 3.2356039,0 c 0.094503,0 0.1535552,-0.10231 0.106291,-0.18416 L 2.0185518,3.994515 c -0.047327,-0.082 -0.1653304,-0.0816 -0.2127959,3e-4 C 1.2933853,4.878505 1,5.904985 1,7.000015 c 0,0.62198 0.094717,1.22181 0.2703885,1.78596 0.01599,0.0514 0.063518,0.0864 0.1173239,0.0864 z" /> diff --git a/icons/valve_control/light/timing.svg b/icons/valve_control/light/timing.svg index dd2d1e1..49bcce6 100644 --- a/icons/valve_control/light/timing.svg +++ b/icons/valve_control/light/timing.svg @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> -<svg fill="#000000" width="800px" height="800px" viewBox="0 0 56 56" xmlns="http://www.w3.org/2000/svg"> +<svg fill="#505050" width="800px" height="800px" viewBox="0 0 56 56" xmlns="http://www.w3.org/2000/svg"> <path d="M 27.9999 51.9063 C 41.0546 51.9063 51.9063 41.0781 51.9063 28 C 51.9063 14.9453 41.0780 4.0937 28.0234 4.0937 C 26.7812 4.0937 26.1718 4.8437 26.1718 6.0625 L 26.1718 15.1563 C 26.1718 16.1641 26.8514 16.9844 27.8827 16.9844 C 28.9140 16.9844 29.6171 16.1641 29.6171 15.1563 L 29.6171 8.1484 C 39.9296 8.9688 47.8983 17.5 47.8983 28 C 47.8983 39.0625 39.0390 47.9219 27.9999 47.9219 C 16.9374 47.9219 8.0546 39.0625 8.0780 28 C 8.1014 23.0781 9.8593 18.6016 12.7890 15.1563 C 13.5155 14.2422 13.5624 13.1406 12.7890 12.3203 C 12.0155 11.4766 10.7030 11.5469 9.8593 12.6016 C 6.2733 16.7734 4.0937 22.1641 4.0937 28 C 4.0937 41.0781 14.9218 51.9063 27.9999 51.9063 Z M 31.7499 31.6094 C 33.6014 29.6875 33.2265 27.0625 30.9999 25.5156 L 18.6014 16.8672 C 17.4296 16.0469 16.2109 17.2656 17.0312 18.4375 L 25.6796 30.8359 C 27.2265 33.0625 29.8514 33.4609 31.7499 31.6094 Z" /> </svg> diff --git a/src/ui/panes/valve_control.rs b/src/ui/panes/valve_control.rs index 1e781c9..e42c201 100644 --- a/src/ui/panes/valve_control.rs +++ b/src/ui/panes/valve_control.rs @@ -5,8 +5,8 @@ mod valves; use std::time::{Duration, Instant}; use egui::{ - Color32, DragValue, Frame, Label, Rect, RichText, Sense, Stroke, Ui, UiBuilder, Vec2, Widget, - vec2, + Color32, DragValue, Frame, Grid, Label, Layout, Rect, RichText, Sense, Stroke, Ui, UiBuilder, + Vec2, Widget, vec2, }; use egui_extras::{Size, StripBuilder}; use itertools::Itertools; @@ -16,7 +16,7 @@ use skyward_mavlink::{ orion::{ACK_TM_DATA, NACK_TM_DATA, WACK_TM_DATA}, }; use strum::IntoEnumIterator; -use tracing::info; +use tracing::{info, warn}; use crate::{ mavlink::{MavMessage, TimedMessage}, @@ -146,17 +146,15 @@ impl ValveControlPane { fn pane_ui(&mut self) -> impl FnOnce(&mut Ui) { |ui| { let valve_chunks = Valve::iter().chunks(3); - StripBuilder::new(ui) - .sizes(Size::remainder(), 3) - .vertical(|mut strip| { + Grid::new("valves_grid") + .num_columns(3) + .spacing(Vec2::splat(5.)) + .show(ui, |ui| { for chunk in &valve_chunks { - strip.strip(|builder| { - builder.sizes(Size::remainder(), 3).horizontal(|mut strip| { - for valve in chunk { - strip.cell(self.valve_frame_ui(valve)); - } - }); - }); + for valve in chunk { + ui.scope(self.valve_frame_ui(valve)); + } + ui.end_row(); } }); } @@ -226,14 +224,11 @@ impl ValveControlPane { }; let inside_frame = |ui: &mut Ui| { - let response = ui.response(); - let visuals = ui.style().interact(&response); - let text_color = visuals.text_color(); + let text_color = ui.visuals().text_color(); let icon_size = Vec2::splat(17.); StripBuilder::new(ui) - .size(Size::exact(20.)) - .size(Size::exact(20.)) + .sizes(Size::exact(20.), 2) .vertical(|mut strip| { strip.cell(|ui| { Label::new( @@ -273,7 +268,7 @@ impl ValveControlPane { .sense(Sense::click()), |ui| { ui.set_width(200.); - ui.set_height(60.); + ui.set_height(80.); let response = ui.response(); let visuals = ui.style().interact(&response); @@ -287,6 +282,7 @@ impl ValveControlPane { .fill(fill_color) .stroke(Stroke::NONE) .inner_margin(ui.spacing().menu_margin) + .corner_radius(visuals.corner_radius) .show(ui, inside_frame); if response.clicked() { -- GitLab From ea5d75d2335948f78654a10bfba37c87b3ff4093 Mon Sep 17 00:00:00 2001 From: Federico Lolli <federico.lolli@skywarder.eu> Date: Fri, 21 Mar 2025 19:12:50 +0100 Subject: [PATCH 09/24] CHECKPOINT --- src/ui/panes/valve_control.rs | 129 ++++++++++++++++++++++++---------- 1 file changed, 91 insertions(+), 38 deletions(-) diff --git a/src/ui/panes/valve_control.rs b/src/ui/panes/valve_control.rs index e42c201..91cf456 100644 --- a/src/ui/panes/valve_control.rs +++ b/src/ui/panes/valve_control.rs @@ -5,8 +5,8 @@ mod valves; use std::time::{Duration, Instant}; use egui::{ - Color32, DragValue, Frame, Grid, Label, Layout, Rect, RichText, Sense, Stroke, Ui, UiBuilder, - Vec2, Widget, vec2, + Color32, DragValue, FontId, Frame, Grid, Label, Layout, Rect, Response, RichText, Sense, + Stroke, Ui, UiBuilder, Vec2, Widget, vec2, }; use egui_extras::{Size, StripBuilder}; use itertools::Itertools; @@ -145,14 +145,14 @@ impl PaneBehavior for ValveControlPane { impl ValveControlPane { fn pane_ui(&mut self) -> impl FnOnce(&mut Ui) { |ui| { - let valve_chunks = Valve::iter().chunks(3); + let valve_chunks = Valve::iter().enumerate().chunks(3); Grid::new("valves_grid") .num_columns(3) .spacing(Vec2::splat(5.)) .show(ui, |ui| { for chunk in &valve_chunks { - for valve in chunk { - ui.scope(self.valve_frame_ui(valve)); + for (i, valve) in chunk { + ui.scope(self.valve_frame_ui(valve, i as u8)); } ui.end_row(); } @@ -198,7 +198,7 @@ impl ValveControlPane { } } - fn valve_frame_ui(&self, valve: Valve) -> impl FnOnce(&mut Ui) { + fn valve_frame_ui(&self, valve: Valve, number: u8) -> impl FnOnce(&mut Ui) { move |ui| { let valve_str = valve.to_string(); let timing = self.valves_state.get_timing_for(valve); @@ -222,45 +222,89 @@ impl ValveControlPane { format!("ERROR: {}", err_id) } }; + let text_color = ui.visuals().text_color(); + + let valve_title_ui = |ui: &mut Ui| { + Label::new( + RichText::new(valve_str.to_ascii_uppercase()) + .color(text_color) + .size(16.0), + ) + .selectable(false) + .ui(ui); + }; - let inside_frame = |ui: &mut Ui| { - let text_color = ui.visuals().text_color(); - let icon_size = Vec2::splat(17.); + fn big_number_ui( + response: &Response, + number: u8, + text_color: Color32, + ) -> impl Fn(&mut Ui) { + move |ui: &mut Ui| { + let visuals = ui.style().interact(response); + let number = RichText::new(number.to_string()) + .color(text_color) + .font(FontId::monospace(25.)); + + let fill_color = if response.hovered() { + visuals.bg_fill.gamma_multiply(0.8).to_opaque() + } else { + visuals.bg_fill + }; + + Frame::canvas(ui.style()) + .fill(fill_color) + .stroke(Stroke::NONE) + .inner_margin(ui.spacing().menu_margin) + .corner_radius(visuals.corner_radius) + .show(ui, |ui| { + Label::new(number).selectable(false).ui(ui); + }); + } + } - StripBuilder::new(ui) - .sizes(Size::exact(20.), 2) - .vertical(|mut strip| { - strip.cell(|ui| { - Label::new( - RichText::new(valve_str.to_ascii_uppercase()) - .color(text_color) - .size(16.0), - ) + let labels_ui = |ui: &mut Ui| { + let icon_size = Vec2::splat(17.); + ui.vertical(|ui| { + ui.horizontal(|ui| { + let rect = Rect::from_min_size(ui.cursor().min, icon_size); + Icon::Timing.paint(ui, rect); + ui.allocate_rect(rect, Sense::hover()); + Label::new(format!("Timing: {}", timing_str)) .selectable(false) .ui(ui); - }); - strip.cell(|ui| { - ui.vertical(|ui| { - ui.horizontal(|ui| { - let rect = Rect::from_min_size(ui.cursor().min, icon_size); - Icon::Timing.paint(ui, rect); - ui.allocate_rect(rect, Sense::hover()); - Label::new(format!("Timing: {}", timing_str)) - .selectable(false) - .ui(ui); - }); + }); + ui.horizontal(|ui| { + let rect = Rect::from_min_size(ui.cursor().min, icon_size); + Icon::Aperture.paint(ui, rect); + ui.allocate_rect(rect, Sense::hover()); + Label::new(format!("Aperture: {}", aperture_str)) + .selectable(false) + .ui(ui); + }); + }); + }; + + fn inside_frame( + response: &Response, + valve_title_ui: impl Fn(&mut Ui), + number: &u8, + text_color: &Color32, + labels_ui: &impl Fn(&mut Ui), + ) -> impl FnOnce(&mut Ui) { + |ui: &mut Ui| { + StripBuilder::new(ui) + .sizes(Size::exact(20.), 2) + .vertical(|mut strip| { + strip.cell(valve_title_ui); + strip.cell(|ui| { ui.horizontal(|ui| { - let rect = Rect::from_min_size(ui.cursor().min, icon_size); - Icon::Aperture.paint(ui, rect); - ui.allocate_rect(rect, Sense::hover()); - Label::new(format!("Aperture: {}", aperture_str)) - .selectable(false) - .ui(ui); + big_number_ui(response, *number, *text_color)(ui); + labels_ui(ui); }); }); }); - }); - }; + } + } ui.scope_builder( UiBuilder::new() @@ -283,7 +327,16 @@ impl ValveControlPane { .stroke(Stroke::NONE) .inner_margin(ui.spacing().menu_margin) .corner_radius(visuals.corner_radius) - .show(ui, inside_frame); + .show( + ui, + inside_frame( + &response, + &valve_title_ui, + &number, + &text_color, + &labels_ui, + ), + ); if response.clicked() { info!("Clicked!"); -- GitLab From 5f8ccd8156f7ace2aefb74c0907a431a49215d69 Mon Sep 17 00:00:00 2001 From: Federico Lolli <federico.lolli@skywarder.eu> Date: Fri, 21 Mar 2025 19:49:35 +0100 Subject: [PATCH 10/24] Added minimum size --- src/ui/app.rs | 4 ++++ src/ui/panes/valve_control.rs | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/ui/app.rs b/src/ui/app.rs index 801b580..a5a1ba1 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -427,6 +427,10 @@ impl Behavior<Pane> for AppBehavior { fn tab_title_for_pane(&mut self, _pane: &Pane) -> egui::WidgetText { "Tab".into() } + + fn min_size(&self) -> f32 { + 200.0 + } } #[derive(Clone)] diff --git a/src/ui/panes/valve_control.rs b/src/ui/panes/valve_control.rs index 91cf456..87fd3e7 100644 --- a/src/ui/panes/valve_control.rs +++ b/src/ui/panes/valve_control.rs @@ -145,9 +145,11 @@ impl PaneBehavior for ValveControlPane { impl ValveControlPane { fn pane_ui(&mut self) -> impl FnOnce(&mut Ui) { |ui| { - let valve_chunks = Valve::iter().enumerate().chunks(3); + ui.set_min_width(200.0); + let n = (ui.max_rect().width() / 200.0) as usize; + let valve_chunks = Valve::iter().enumerate().chunks(n); Grid::new("valves_grid") - .num_columns(3) + .num_columns(n) .spacing(Vec2::splat(5.)) .show(ui, |ui| { for chunk in &valve_chunks { -- GitLab From e2667484c411e56f0c99a22ffda10eb5449dadb1 Mon Sep 17 00:00:00 2001 From: Federico Lolli <federico.lolli@skywarder.eu> Date: Sat, 22 Mar 2025 14:06:37 +0100 Subject: [PATCH 11/24] Updated refresh settings from frequency to period --- src/ui/panes/valve_control.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/ui/panes/valve_control.rs b/src/ui/panes/valve_control.rs index 87fd3e7..54a5502 100644 --- a/src/ui/panes/valve_control.rs +++ b/src/ui/panes/valve_control.rs @@ -142,11 +142,12 @@ impl PaneBehavior for ValveControlPane { // ┌────────────────────────┐ // │ UI METHODS │ // └────────────────────────┘ +const BTN_WIDTH: f32 = 200.0; impl ValveControlPane { fn pane_ui(&mut self) -> impl FnOnce(&mut Ui) { |ui| { - ui.set_min_width(200.0); - let n = (ui.max_rect().width() / 200.0) as usize; + ui.set_min_width(BTN_WIDTH); + let n = (ui.max_rect().width() / BTN_WIDTH) as usize; let valve_chunks = Valve::iter().enumerate().chunks(n); Grid::new("valves_grid") .num_columns(n) @@ -184,15 +185,16 @@ impl ValveControlPane { if auto_refresh { let auto_refresh_duration = auto_refresh_setting.get_or_insert(DEFAULT_AUTO_REFRESH_RATE); - let mut auto_refresh_rate = 1. / auto_refresh_duration.as_secs_f32(); - DragValue::new(&mut auto_refresh_rate) - .speed(0.1) - .range(0.1..=10.0) + let mut auto_refresh_period = auto_refresh_duration.as_secs_f32(); + DragValue::new(&mut auto_refresh_period) + .speed(0.2) + .range(0.5..=5.0) .fixed_decimals(1) .update_while_editing(false) - .suffix(" Hz") + .prefix("Every ") + .suffix(" s") .ui(ui); - *auto_refresh_duration = Duration::from_secs_f32(1. / auto_refresh_rate); + *auto_refresh_duration = Duration::from_secs_f32(auto_refresh_period); } else { *auto_refresh_setting = None; } -- GitLab From 10e9d768f12ef8c5624f90a7c1731737e1037cf5 Mon Sep 17 00:00:00 2001 From: Federico Lolli <federico.lolli@skywarder.eu> Date: Sat, 22 Mar 2025 17:37:48 +0100 Subject: [PATCH 12/24] CHECKPOINT UI design of valve buttons --- src/ui/app.rs | 2 +- src/ui/panes/valve_control.rs | 64 +++++++++++++++++++++++------------ 2 files changed, 43 insertions(+), 23 deletions(-) diff --git a/src/ui/app.rs b/src/ui/app.rs index a5a1ba1..806b0e4 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -429,7 +429,7 @@ impl Behavior<Pane> for AppBehavior { } fn min_size(&self) -> f32 { - 200.0 + 185.0 } } diff --git a/src/ui/panes/valve_control.rs b/src/ui/panes/valve_control.rs index 54a5502..29b6d4f 100644 --- a/src/ui/panes/valve_control.rs +++ b/src/ui/panes/valve_control.rs @@ -2,11 +2,16 @@ mod commands; mod icons; mod valves; -use std::time::{Duration, Instant}; +use std::{ + fmt::format, + time::{Duration, Instant}, +}; use egui::{ - Color32, DragValue, FontId, Frame, Grid, Label, Layout, Rect, Response, RichText, Sense, - Stroke, Ui, UiBuilder, Vec2, Widget, vec2, + Color32, DragValue, FontId, Frame, Grid, Label, Layout, Margin, Rect, Response, RichText, + Sense, Stroke, TextFormat, Ui, UiBuilder, Vec2, Widget, WidgetText, + text::{Fonts, LayoutJob}, + vec2, }; use egui_extras::{Size, StripBuilder}; use itertools::Itertools; @@ -142,7 +147,8 @@ impl PaneBehavior for ValveControlPane { // ┌────────────────────────┐ // │ UI METHODS │ // └────────────────────────┘ -const BTN_WIDTH: f32 = 200.0; +const BTN_WIDTH: f32 = 185.0; +const BTN_HEIGHT: f32 = 70.0; impl ValveControlPane { fn pane_ui(&mut self) -> impl FnOnce(&mut Ui) { |ui| { @@ -214,16 +220,16 @@ impl ValveControlPane { } valves::ParameterValue::Missing => "N/A".to_owned(), valves::ParameterValue::Invalid(err_id) => { - format!("ERROR: {}", err_id) + format!("ERROR({})", err_id) } }; let aperture_str = match aperture { valves::ParameterValue::Valid(value) => { - format!("{}", value) + format!("{:.2}", value) } valves::ParameterValue::Missing => "N/A".to_owned(), valves::ParameterValue::Invalid(err_id) => { - format!("ERROR: {}", err_id) + format!("ERROR({})", err_id) } }; let text_color = ui.visuals().text_color(); @@ -232,7 +238,7 @@ impl ValveControlPane { Label::new( RichText::new(valve_str.to_ascii_uppercase()) .color(text_color) - .size(16.0), + .size(15.0), ) .selectable(false) .ui(ui); @@ -247,7 +253,7 @@ impl ValveControlPane { let visuals = ui.style().interact(response); let number = RichText::new(number.to_string()) .color(text_color) - .font(FontId::monospace(25.)); + .font(FontId::monospace(20.)); let fill_color = if response.hovered() { visuals.bg_fill.gamma_multiply(0.8).to_opaque() @@ -258,7 +264,7 @@ impl ValveControlPane { Frame::canvas(ui.style()) .fill(fill_color) .stroke(Stroke::NONE) - .inner_margin(ui.spacing().menu_margin) + .inner_margin(Margin::same(5)) .corner_radius(visuals.corner_radius) .show(ui, |ui| { Label::new(number).selectable(false).ui(ui); @@ -267,23 +273,36 @@ impl ValveControlPane { } let labels_ui = |ui: &mut Ui| { - let icon_size = Vec2::splat(17.); + let icon_size = Vec2::splat(16.); + let text_format = TextFormat { + font_id: FontId::proportional(12.), + extra_letter_spacing: 0., + line_height: Some(12.), + color: text_color, + ..Default::default() + }; ui.vertical(|ui| { - ui.horizontal(|ui| { + 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()); - Label::new(format!("Timing: {}", timing_str)) - .selectable(false) - .ui(ui); + let layout_job = LayoutJob::single_section( + format!("Timing: {}", timing_str), + text_format.clone(), + ); + let galley = ui.fonts(|fonts| fonts.layout_job(layout_job)); + Label::new(galley).selectable(false).ui(ui); }); - ui.horizontal(|ui| { + 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()); - Label::new(format!("Aperture: {}", aperture_str)) - .selectable(false) - .ui(ui); + let layout_job = LayoutJob::single_section( + format!("Aperture: {}", aperture_str), + text_format, + ); + let galley = ui.fonts(|fonts| fonts.layout_job(layout_job)); + Label::new(galley).selectable(false).ui(ui); }); }); }; @@ -297,7 +316,8 @@ impl ValveControlPane { ) -> impl FnOnce(&mut Ui) { |ui: &mut Ui| { StripBuilder::new(ui) - .sizes(Size::exact(20.), 2) + .size(Size::exact(10.)) + .size(Size::exact(15.)) .vertical(|mut strip| { strip.cell(valve_title_ui); strip.cell(|ui| { @@ -315,8 +335,8 @@ impl ValveControlPane { .id_salt("valve_".to_owned() + &valve_str) .sense(Sense::click()), |ui| { - ui.set_width(200.); - ui.set_height(80.); + ui.set_width(BTN_WIDTH); + ui.set_height(BTN_HEIGHT); let response = ui.response(); let visuals = ui.style().interact(&response); -- GitLab From fb3886b5d6aad208fcc18cf365974c090fc072e4 Mon Sep 17 00:00:00 2001 From: Federico Lolli <federico.lolli@skywarder.eu> Date: Sun, 23 Mar 2025 10:56:14 +0100 Subject: [PATCH 13/24] CHECKPOINT --- src/ui/app.rs | 4 -- src/ui/panes/valve_control.rs | 79 ++++++++++++++++------------------- 2 files changed, 37 insertions(+), 46 deletions(-) diff --git a/src/ui/app.rs b/src/ui/app.rs index 806b0e4..801b580 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -427,10 +427,6 @@ impl Behavior<Pane> for AppBehavior { fn tab_title_for_pane(&mut self, _pane: &Pane) -> egui::WidgetText { "Tab".into() } - - fn min_size(&self) -> f32 { - 185.0 - } } #[derive(Clone)] diff --git a/src/ui/panes/valve_control.rs b/src/ui/panes/valve_control.rs index 29b6d4f..8488f60 100644 --- a/src/ui/panes/valve_control.rs +++ b/src/ui/panes/valve_control.rs @@ -147,21 +147,21 @@ impl PaneBehavior for ValveControlPane { // ┌────────────────────────┐ // │ UI METHODS │ // └────────────────────────┘ -const BTN_WIDTH: f32 = 185.0; -const BTN_HEIGHT: f32 = 70.0; +const BTN_MAX_WIDTH: f32 = 125.; impl ValveControlPane { fn pane_ui(&mut self) -> impl FnOnce(&mut Ui) { |ui| { - ui.set_min_width(BTN_WIDTH); - let n = (ui.max_rect().width() / BTN_WIDTH) as usize; - let valve_chunks = Valve::iter().enumerate().chunks(n); + 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); Grid::new("valves_grid") .num_columns(n) .spacing(Vec2::splat(5.)) .show(ui, |ui| { for chunk in &valve_chunks { - for (i, valve) in chunk { - ui.scope(self.valve_frame_ui(valve, i as u8)); + for (valve, symbol) in chunk { + ui.scope(self.valve_frame_ui(valve, symbol)); } ui.end_row(); } @@ -208,7 +208,7 @@ impl ValveControlPane { } } - fn valve_frame_ui(&self, valve: Valve, number: u8) -> impl FnOnce(&mut Ui) { + fn valve_frame_ui(&self, valve: Valve, symbol: char) -> impl FnOnce(&mut Ui) { move |ui| { let valve_str = valve.to_string(); let timing = self.valves_state.get_timing_for(valve); @@ -225,7 +225,7 @@ impl ValveControlPane { }; let aperture_str = match aperture { valves::ParameterValue::Valid(value) => { - format!("{:.2}", value) + format!("{:.2}%", value * 100.) } valves::ParameterValue::Missing => "N/A".to_owned(), valves::ParameterValue::Invalid(err_id) => { @@ -235,23 +235,26 @@ impl ValveControlPane { let text_color = ui.visuals().text_color(); let valve_title_ui = |ui: &mut Ui| { + ui.set_max_width(100.); Label::new( RichText::new(valve_str.to_ascii_uppercase()) .color(text_color) + .strong() .size(15.0), ) .selectable(false) + .wrap() .ui(ui); }; fn big_number_ui( response: &Response, - number: u8, + symbol: char, text_color: Color32, ) -> impl Fn(&mut Ui) { move |ui: &mut Ui| { let visuals = ui.style().interact(response); - let number = RichText::new(number.to_string()) + let number = RichText::new(symbol.to_string()) .color(text_color) .font(FontId::monospace(20.)); @@ -273,7 +276,7 @@ impl ValveControlPane { } let labels_ui = |ui: &mut Ui| { - let icon_size = Vec2::splat(16.); + let icon_size = Vec2::splat(17.); let text_format = TextFormat { font_id: FontId::proportional(12.), extra_letter_spacing: 0., @@ -282,25 +285,24 @@ impl ValveControlPane { ..Default::default() }; 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()); - let layout_job = LayoutJob::single_section( - format!("Timing: {}", timing_str), - text_format.clone(), - ); - let galley = ui.fonts(|fonts| fonts.layout_job(layout_job)); - Label::new(galley).selectable(false).ui(ui); + ui.allocate_ui(vec2(20., 10.), |ui| { + let layout_job = + LayoutJob::single_section(timing_str.clone(), text_format.clone()); + let galley = ui.fonts(|fonts| fonts.layout_job(layout_job)); + Label::new(galley).selectable(false).ui(ui); + }); }); 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()); - let layout_job = LayoutJob::single_section( - format!("Aperture: {}", aperture_str), - text_format, - ); + let layout_job = + LayoutJob::single_section(aperture_str.clone(), text_format); let galley = ui.fonts(|fonts| fonts.layout_job(layout_job)); Label::new(galley).selectable(false).ui(ui); }); @@ -309,24 +311,19 @@ impl ValveControlPane { fn inside_frame( response: &Response, - valve_title_ui: impl Fn(&mut Ui), - number: &u8, - text_color: &Color32, - labels_ui: &impl Fn(&mut Ui), + valve_title_ui: impl FnOnce(&mut Ui), + symbol: char, + text_color: Color32, + labels_ui: impl FnOnce(&mut Ui), ) -> impl FnOnce(&mut Ui) { - |ui: &mut Ui| { - StripBuilder::new(ui) - .size(Size::exact(10.)) - .size(Size::exact(15.)) - .vertical(|mut strip| { - strip.cell(valve_title_ui); - strip.cell(|ui| { - ui.horizontal(|ui| { - big_number_ui(response, *number, *text_color)(ui); - labels_ui(ui); - }); - }); + move |ui: &mut Ui| { + ui.vertical(|ui| { + valve_title_ui(ui); + ui.horizontal(|ui| { + big_number_ui(response, symbol, text_color)(ui); + labels_ui(ui); }); + }); } } @@ -335,8 +332,6 @@ impl ValveControlPane { .id_salt("valve_".to_owned() + &valve_str) .sense(Sense::click()), |ui| { - ui.set_width(BTN_WIDTH); - ui.set_height(BTN_HEIGHT); let response = ui.response(); let visuals = ui.style().interact(&response); @@ -356,8 +351,8 @@ impl ValveControlPane { inside_frame( &response, &valve_title_ui, - &number, - &text_color, + symbol, + text_color, &labels_ui, ), ); -- GitLab From ae526f91000865a3b7caf046612f079d4ea42038 Mon Sep 17 00:00:00 2001 From: Federico Lolli <federico.lolli@skywarder.eu> Date: Mon, 24 Mar 2025 14:20:05 +0100 Subject: [PATCH 14/24] CHECKPOINT added keyboard shortcut manager --- src/ui/app.rs | 54 ++-- src/ui/panes.rs | 8 +- src/ui/panes/default.rs | 9 +- src/ui/panes/messages_viewer.rs | 4 +- src/ui/panes/pid_drawing_tool.rs | 6 +- src/ui/panes/plot.rs | 4 +- src/ui/panes/valve_control.rs | 345 +++++++++++++++++++++++-- src/ui/panes/valve_control/commands.rs | 94 +++++-- src/ui/panes/valve_control/icons.rs | 35 ++- src/ui/panes/valve_control/valves.rs | 8 +- src/ui/shortcuts.rs | 169 +++++++++++- src/ui/utils.rs | 9 +- 12 files changed, 640 insertions(+), 105 deletions(-) diff --git a/src/ui/app.rs b/src/ui/app.rs index 801b580..b83784b 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 9e0ea2f..3fa948c 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 e0419ca..dd2406f 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 cfee6c6..eb96466 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 05160cc..9257cf4 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 af83540..e4616d1 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 8488f60..6564ae3 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 8593fd9..ed4d282 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 064fee5..f8d70a9 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 9580dc1..cf13fcb 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 7efa69f..d95bcaf 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 2469c54..7d1b193 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)] -- GitLab From 308375d706dfefc28ae6040aa4759768017e02a1 Mon Sep 17 00:00:00 2001 From: Federico Lolli <federico.lolli@skywarder.eu> Date: Mon, 24 Mar 2025 15:00:59 +0100 Subject: [PATCH 15/24] CHECKPOINT added wiggle icon --- icons/valve_control/dark/wiggle.svg | 45 +++++++++++ icons/valve_control/light/wiggle.svg | 45 +++++++++++ src/ui/panes/valve_control.rs | 111 ++++++++++++++++++--------- src/ui/panes/valve_control/icons.rs | 18 +++-- 4 files changed, 177 insertions(+), 42 deletions(-) create mode 100644 icons/valve_control/dark/wiggle.svg create mode 100644 icons/valve_control/light/wiggle.svg diff --git a/icons/valve_control/dark/wiggle.svg b/icons/valve_control/dark/wiggle.svg new file mode 100644 index 0000000..f033a58 --- /dev/null +++ b/icons/valve_control/dark/wiggle.svg @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> + +<svg + width="800px" + height="800px" + viewBox="0 0 24 24" + fill="none" + version="1.1" + id="svg1" + sodipodi:docname="rotate-svgrepo-com.svg" + inkscape:version="1.4 (e7c3feb1, 2024-10-09)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <defs + id="defs1" /> + <sodipodi:namedview + id="namedview1" + pagecolor="#505050" + bordercolor="#eeeeee" + borderopacity="1" + inkscape:showpageshadow="0" + inkscape:pageopacity="0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#505050" + inkscape:zoom="0.26520082" + inkscape:cx="350.67764" + inkscape:cy="444.94583" + inkscape:window-width="1104" + inkscape:window-height="847" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="0" + inkscape:current-layer="svg1" /> + <path + d="M4.06189 13C4.02104 12.6724 4 12.3387 4 12C4 7.58172 7.58172 4 12 4C14.5006 4 16.7332 5.14727 18.2002 6.94416M19.9381 11C19.979 11.3276 20 11.6613 20 12C20 16.4183 16.4183 20 12 20C9.61061 20 7.46589 18.9525 6 17.2916M9 17H6V17.2916M18.2002 4V6.94416M18.2002 6.94416V6.99993L15.2002 7M6 20V17.2916" + stroke="#000000" + stroke-width="2" + stroke-linecap="round" + stroke-linejoin="round" + id="path1" + style="fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#8c8c8c;stroke-opacity:1" /> +</svg> diff --git a/icons/valve_control/light/wiggle.svg b/icons/valve_control/light/wiggle.svg new file mode 100644 index 0000000..864577e --- /dev/null +++ b/icons/valve_control/light/wiggle.svg @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> + +<svg + width="800px" + height="800px" + viewBox="0 0 24 24" + fill="none" + version="1.1" + id="svg1" + sodipodi:docname="wiggle.svg" + inkscape:version="1.4 (e7c3feb1, 2024-10-09)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <defs + id="defs1" /> + <sodipodi:namedview + id="namedview1" + pagecolor="#505050" + bordercolor="#eeeeee" + borderopacity="1" + inkscape:showpageshadow="0" + inkscape:pageopacity="0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#505050" + inkscape:zoom="0.26520082" + inkscape:cx="350.67765" + inkscape:cy="444.94583" + inkscape:window-width="1104" + inkscape:window-height="847" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="0" + inkscape:current-layer="svg1" /> + <path + d="M4.06189 13C4.02104 12.6724 4 12.3387 4 12C4 7.58172 7.58172 4 12 4C14.5006 4 16.7332 5.14727 18.2002 6.94416M19.9381 11C19.979 11.3276 20 11.6613 20 12C20 16.4183 16.4183 20 12 20C9.61061 20 7.46589 18.9525 6 17.2916M9 17H6V17.2916M18.2002 4V6.94416M18.2002 6.94416V6.99993L15.2002 7M6 20V17.2916" + stroke="#000000" + stroke-width="2" + stroke-linecap="round" + stroke-linejoin="round" + id="path1" + style="fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#505050;stroke-opacity:1" /> +</svg> diff --git a/src/ui/panes/valve_control.rs b/src/ui/panes/valve_control.rs index 6564ae3..d1971b6 100644 --- a/src/ui/panes/valve_control.rs +++ b/src/ui/panes/valve_control.rs @@ -22,6 +22,7 @@ use strum::IntoEnumIterator; use tracing::{error, info, warn}; use crate::{ + error::ErrInstrument, mavlink::{MavMessage, TimedMessage}, ui::{ app::PaneResponse, @@ -87,6 +88,7 @@ impl Default for ValveControlPane { } impl PaneBehavior for ValveControlPane { + #[profiling::function] fn ui(&mut self, ui: &mut Ui, shortcut_handler: &mut ShortcutHandler) -> PaneResponse { let mut pane_response = PaneResponse::default(); @@ -152,6 +154,7 @@ impl PaneBehavior for ValveControlPane { pane_response } + #[profiling::function] fn get_message_subscriptions(&self) -> Box<dyn Iterator<Item = u32>> { let mut subscriptions = vec![]; if self.needs_refresh() { @@ -169,6 +172,7 @@ impl PaneBehavior for ValveControlPane { Box::new(subscriptions.into_iter()) } + #[profiling::function] fn update(&mut self, messages: &[&TimedMessage]) { if self.needs_refresh() { // TODO @@ -192,6 +196,7 @@ impl PaneBehavior for ValveControlPane { self.reset_last_refresh(); } + #[profiling::function] fn drain_outgoing_messages(&mut self) -> Vec<MavMessage> { let mut outgoing = vec![]; @@ -213,6 +218,7 @@ const BTN_MAX_WIDTH: f32 = 125.; impl ValveControlPane { fn pane_ui(&mut self) -> impl FnOnce(&mut Ui) { |ui| { + profiling::function_scope!("pane_ui"); ui.set_min_width(BTN_MAX_WIDTH); let n = (ui.max_rect().width() / BTN_MAX_WIDTH) as usize; let valve_chunks = SYMBOL_LIST.chars().zip(Valve::iter()).chunks(n.max(1)); @@ -232,6 +238,7 @@ impl ValveControlPane { fn menu_ui(&mut self) -> impl FnOnce(&mut Ui) { |ui| { + profiling::function_scope!("menu_ui"); if ui.button("Refresh now").clicked() { self.manual_refresh = true; ui.close_menu(); @@ -245,6 +252,7 @@ impl ValveControlPane { fn settings_window_ui(auto_refresh_setting: &mut Option<Duration>) -> impl FnOnce(&mut Ui) { |ui| { + profiling::function_scope!("settings_window_ui"); // Display auto refresh setting let mut auto_refresh = auto_refresh_setting.is_some(); ui.horizontal(|ui| { @@ -271,6 +279,7 @@ impl ValveControlPane { fn valve_frame_ui(&self, valve: Valve, symbol: char) -> impl FnOnce(&mut Ui) { move |ui| { + profiling::function_scope!("valve_frame_ui"); let valve_str = valve.to_string(); let timing = self.valves_state.get_timing_for(valve); let aperture = self.valves_state.get_aperture_for(valve); @@ -308,34 +317,6 @@ impl ValveControlPane { .ui(ui); }; - fn big_number_ui( - response: &Response, - symbol: char, - text_color: Color32, - ) -> impl Fn(&mut Ui) { - move |ui: &mut Ui| { - let visuals = ui.style().interact(response); - let number = RichText::new(symbol.to_string()) - .color(text_color) - .font(FontId::monospace(20.)); - - let fill_color = if response.hovered() { - visuals.bg_fill.gamma_multiply(0.8).to_opaque() - } else { - visuals.bg_fill - }; - - Frame::canvas(ui.style()) - .fill(fill_color) - .stroke(Stroke::NONE) - .inner_margin(Margin::same(5)) - .corner_radius(visuals.corner_radius) - .show(ui, |ui| { - Label::new(number).selectable(false).ui(ui); - }); - } - } - let labels_ui = |ui: &mut Ui| { let icon_size = Vec2::splat(17.); let text_format = TextFormat { @@ -377,9 +358,9 @@ impl ValveControlPane { }; fn inside_frame( - response: &Response, valve_title_ui: impl FnOnce(&mut Ui), symbol: char, + btn_fill_color: Color32, text_color: Color32, labels_ui: impl FnOnce(&mut Ui), ) -> impl FnOnce(&mut Ui) { @@ -387,7 +368,7 @@ impl ValveControlPane { ui.vertical(|ui| { valve_title_ui(ui); ui.horizontal(|ui| { - big_number_ui(response, symbol, text_color)(ui); + big_symbol_ui(symbol, btn_fill_color, text_color)(ui); labels_ui(ui); }); }); @@ -408,6 +389,12 @@ impl ValveControlPane { visuals.bg_fill.gamma_multiply(0.3) }; + let btn_fill_color = if response.hovered() { + visuals.bg_fill.gamma_multiply(0.8).to_opaque() + } else { + visuals.bg_fill + }; + Frame::canvas(ui.style()) .fill(fill_color) .stroke(Stroke::NONE) @@ -416,9 +403,9 @@ impl ValveControlPane { .show( ui, inside_frame( - &response, &valve_title_ui, symbol, + btn_fill_color, text_color, &labels_ui, ), @@ -442,11 +429,13 @@ impl ValveControlPane { 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>( valve: Valve, + key: Key, add_contents: impl FnOnce(&mut Ui) -> R, ) -> impl FnOnce(&mut Ui) -> Response { move |ui| { @@ -455,7 +444,7 @@ impl ValveControlPane { .corner_radius(ui.visuals().noninteractive().corner_radius); wiggle_btn = ui.ctx().input(|input| { - if input.key_down(ValveControlPane::WIGGLE_KEY) { + if input.key_down(key) { wiggle_btn .fill(ui.visuals().widgets.active.bg_fill) .stroke(ui.visuals().widgets.active.fg_stroke) @@ -471,16 +460,28 @@ impl ValveControlPane { .id_salt(format!("valve_control_window_{}_wiggle", valve)) .sense(Sense::click()), |ui| { - wiggle_btn.show(ui, |ui| ui.horizontal(|ui| add_contents(ui))); + wiggle_btn.show(ui, |ui| { + ui.set_width(200.); + ui.horizontal(|ui| add_contents(ui)) + }); }, ) .response } } - let wiggle_btn_response = btn_ui(valve, |ui| { + let wiggle_btn_response = btn_ui(valve, Self::WIGGLE_KEY, |ui| { + big_symbol_ui( + Self::WIGGLE_KEY + .symbol_or_name() + .chars() + .next() + .log_unwrap(), + ui.visuals().widgets.inactive.bg_fill, + ui.visuals().text_color(), + )(ui); ui.add( - Icon::Aperture + Icon::Wiggle .as_image(ui.ctx().theme()) .fit_to_exact_size(icon_size), ); @@ -488,7 +489,16 @@ impl ValveControlPane { })(ui); let mut aperture = 0_u32; - let aperture_btn_response = btn_ui(valve, |ui| { + let aperture_btn_response = btn_ui(valve, Self::APERTURE_KEY, |ui| { + big_symbol_ui( + Self::APERTURE_KEY + .symbol_or_name() + .chars() + .next() + .log_unwrap(), + ui.visuals().widgets.inactive.bg_fill, + ui.visuals().text_color(), + )(ui); ui.add( Icon::Aperture .as_image(ui.ctx().theme()) @@ -506,7 +516,16 @@ impl ValveControlPane { })(ui); let mut timing_ms = 0_u32; - let timing_btn_response = btn_ui(valve, |ui| { + let timing_btn_response = btn_ui(valve, Self::TIMING_KEY, |ui| { + big_symbol_ui( + Self::TIMING_KEY + .symbol_or_name() + .chars() + .next() + .log_unwrap(), + ui.visuals().widgets.inactive.bg_fill, + ui.visuals().text_color(), + )(ui); ui.add( Icon::Timing .as_image(ui.ctx().theme()) @@ -532,6 +551,7 @@ impl ValveControlPane { } } + #[profiling::function] fn keyboard_actions(&self, shortcut_handler: &mut ShortcutHandler) -> Option<PaneAction> { let mut key_action_pairs = Vec::new(); match self @@ -627,6 +647,23 @@ impl ValveControlPane { } } +fn big_symbol_ui(symbol: char, fill_color: Color32, text_color: Color32) -> impl Fn(&mut Ui) { + move |ui: &mut Ui| { + let number = RichText::new(symbol.to_string()) + .color(text_color) + .font(FontId::monospace(20.)); + + Frame::canvas(ui.style()) + .fill(fill_color) + .stroke(Stroke::NONE) + .inner_margin(Margin::same(5)) + .corner_radius(ui.visuals().widgets.noninteractive.corner_radius) + .show(ui, |ui| { + Label::new(number).selectable(false).ui(ui); + }); + } +} + // ┌───────────────────────────┐ // │ UTILS METHODS │ // └───────────────────────────┘ diff --git a/src/ui/panes/valve_control/icons.rs b/src/ui/panes/valve_control/icons.rs index f8d70a9..b8b14e4 100644 --- a/src/ui/panes/valve_control/icons.rs +++ b/src/ui/panes/valve_control/icons.rs @@ -7,6 +7,7 @@ use crate::error::ErrInstrument; #[derive(Debug, Clone, Copy, EnumIter)] pub enum Icon { + Wiggle, Aperture, Timing, } @@ -14,6 +15,18 @@ pub enum Icon { impl Icon { fn as_image_source(&self, theme: Theme) -> ImageSource { match (&self, theme) { + (Icon::Wiggle, Theme::Light) => { + egui::include_image!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/icons/valve_control/light/wiggle.svg" + )) + } + (Icon::Wiggle, Theme::Dark) => { + egui::include_image!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/icons/valve_control/dark/wiggle.svg" + )) + } (Icon::Aperture, Theme::Light) => { egui::include_image!(concat!( env!("CARGO_MANIFEST_DIR"), @@ -56,9 +69,4 @@ impl Icon { 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()); - } } -- GitLab From 14da876e3ce26ce03ec5c620321d4fadaaad9896 Mon Sep 17 00:00:00 2001 From: Federico Lolli <federico.lolli@skywarder.eu> Date: Mon, 24 Mar 2025 15:27:54 +0100 Subject: [PATCH 16/24] CHECKPOINT moved shortcut card (old big_symbol_ui to ui.rs custom widget ShortcutCard) --- src/ui/panes/valve_control.rs | 155 +++++++++++----------------- src/ui/panes/valve_control/icons.rs | 4 +- src/ui/panes/valve_control/ui.rs | 66 ++++++++++++ 3 files changed, 128 insertions(+), 97 deletions(-) create mode 100644 src/ui/panes/valve_control/ui.rs diff --git a/src/ui/panes/valve_control.rs b/src/ui/panes/valve_control.rs index d1971b6..618e29e 100644 --- a/src/ui/panes/valve_control.rs +++ b/src/ui/panes/valve_control.rs @@ -1,5 +1,6 @@ mod commands; mod icons; +mod ui; mod valves; use std::{ @@ -8,9 +9,9 @@ use std::{ }; use egui::{ - Color32, DragValue, FontId, Frame, Grid, Key, Label, Margin, Modal, Modifiers, Response, - RichText, Sense, Stroke, TextFormat, Ui, UiBuilder, Vec2, Widget, Window, text::LayoutJob, - vec2, + Color32, DragValue, FontId, Frame, Grid, Key, KeyboardShortcut, Label, Modal, Modifiers, + Response, RichText, Sense, Stroke, TextFormat, Ui, UiBuilder, Vec2, Widget, Window, + text::LayoutJob, vec2, }; use itertools::Itertools; use serde::{Deserialize, Serialize}; @@ -20,9 +21,9 @@ use skyward_mavlink::{ }; use strum::IntoEnumIterator; use tracing::{error, info, warn}; +use ui::ShortcutCard; use crate::{ - error::ErrInstrument, mavlink::{MavMessage, TimedMessage}, ui::{ app::PaneResponse, @@ -228,7 +229,7 @@ impl ValveControlPane { .show(ui, |ui| { for chunk in &valve_chunks { for (symbol, valve) in chunk { - ui.scope(self.valve_frame_ui(valve, symbol)); + ui.scope(self.valve_frame_ui(valve, map_symbol_to_key(symbol))); } ui.end_row(); } @@ -277,7 +278,7 @@ impl ValveControlPane { } } - fn valve_frame_ui(&self, valve: Valve, symbol: char) -> impl FnOnce(&mut Ui) { + fn valve_frame_ui(&self, valve: Valve, shortcut_key: Key) -> impl FnOnce(&mut Ui) { move |ui| { profiling::function_scope!("valve_frame_ui"); let valve_str = valve.to_string(); @@ -357,24 +358,6 @@ impl ValveControlPane { }); }; - fn inside_frame( - valve_title_ui: impl FnOnce(&mut Ui), - symbol: char, - btn_fill_color: Color32, - text_color: Color32, - labels_ui: impl FnOnce(&mut Ui), - ) -> impl FnOnce(&mut Ui) { - move |ui: &mut Ui| { - ui.vertical(|ui| { - valve_title_ui(ui); - ui.horizontal(|ui| { - big_symbol_ui(symbol, btn_fill_color, text_color)(ui); - labels_ui(ui); - }); - }); - } - } - ui.scope_builder( UiBuilder::new() .id_salt("valve_".to_owned() + &valve_str) @@ -395,21 +378,26 @@ impl ValveControlPane { visuals.bg_fill }; + let inside_frame = |ui: &mut Ui| { + ui.vertical(|ui| { + valve_title_ui(ui); + ui.horizontal(|ui| { + ShortcutCard::new(map_key_to_shortcut(shortcut_key)) + .text_color(text_color) + .fill_color(btn_fill_color) + .text_size(20.) + .ui(ui); + labels_ui(ui); + }); + }); + }; + Frame::canvas(ui.style()) .fill(fill_color) .stroke(Stroke::NONE) .inner_margin(ui.spacing().menu_margin) .corner_radius(visuals.corner_radius) - .show( - ui, - inside_frame( - &valve_title_ui, - symbol, - btn_fill_color, - text_color, - &labels_ui, - ), - ); + .show(ui, inside_frame); if response.clicked() { info!("Clicked!"); @@ -471,15 +459,11 @@ impl ValveControlPane { } let wiggle_btn_response = btn_ui(valve, Self::WIGGLE_KEY, |ui| { - big_symbol_ui( - Self::WIGGLE_KEY - .symbol_or_name() - .chars() - .next() - .log_unwrap(), - ui.visuals().widgets.inactive.bg_fill, - ui.visuals().text_color(), - )(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()) @@ -490,15 +474,11 @@ impl ValveControlPane { let mut aperture = 0_u32; let aperture_btn_response = btn_ui(valve, Self::APERTURE_KEY, |ui| { - big_symbol_ui( - Self::APERTURE_KEY - .symbol_or_name() - .chars() - .next() - .log_unwrap(), - ui.visuals().widgets.inactive.bg_fill, - ui.visuals().text_color(), - )(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()) @@ -517,15 +497,11 @@ impl ValveControlPane { let mut timing_ms = 0_u32; let timing_btn_response = btn_ui(valve, Self::TIMING_KEY, |ui| { - big_symbol_ui( - Self::TIMING_KEY - .symbol_or_name() - .chars() - .next() - .log_unwrap(), - ui.visuals().widgets.inactive.bg_fill, - ui.visuals().text_color(), - )(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()) @@ -616,27 +592,9 @@ impl ValveControlPane { 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, + map_symbol_to_key(symbol), PaneAction::OpenValveControl(self.valve_symbol_map[&symbol]), )); } @@ -647,23 +605,32 @@ impl ValveControlPane { } } -fn big_symbol_ui(symbol: char, fill_color: Color32, text_color: Color32) -> impl Fn(&mut Ui) { - move |ui: &mut Ui| { - let number = RichText::new(symbol.to_string()) - .color(text_color) - .font(FontId::monospace(20.)); - - Frame::canvas(ui.style()) - .fill(fill_color) - .stroke(Stroke::NONE) - .inner_margin(Margin::same(5)) - .corner_radius(ui.visuals().widgets.noninteractive.corner_radius) - .show(ui, |ui| { - Label::new(number).selectable(false).ui(ui); - }); +fn map_symbol_to_key(symbol: char) -> 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) + } } } +#[inline] +fn map_key_to_shortcut(key: Key) -> KeyboardShortcut { + KeyboardShortcut::new(Modifiers::NONE, key) +} + // ┌───────────────────────────┐ // │ UTILS METHODS │ // └───────────────────────────┘ diff --git a/src/ui/panes/valve_control/icons.rs b/src/ui/panes/valve_control/icons.rs index b8b14e4..654acae 100644 --- a/src/ui/panes/valve_control/icons.rs +++ b/src/ui/panes/valve_control/icons.rs @@ -1,10 +1,8 @@ -use egui::{Context, Image, ImageSource, SizeHint, TextureOptions, Theme, Ui}; +use egui::{Context, Image, ImageSource, SizeHint, TextureOptions, Theme}; use strum::IntoEnumIterator; use strum_macros::EnumIter; use tracing::error; -use crate::error::ErrInstrument; - #[derive(Debug, Clone, Copy, EnumIter)] pub enum Icon { Wiggle, diff --git a/src/ui/panes/valve_control/ui.rs b/src/ui/panes/valve_control/ui.rs new file mode 100644 index 0000000..9bbc056 --- /dev/null +++ b/src/ui/panes/valve_control/ui.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 + } +} -- GitLab From 0c590eca6090b561103c022b4c6af007bea0d3c2 Mon Sep 17 00:00:00 2001 From: Federico Lolli <federico.lolli@skywarder.eu> Date: Mon, 24 Mar 2025 16:17:31 +0100 Subject: [PATCH 17/24] CHECKPOINT connected click and shortcuts in opening windows --- src/ui/panes/valve_control.rs | 158 ++++++++++++++++++++-------------- 1 file changed, 94 insertions(+), 64 deletions(-) diff --git a/src/ui/panes/valve_control.rs b/src/ui/panes/valve_control.rs index 618e29e..6a6e152 100644 --- a/src/ui/panes/valve_control.rs +++ b/src/ui/panes/valve_control.rs @@ -20,7 +20,7 @@ use skyward_mavlink::{ orion::{ACK_TM_DATA, NACK_TM_DATA, WACK_TM_DATA}, }; use strum::IntoEnumIterator; -use tracing::{error, info, warn}; +use tracing::{info, trace, warn}; use ui::ShortcutCard; use crate::{ @@ -40,6 +40,26 @@ use valves::{Valve, ValveStateManager}; const DEFAULT_AUTO_REFRESH_RATE: Duration = Duration::from_secs(1); const SYMBOL_LIST: &str = "123456789-/."; +fn map_symbol_to_key(symbol: char) -> 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, + _ => { + unreachable!("Invalid symbol: {}", symbol); + } + } +} + #[derive(Clone, PartialEq, Serialize, Deserialize, Debug)] pub struct ValveControlPane { // INTERNAL @@ -63,7 +83,7 @@ pub struct ValveControlPane { #[serde(skip)] is_settings_window_open: bool, #[serde(skip)] - valve_symbol_map: HashMap<char, Valve>, + valve_key_map: HashMap<Valve, Key>, #[serde(skip)] valve_window_states: HashMap<Valve, ValveWindowState>, } @@ -71,7 +91,9 @@ pub struct ValveControlPane { 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_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(); @@ -82,7 +104,7 @@ impl Default for ValveControlPane { manual_refresh: false, last_refresh: None, is_settings_window_open: false, - valve_symbol_map: valve_symbol, + valve_key_map, valve_window_states, } } @@ -146,6 +168,10 @@ impl PaneBehavior for ValveControlPane { .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), @@ -229,7 +255,15 @@ impl ValveControlPane { .show(ui, |ui| { for chunk in &valve_chunks { for (symbol, valve) in chunk { - ui.scope(self.valve_frame_ui(valve, map_symbol_to_key(symbol))); + let response = ui + .scope(self.valve_frame_ui(valve, map_symbol_to_key(symbol))) + .inner; + + if response.clicked() { + info!("Clicked on valve: {:?}", valve); + self.valve_window_states + .insert(valve, ValveWindowState::Open); + } } ui.end_row(); } @@ -278,7 +312,7 @@ impl ValveControlPane { } } - fn valve_frame_ui(&self, valve: Valve, shortcut_key: Key) -> impl FnOnce(&mut Ui) { + fn valve_frame_ui(&self, valve: Valve, shortcut_key: Key) -> impl FnOnce(&mut Ui) -> Response { move |ui| { profiling::function_scope!("valve_frame_ui"); let valve_str = valve.to_string(); @@ -364,19 +398,28 @@ impl ValveControlPane { .sense(Sense::click()), |ui| { let response = ui.response(); + let shortcut_key_is_down = ui + .ctx() + .input(|input| input.key_down(self.valve_key_map[&valve])); let visuals = ui.style().interact(&response); - let fill_color = if response.hovered() { - visuals.bg_fill - } else { - visuals.bg_fill.gamma_multiply(0.3) - }; - - let btn_fill_color = if response.hovered() { - visuals.bg_fill.gamma_multiply(0.8).to_opaque() - } else { - visuals.bg_fill - }; + let (fill_color, btn_fill_color, stroke) = + if response.clicked() || shortcut_key_is_down { + let visuals = ui.visuals().widgets.active; + (visuals.bg_fill, visuals.bg_fill, visuals.bg_stroke) + } else if response.hovered() { + ( + visuals.bg_fill, + visuals.bg_fill.gamma_multiply(0.8).to_opaque(), + visuals.bg_stroke, + ) + } else { + ( + visuals.bg_fill.gamma_multiply(0.3), + visuals.bg_fill, + Stroke::new(1.0, Color32::TRANSPARENT), + ) + }; let inside_frame = |ui: &mut Ui| { ui.vertical(|ui| { @@ -394,16 +437,13 @@ impl ValveControlPane { Frame::canvas(ui.style()) .fill(fill_color) - .stroke(Stroke::NONE) + .stroke(stroke) .inner_margin(ui.spacing().menu_margin) .corner_radius(visuals.corner_radius) .show(ui, inside_frame); - - if response.clicked() { - info!("Clicked!"); - } }, - ); + ) + .response } } @@ -427,31 +467,42 @@ impl ValveControlPane { add_contents: impl FnOnce(&mut Ui) -> R, ) -> impl FnOnce(&mut Ui) -> Response { move |ui| { - let mut wiggle_btn = Frame::canvas(ui.style()) + let 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(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.set_width(200.); - ui.horizontal(|ui| add_contents(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 stroke = if shortcut_down || clicked { + let visuals = ui.visuals().widgets.active; + visuals.bg_stroke + } else if response.hovered() { + visuals.bg_stroke + } else { + Stroke::new(1., Color32::TRANSPARENT) + }; + + wiggle_btn + .fill(visuals.bg_fill.gamma_multiply(0.3).to_opaque()) + .stroke(stroke) + .stroke(stroke) + .show(ui, |ui| { + ui.set_width(200.); + ui.horizontal(|ui| add_contents(ui)) + }); + + if response.clicked() { + info!("Clicked!"); + } }, ) .response @@ -591,11 +642,11 @@ impl ValveControlPane { 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() { + for (&valve, &key) in self.valve_key_map.iter() { key_action_pairs.push(( Modifiers::NONE, - map_symbol_to_key(symbol), - PaneAction::OpenValveControl(self.valve_symbol_map[&symbol]), + key, + PaneAction::OpenValveControl(valve), )); } shortcut_handler @@ -605,27 +656,6 @@ impl ValveControlPane { } } -fn map_symbol_to_key(symbol: char) -> 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) - } - } -} - #[inline] fn map_key_to_shortcut(key: Key) -> KeyboardShortcut { KeyboardShortcut::new(Modifiers::NONE, key) -- GitLab From bab943555e3592e22d3bdb8000749ae3861ac4b6 Mon Sep 17 00:00:00 2001 From: Federico Lolli <federico.lolli@skywarder.eu> Date: Mon, 24 Mar 2025 16:27:48 +0100 Subject: [PATCH 18/24] fixed fill color and bad light theme adaptation --- src/ui/panes/valve_control.rs | 46 ++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/src/ui/panes/valve_control.rs b/src/ui/panes/valve_control.rs index 6a6e152..cfa1ffa 100644 --- a/src/ui/panes/valve_control.rs +++ b/src/ui/panes/valve_control.rs @@ -403,23 +403,24 @@ impl ValveControlPane { .input(|input| input.key_down(self.valve_key_map[&valve])); let visuals = ui.style().interact(&response); - let (fill_color, btn_fill_color, stroke) = - if response.clicked() || shortcut_key_is_down { - let visuals = ui.visuals().widgets.active; - (visuals.bg_fill, visuals.bg_fill, visuals.bg_stroke) - } else if response.hovered() { - ( - visuals.bg_fill, - visuals.bg_fill.gamma_multiply(0.8).to_opaque(), - visuals.bg_stroke, - ) - } else { - ( - visuals.bg_fill.gamma_multiply(0.3), - visuals.bg_fill, - Stroke::new(1.0, Color32::TRANSPARENT), - ) - }; + let (fill_color, btn_fill_color, stroke) = if response.clicked() + || shortcut_key_is_down && self.valve_window_states[&valve].is_closed() + { + let visuals = ui.visuals().widgets.active; + (visuals.bg_fill, visuals.bg_fill, visuals.bg_stroke) + } else if response.hovered() { + ( + visuals.bg_fill, + visuals.bg_fill.gamma_multiply(0.8).to_opaque(), + visuals.bg_stroke, + ) + } else { + ( + visuals.bg_fill.gamma_multiply(0.3), + visuals.bg_fill, + Stroke::new(1.0, Color32::TRANSPARENT), + ) + }; let inside_frame = |ui: &mut Ui| { ui.vertical(|ui| { @@ -482,17 +483,18 @@ impl ValveControlPane { let shortcut_down = ui.ctx().input(|input| input.key_down(key)); let visuals = ui.style().interact(&response); - let stroke = if shortcut_down || clicked { + let (fill_color, stroke) = if shortcut_down || clicked { let visuals = ui.visuals().widgets.active; - visuals.bg_stroke + (visuals.bg_fill, visuals.bg_stroke) } else if response.hovered() { - visuals.bg_stroke + (visuals.bg_fill, visuals.bg_stroke) } else { - Stroke::new(1., Color32::TRANSPARENT) + let stroke = Stroke::new(1., Color32::TRANSPARENT); + (visuals.bg_fill.gamma_multiply(0.3), stroke) }; wiggle_btn - .fill(visuals.bg_fill.gamma_multiply(0.3).to_opaque()) + .fill(fill_color) .stroke(stroke) .stroke(stroke) .show(ui, |ui| { -- GitLab From ced64b4e061ea6991a566a2f87716cf823879eb4 Mon Sep 17 00:00:00 2001 From: Federico Lolli <federico.lolli@skywarder.eu> Date: Mon, 24 Mar 2025 17:56:47 +0100 Subject: [PATCH 19/24] CHECKPOINT --- src/ui/panes/valve_control.rs | 134 ++++++++++++++++++--------- src/ui/panes/valve_control/valves.rs | 10 ++ 2 files changed, 99 insertions(+), 45 deletions(-) diff --git a/src/ui/panes/valve_control.rs b/src/ui/panes/valve_control.rs index cfa1ffa..793d004 100644 --- a/src/ui/panes/valve_control.rs +++ b/src/ui/panes/valve_control.rs @@ -139,15 +139,13 @@ impl PaneBehavior for ValveControlPane { 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); + 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.valve_window_states - .insert(valve, ValveWindowState::Closed); + self.set_window_state(valve, ValveWindowState::Closed); } } // Ignore otherwise @@ -261,8 +259,7 @@ impl ValveControlPane { if response.clicked() { info!("Clicked on valve: {:?}", valve); - self.valve_window_states - .insert(valve, ValveWindowState::Open); + self.set_window_state(valve, ValveWindowState::Open); } } ui.end_row(); @@ -398,13 +395,12 @@ impl ValveControlPane { .sense(Sense::click()), |ui| { let response = ui.response(); - let shortcut_key_is_down = ui - .ctx() - .input(|input| input.key_down(self.valve_key_map[&valve])); + let shortcut_key_is_down = ui.ctx().input(|input| input.key_down(shortcut_key)); let visuals = ui.style().interact(&response); let (fill_color, btn_fill_color, stroke) = if response.clicked() - || shortcut_key_is_down && self.valve_window_states[&valve].is_closed() + || shortcut_key_is_down + && self.valve_window_states.values().all(|&v| v.is_closed()) { let visuals = ui.visuals().widgets.active; (visuals.bg_fill, visuals.bg_fill, visuals.bg_stroke) @@ -455,7 +451,7 @@ impl ValveControlPane { fn valve_control_window_ui( &mut self, valve: Valve, - action: Option<PaneAction>, + mut action: Option<PaneAction>, ) -> impl FnOnce(&mut Ui) { move |ui| { profiling::function_scope!("valve_control_window_ui"); @@ -463,7 +459,7 @@ impl ValveControlPane { let text_size = 16.; fn btn_ui<R>( - valve: Valve, + window_state: &ValveWindowState, key: Key, add_contents: impl FnOnce(&mut Ui) -> R, ) -> impl FnOnce(&mut Ui) -> Response { @@ -472,18 +468,15 @@ impl ValveControlPane { .inner_margin(ui.spacing().menu_margin) .corner_radius(ui.visuals().noninteractive().corner_radius); - ui.scope_builder( - UiBuilder::new() - .id_salt(format!("valve_control_window_{}_wiggle", valve)) - .sense(Sense::click()), - |ui| { - let response = ui.response(); + 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 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 shortcut_down || clicked { + 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() { @@ -493,25 +486,25 @@ impl ValveControlPane { (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)) - }); + 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!"); - } - }, - ) + if response.clicked() { + info!("Clicked!"); + } + }) .response } } - let wiggle_btn_response = btn_ui(valve, Self::WIGGLE_KEY, |ui| { + 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) @@ -525,8 +518,8 @@ impl ValveControlPane { 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, Self::APERTURE_KEY, |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) @@ -538,18 +531,24 @@ impl ValveControlPane { .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(1) + .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(valve, Self::TIMING_KEY, |ui| { + 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) @@ -561,6 +560,7 @@ impl ValveControlPane { .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) @@ -569,14 +569,53 @@ impl ValveControlPane { .update_while_editing(false) .suffix(" [ms]"), ); + if matches!(window_state, ValveWindowState::TimingFocused) { + ui.ctx().memory_mut(|m| { + m.request_focus(drag_value_id); + }); + } })(ui); - if wiggle_btn_response.clicked() || matches!(action, Some(PaneAction::Wiggle)) { - info!("Wiggle valve: {:?}", valve); - self.commands.push(Command::wiggle(valve).into()); + // 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); + } + _ => {} } - // self.valve_window_states - // .insert(valve, ValveWindowState::Closed); } } @@ -685,6 +724,11 @@ 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)] diff --git a/src/ui/panes/valve_control/valves.rs b/src/ui/panes/valve_control/valves.rs index cf13fcb..cf9b15a 100644 --- a/src/ui/panes/valve_control/valves.rs +++ b/src/ui/panes/valve_control/valves.rs @@ -128,6 +128,16 @@ pub enum ParameterValue<T, E> { Invalid(E), // E is the reason why the parameter is invalid } +impl<T, E> ParameterValue<T, E> { + pub fn valid_or(self, default: T) -> T { + match self { + Self::Valid(value) => value, + Self::Missing => default, + Self::Invalid(_) => default, + } + } +} + impl<T: Display, E: Display> Display for ParameterValue<T, E> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { -- GitLab 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 20/24] 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 From 8bc214289b65182ee59131b10bff4789f187701e Mon Sep 17 00:00:00 2001 From: Federico Lolli <federico.lolli@skywarder.eu> Date: Wed, 2 Apr 2025 20:20:34 +0200 Subject: [PATCH 21/24] Fixed issue with cargo field --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 5698828..8c2de1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ authors = [ "Niccolò Betto <niccolo.betto@skywarder.eu>", ] edition = "2024" -edescription = "Skyward Enhanced Ground Software" +description = "Skyward Enhanced Ground Software" license = "MIT" [dependencies] -- GitLab From adb2728deb5a5e97676cd6884678638fc1f4fb8f Mon Sep 17 00:00:00 2001 From: Federico Lolli <federico.lolli@skywarder.eu> Date: Thu, 10 Apr 2025 19:55:47 +0200 Subject: [PATCH 22/24] WIP --- Cargo.lock | 12 +- Cargo.toml | 1 + src/ui/panes/valve_control.rs | 82 +-- src/ui/panes/valve_control/ui.rs | 2 +- .../panes/valve_control/ui/shortcut_widget.rs | 9 +- .../valve_control/ui/valve_control_window.rs | 544 ++++++++++++++---- 6 files changed, 489 insertions(+), 161 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f65be72..28368be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1050,7 +1050,7 @@ checksum = "67756b63b283a65bd0534b0c2a5fb1a12a5768bb6383d422147cc93193d09cfc" dependencies = [ "ahash", "egui", - "itertools", + "itertools 0.13.0", "log", "serde", ] @@ -1827,6 +1827,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -3059,6 +3068,7 @@ dependencies = [ "egui_tiles", "enum_dispatch", "glam", + "itertools 0.14.0", "mavlink-bindgen", "mint", "profiling", diff --git a/Cargo.toml b/Cargo.toml index 8c2de1b..b049c64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ egui_plot = "0.31" egui_file = "0.22" enum_dispatch = "0.3" glam = { version = "0.29", features = ["serde", "mint"] } +itertools = "0.14.0" mint = "0.5.9" profiling = "1.0" ring-channel = "0.12.0" diff --git a/src/ui/panes/valve_control.rs b/src/ui/panes/valve_control.rs index 73289d7..ed6743b 100644 --- a/src/ui/panes/valve_control.rs +++ b/src/ui/panes/valve_control.rs @@ -33,7 +33,7 @@ use super::PaneBehavior; use commands::CommandSM; use icons::Icon; -use ui::{ShortcutCard, ValveControlWindow, map_key_to_shortcut}; +use ui::{ShortcutCard, ValveControlView, map_key_to_shortcut}; use valves::{Valve, ValveStateManager}; const DEFAULT_AUTO_REFRESH_RATE: Duration = Duration::from_secs(1); @@ -84,7 +84,7 @@ pub struct ValveControlPane { #[serde(skip)] valve_key_map: HashMap<Valve, Key>, #[serde(skip)] - valve_window: Option<ValveControlWindow>, + valve_view: Option<ValveControlView>, } impl Default for ValveControlPane { @@ -101,7 +101,7 @@ impl Default for ValveControlPane { last_refresh: None, is_settings_window_open: false, valve_key_map, - valve_window: None, + valve_view: None, } } } @@ -114,48 +114,48 @@ impl PaneBehavior for ValveControlPane { // 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); - ui.allocate_space(ui.available_size()); - }) - .response; - - // Show the menu when the user right-clicks the pane - res.context_menu(self.menu_ui()); - - // Check if the user started dragging the pane - if res.drag_started() { - pane_response.set_drag_started(); - } - - // capture actions from keyboard shortcuts - let action = self.keyboard_actions(shortcut_handler); + if let Some(valve_view) = &mut self.valve_view { + if let Some(command) = valve_view.ui(ui, shortcut_handler) { + self.commands.push(command.into()); + } - match action { - // Open the valve control window if the action is to open it - Some(PaneAction::OpenValveControl(valve)) => { - self.valve_window.replace(ValveControlWindow::new(valve)); + if valve_view.is_closed() { + self.valve_view = None; + } + } else { + let res = ui + .scope_builder(UiBuilder::new().sense(Sense::click_and_drag()), |ui| { + self.pane_ui()(ui); + ui.allocate_space(ui.available_size()); + }) + .response; + + // Show the menu when the user right-clicks the pane + res.context_menu(self.menu_ui()); + + // Check if the user started dragging the pane + if res.drag_started() { + pane_response.set_drag_started(); } - None => {} - } - 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::settings_window_ui(&mut self.auto_refresh)); + // capture actions from keyboard shortcuts + let action = self.keyboard_actions(shortcut_handler); - if let Some(valve_window) = &mut self.valve_window { - if let Some(command) = valve_window.ui(ui, shortcut_handler) { - self.commands.push(command.into()); + match action { + // Open the valve control window if the action is to open it + Some(PaneAction::OpenValveControl(valve)) => { + self.valve_view.replace(ValveControlView::new(valve)); + } + None => {} } - if valve_window.is_closed() { - self.valve_window = None; - } + 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::settings_window_ui(&mut self.auto_refresh)); } pane_response @@ -241,7 +241,7 @@ impl ValveControlPane { if response.clicked() { info!("Clicked on valve: {:?}", valve); - self.valve_window = Some(ValveControlWindow::new(valve)); + self.valve_view = Some(ValveControlView::new(valve)); } } ui.end_row(); @@ -381,7 +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.is_none() + || shortcut_key_is_down && self.valve_view.is_none() { let visuals = ui.visuals().widgets.active; (visuals.bg_fill, visuals.bg_fill, visuals.bg_stroke) diff --git a/src/ui/panes/valve_control/ui.rs b/src/ui/panes/valve_control/ui.rs index 2c5a1d3..6350f61 100644 --- a/src/ui/panes/valve_control/ui.rs +++ b/src/ui/panes/valve_control/ui.rs @@ -6,7 +6,7 @@ use egui::{Key, KeyboardShortcut, Modifiers}; // Re-export the modules for the UI modules use super::{commands, icons, valves}; -pub use {shortcut_widget::ShortcutCard, valve_control_window::ValveControlWindow}; +pub use {shortcut_widget::ShortcutCard, valve_control_window::ValveControlView}; #[inline] pub fn map_key_to_shortcut(key: Key) -> KeyboardShortcut { diff --git a/src/ui/panes/valve_control/ui/shortcut_widget.rs b/src/ui/panes/valve_control/ui/shortcut_widget.rs index 9bbc056..212d895 100644 --- a/src/ui/panes/valve_control/ui/shortcut_widget.rs +++ b/src/ui/panes/valve_control/ui/shortcut_widget.rs @@ -6,6 +6,7 @@ use egui::{ pub struct ShortcutCard { shortcut: KeyboardShortcut, text_size: f32, + margin: Margin, text_color: Option<Color32>, fill_color: Option<Color32>, } @@ -30,7 +31,7 @@ impl Widget for ShortcutCard { Frame::canvas(ui.style()) .fill(fill_color) .stroke(Stroke::NONE) - .inner_margin(Margin::same(5)) + .inner_margin(self.margin) .corner_radius(corner_radius) .show(ui, |ui| { Label::new(number).selectable(false).ui(ui); @@ -44,6 +45,7 @@ impl ShortcutCard { Self { shortcut, text_size: 20., + margin: Margin::same(5), text_color: None, fill_color: None, } @@ -63,4 +65,9 @@ impl ShortcutCard { self.fill_color = Some(fill_color); self } + + pub fn margin(mut self, margin: Margin) -> Self { + self.margin = margin; + 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 index 13a2642..5fa463d 100644 --- a/src/ui/panes/valve_control/ui/valve_control_window.rs +++ b/src/ui/panes/valve_control/ui/valve_control_window.rs @@ -1,7 +1,8 @@ use egui::{ - Color32, DragValue, Frame, Key, Label, Modal, Modifiers, Response, RichText, Sense, Stroke, Ui, - UiBuilder, Vec2, Widget, + Align, Button, Color32, Direction, DragValue, FontId, Frame, Grid, Key, Label, Layout, Margin, + Modifiers, Response, RichText, Sense, Stroke, TextEdit, Ui, UiBuilder, Vec2, Widget, }; +use egui_extras::{Size, Strip, StripBuilder}; use tracing::info; use crate::ui::shortcuts::{ShortcutHandler, ShortcutMode}; @@ -12,29 +13,33 @@ use super::{ }; const WIGGLE_KEY: Key = Key::Minus; -const TIMING_KEY: Key = Key::Slash; -const APERTURE_KEY: Key = Key::Period; +/// Key used to focus on the aperture field +const FOCUS_APERTURE_KEY: Key = Key::Num1; +/// Key used to focus on the timing field +const FOCUS_TIMING_KEY: Key = Key::Num2; +/// Key used to set the parameter and loose focus on the field +const SET_PAR_KEY: Key = Key::Plus; #[derive(Debug, Clone, PartialEq)] -pub struct ValveControlWindow { +pub struct ValveControlView { valve: Valve, - state: ValveWindowState, + state: ValveViewState, timing_ms: u32, aperture_perc: f32, } -impl ValveControlWindow { - pub fn new(valve: Valve) -> ValveControlWindow { - ValveControlWindow { +impl ValveControlView { + pub fn new(valve: Valve) -> ValveControlView { + ValveControlView { valve, - state: ValveWindowState::Open, + state: ValveViewState::Open, timing_ms: 0, aperture_perc: 0.0, } } pub fn is_closed(&self) -> bool { - matches!(self.state, ValveWindowState::Closed) + matches!(self.state, ValveViewState::Closed) } #[profiling::function] @@ -47,27 +52,26 @@ impl ValveControlWindow { // 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)); + // Draw the view inside the pane + ui.scope(self.draw_view_ui(&mut action)); // Handle the actions self.handle_actions(action) } - fn draw_window_ui(&mut self, action: &mut Option<WindowAction>) -> impl FnOnce(&mut Ui) { + fn draw_view_ui(&mut self, action: &mut Option<WindowAction>) -> impl FnOnce(&mut Ui) { |ui: &mut Ui| { - let icon_size = Vec2::splat(25.); - let text_size = 16.; + let icon_size = Vec2::splat(20.); + let text_size = 14.; fn btn_ui<R>( - window_state: &ValveWindowState, + window_state: &ValveViewState, 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) + let btn = Frame::canvas(ui.style()) + .inner_margin(Margin::same(4)) .corner_radius(ui.visuals().noninteractive().corner_radius); ui.scope_builder(UiBuilder::new().id_salt(key).sense(Sense::click()), |ui| { @@ -88,12 +92,11 @@ impl ValveControlWindow { (visuals.bg_fill.gamma_multiply(0.3), stroke) }; - wiggle_btn - .fill(fill_color) + btn.fill(fill_color) .stroke(stroke) .stroke(stroke) .show(ui, |ui| { - ui.set_width(200.); + ui.set_width(ui.available_width()); ui.horizontal(|ui| add_contents(ui)) }); @@ -105,84 +108,391 @@ impl ValveControlWindow { } } - 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); - }); + let valid_fill = ui + .visuals() + .widgets + .inactive + .bg_fill + .lerp_to_gamma(Color32::GREEN, 0.3); + let invalid_fill = ui + .visuals() + .widgets + .inactive + .bg_fill + .lerp_to_gamma(Color32::RED, 0.3); + + fn shortcut_ui(ui: &Ui, key: &Key) -> ShortcutCard { + let vis = ui.visuals(); + ShortcutCard::new(map_key_to_shortcut(*key)) + .text_color(vis.strong_text_color()) + .fill_color(vis.gray_out(vis.widgets.inactive.bg_fill)) + .margin(Margin::symmetric(5, 2)) + .text_size(12.) + } + + fn add_parameter_btn(ui: &mut Ui, key: Key) -> Response { + ui.scope_builder(UiBuilder::new().id_salt(key).sense(Sense::click()), |ui| { + Frame::canvas(ui.style()) + .inner_margin(Margin::symmetric(4, 2)) + .outer_margin(0) + .corner_radius(ui.visuals().noninteractive().corner_radius) + .fill(ui.visuals().widgets.inactive.bg_fill) + .stroke(Stroke::new(1., Color32::TRANSPARENT)) + .show(ui, |ui| { + ui.set_height(ui.available_height()); + ui.horizontal_centered(|ui| { + ui.set_height(21.); + ui.add_space(1.); + Label::new( + RichText::new("SET") + .size(16.) + .color(ui.visuals().widgets.inactive.text_color()), + ) + .selectable(false) + .ui(ui); + shortcut_ui(ui, &key).ui(ui); + }); + }); + }) + .response + } + + // set aperture and timing buttons + let aperture_btn: Box<dyn FnOnce(&mut Ui) -> Response> = match self.state { + ValveViewState::Open => Box::new(|ui| add_parameter_btn(ui, FOCUS_APERTURE_KEY)), + ValveViewState::ApertureFocused => { + Box::new(|ui| add_parameter_btn(ui, SET_PAR_KEY)) } - })(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]"), + ValveViewState::TimingFocused | ValveViewState::Closed => { + Box::new(|ui| ui.response()) + } + }; + + // set timing button + let timing_btn: Box<dyn FnOnce(&mut Ui) -> Response> = match self.state { + ValveViewState::Open => Box::new(|ui| add_parameter_btn(ui, FOCUS_TIMING_KEY)), + ValveViewState::TimingFocused => Box::new(|ui| add_parameter_btn(ui, SET_PAR_KEY)), + ValveViewState::ApertureFocused | ValveViewState::Closed => { + Box::new(|ui| ui.response()) + } + }; + + // wiggle button with shortcut + let wiggle_btn = |ui: &mut Ui| { + ui.scope_builder( + UiBuilder::new().id_salt(WIGGLE_KEY).sense(Sense::click()), + |ui| { + Frame::canvas(ui.style()) + .inner_margin(Margin::symmetric(4, 2)) + .outer_margin(0) + .corner_radius(ui.visuals().noninteractive().corner_radius) + .fill(ui.visuals().widgets.inactive.bg_fill) + .stroke(Stroke::new(1., Color32::TRANSPARENT)) + .show(ui, |ui| { + ui.set_height(ui.available_height()); + ui.horizontal_centered(|ui| { + ui.set_height(21.); + ui.add_space(1.); + Label::new( + RichText::new("WIGGLE") + .size(16.) + .color(ui.visuals().widgets.inactive.text_color()), + ) + .selectable(false) + .ui(ui); + ui.add( + Icon::Wiggle + .as_image(ui.ctx().theme()) + .fit_to_exact_size(Vec2::splat(22.)), + ); + shortcut_ui(ui, &WIGGLE_KEY).ui(ui); + }); + }); + }, ); - if matches!(&self.state, ValveWindowState::TimingFocused) { - ui.ctx().memory_mut(|m| { - m.request_focus(drag_value_id); + }; + + ui.with_layout(Layout::centered_and_justified(Direction::TopDown), |ui| { + ui.set_max_width(300.); + ui.set_min_height(50.); + StripBuilder::new(ui) + .size(Size::exact(5.)) + .sizes(Size::initial(5.), 3) + .vertical(|mut strip| { + strip.empty(); + // strip.cell(|ui| { + // // ui.add_sized( + // // Vec2::new(ui.available_width(), 0.0), + // // Button::new("Wiggle"), + // // ); + // let wiggle_btn_response = btn_ui(&self.state, WIGGLE_KEY, |ui| { + // shortcut_ui(ui, &WIGGLE_KEY).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); + // }); + strip.strip(|builder| { + builder + // .size(Size::exact(230.)) + .size(Size::initial(10.)) + .size(Size::exact(25.)) + .horizontal(|mut strip| { + strip.strip(|builder| { + builder + .size(Size::remainder()) + .size(Size::initial(5.)) + .size(Size::remainder()) + .vertical(|mut strip| { + strip.empty(); + strip.cell(|ui| { + ui.with_layout( + Layout::right_to_left(Align::Min), + |ui| { + Label::new( + RichText::new( + self.valve + .to_string() + .to_uppercase(), + ) + .color( + ui.visuals() + .strong_text_color(), + ) + .size(16.), + ) + .ui(ui); + Label::new( + RichText::new("VALVE: ").size(16.), + ) + .selectable(false) + .ui(ui); + }, + ); + }); + strip.empty(); + }); + }); + strip.cell(wiggle_btn); + }); + }); + strip.strip(|builder| { + builder + .sizes(Size::initial(85.), 4) + .horizontal(|mut strip| { + strip.strip(|builder| { + builder + .size(Size::remainder()) + .size(Size::initial(5.)) + .size(Size::remainder()) + .vertical(|mut strip| { + strip.empty(); + strip.cell(|ui| { + ui.with_layout( + Layout::right_to_left(Align::Min), + |ui| { + Label::new( + RichText::new("APERTURE:") + .size(16.), + ) + .selectable(false) + .ui(ui); + }, + ); + }); + strip.empty(); + }); + }); + strip.cell(|ui| { + Frame::canvas(ui.style()) + .outer_margin(0) + .inner_margin(Margin::symmetric(0, 3)) + .corner_radius( + ui.visuals().noninteractive().corner_radius, + ) + .fill(invalid_fill) + .stroke(Stroke::new(1., Color32::TRANSPARENT)) + .show(ui, |ui| { + Label::new( + RichText::new("0.813").size(14.).strong(), + ) + .ui(ui); + }); + }); + strip.cell(|ui| { + Frame::canvas(ui.style()) + .inner_margin(Margin::symmetric(0, 3)) + .outer_margin(0) + .corner_radius( + ui.visuals().noninteractive().corner_radius, + ) + .fill(ui.visuals().widgets.inactive.bg_fill) + .stroke(Stroke::new(1., Color32::TRANSPARENT)) + .show(ui, |ui| { + let res = ui.add_sized( + Vec2::new(ui.available_width(), 0.0), + DragValue::new(&mut self.aperture_perc) + .speed(0.5) + .range(0.0..=100.0) + .fixed_decimals(0) + .update_while_editing(false) + .suffix("%"), + ); + if res.gained_focus() { + self.state = ValveViewState::ApertureFocused; + } + }); + }); + strip.cell(|ui| { + aperture_btn(ui); + }); + }); + }); + strip.strip(|builder| { + builder + .sizes(Size::initial(85.), 4) + .horizontal(|mut strip| { + strip.strip(|builder| { + builder + .size(Size::remainder()) + .size(Size::initial(10.)) + .size(Size::remainder()) + .vertical(|mut strip| { + strip.empty(); + strip.cell(|ui| { + ui.with_layout( + Layout::right_to_left(Align::Min), + |ui| { + Label::new( + RichText::new("TIMING:").size(16.), + ) + .selectable(false) + .ui(ui); + }, + ); + }); + strip.empty(); + }); + }); + strip.cell(|ui| { + Frame::canvas(ui.style()) + .inner_margin(Margin::same(4)) + .corner_radius( + ui.visuals().noninteractive().corner_radius, + ) + .fill(valid_fill) + .stroke(Stroke::new(1., Color32::TRANSPARENT)) + .show(ui, |ui| { + Label::new( + RichText::new("650ms").size(14.).strong(), + ) + .ui(ui); + }); + }); + strip.cell(|ui| { + Frame::canvas(ui.style()) + .inner_margin(Margin::same(4)) + .corner_radius( + ui.visuals().noninteractive().corner_radius, + ) + .fill(ui.visuals().widgets.inactive.bg_fill) + .stroke(Stroke::new(1., Color32::TRANSPARENT)) + .show(ui, |ui| { + ui.add_sized( + Vec2::new(ui.available_width(), 0.0), + DragValue::new(&mut self.timing_ms) + .speed(1) + .range(1..=10000) + .fixed_decimals(0) + .update_while_editing(false) + .suffix(" [ms]"), + ); + }); + }); + strip.cell(|ui| { + timing_btn(ui); + }); + }); + }); }); - } - })(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); - } + }); + + // ui.horizontal(|ui| { + // let wiggle_btn_response = btn_ui(&self.state, WIGGLE_KEY, |ui| { + // shortcut_ui(ui, &WIGGLE_KEY).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| { + // shortcut_ui(ui, &APERTURE_KEY).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, ValveViewState::ApertureFocused) { + // ui.ctx().memory_mut(|m| { + // m.request_focus(drag_value_id); + // }); + // } + // })(ui); + + // let timing_btn_response = btn_ui(&self.state, TIMING_KEY, |ui| { + // shortcut_ui(ui, &TIMING_KEY).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, ValveViewState::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); + // } + // }); } } @@ -190,11 +500,11 @@ impl ValveControlWindow { match action { // If the action close is called, close the window Some(WindowAction::CloseWindow) => { - self.state = ValveWindowState::Closed; + self.state = ValveViewState::Closed; None } Some(WindowAction::LooseFocus) => { - self.state = ValveWindowState::Open; + self.state = ValveViewState::Open; None } Some(WindowAction::Wiggle) => { @@ -206,7 +516,7 @@ impl ValveControlWindow { "Issued command to set timing for valve {:?} to {} ms", self.valve, self.timing_ms ); - self.state = ValveWindowState::Open; + self.state = ValveViewState::Open; Some(Command::set_atomic_valve_timing(self.valve, self.timing_ms)) } Some(WindowAction::SetAperture) => { @@ -214,18 +524,18 @@ impl ValveControlWindow { "Issued command to set aperture for valve {:?} to {}%", self.valve, self.aperture_perc ); - self.state = ValveWindowState::Open; + self.state = ValveViewState::Open; Some(Command::set_valve_maximum_aperture( self.valve, self.aperture_perc / 100., )) } Some(WindowAction::FocusOnTiming) => { - self.state = ValveWindowState::TimingFocused; + self.state = ValveViewState::TimingFocused; None } Some(WindowAction::FocusOnAperture) => { - self.state = ValveWindowState::ApertureFocused; + self.state = ValveViewState::ApertureFocused; None } _ => None, @@ -233,49 +543,49 @@ impl ValveControlWindow { } } -impl ValveControlWindow { +impl ValveControlView { #[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 => { + ValveViewState::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, 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 => { + ValveViewState::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 => { + ValveViewState::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 => {} + ValveViewState::Closed => {} } shortcut_handler.consume_if_mode_is(ShortcutMode::valve_control(), &key_action_pairs[..]) } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum ValveWindowState { +enum ValveViewState { Closed, Open, TimingFocused, ApertureFocused, } -impl ValveWindowState { +impl ValveViewState { #[inline] fn is_open(&self) -> bool { matches!(self, Self::Open) -- GitLab From 8f1c41825268608a7f96b17355a7abbc368868dd Mon Sep 17 00:00:00 2001 From: Federico Lolli <federico.lolli@skywarder.eu> Date: Fri, 11 Apr 2025 17:15:24 +0200 Subject: [PATCH 23/24] CHECKPOINT --- src/ui/panes/valve_control.rs | 10 +- .../valve_control/ui/valve_control_window.rs | 488 +++++++++--------- 2 files changed, 250 insertions(+), 248 deletions(-) diff --git a/src/ui/panes/valve_control.rs b/src/ui/panes/valve_control.rs index ed6743b..22a86e6 100644 --- a/src/ui/panes/valve_control.rs +++ b/src/ui/panes/valve_control.rs @@ -144,7 +144,10 @@ impl PaneBehavior for ValveControlPane { match action { // Open the valve control window if the action is to open it Some(PaneAction::OpenValveControl(valve)) => { - self.valve_view.replace(ValveControlView::new(valve)); + self.valve_view.replace(ValveControlView::new( + valve, + ui.id().with(valve.to_string()), + )); } None => {} } @@ -241,7 +244,10 @@ impl ValveControlPane { if response.clicked() { info!("Clicked on valve: {:?}", valve); - self.valve_view = Some(ValveControlView::new(valve)); + self.valve_view = Some(ValveControlView::new( + valve, + ui.id().with(valve.to_string()), + )); } } ui.end_row(); diff --git a/src/ui/panes/valve_control/ui/valve_control_window.rs b/src/ui/panes/valve_control/ui/valve_control_window.rs index 5fa463d..470b6a2 100644 --- a/src/ui/panes/valve_control/ui/valve_control_window.rs +++ b/src/ui/panes/valve_control/ui/valve_control_window.rs @@ -1,8 +1,8 @@ use egui::{ - Align, Button, Color32, Direction, DragValue, FontId, Frame, Grid, Key, Label, Layout, Margin, - Modifiers, Response, RichText, Sense, Stroke, TextEdit, Ui, UiBuilder, Vec2, Widget, + Align, Color32, Context, Direction, DragValue, Frame, Id, Key, Label, Layout, Margin, + Modifiers, Response, RichText, Sense, Stroke, Ui, UiBuilder, Vec2, Widget, }; -use egui_extras::{Size, Strip, StripBuilder}; +use egui_extras::{Size, StripBuilder}; use tracing::info; use crate::ui::shortcuts::{ShortcutHandler, ShortcutMode}; @@ -26,15 +26,17 @@ pub struct ValveControlView { state: ValveViewState, timing_ms: u32, aperture_perc: f32, + id: Id, } impl ValveControlView { - pub fn new(valve: Valve) -> ValveControlView { + pub fn new(valve: Valve, id: Id) -> ValveControlView { ValveControlView { valve, state: ValveViewState::Open, timing_ms: 0, aperture_perc: 0.0, + id, } } @@ -56,57 +58,16 @@ impl ValveControlView { ui.scope(self.draw_view_ui(&mut action)); // Handle the actions - self.handle_actions(action) + self.handle_actions(action, ui.ctx()) } + // DISCLAIMER: the code for the UI is really ugly, still learning how to use + // egui and in a hurry due to deadlines. If you know how to do it better + // feel free to help us fn draw_view_ui(&mut self, action: &mut Option<WindowAction>) -> impl FnOnce(&mut Ui) { |ui: &mut Ui| { - let icon_size = Vec2::splat(20.); - let text_size = 14.; - - fn btn_ui<R>( - window_state: &ValveViewState, - key: Key, - add_contents: impl FnOnce(&mut Ui) -> R, - ) -> impl FnOnce(&mut Ui) -> Response { - move |ui| { - let btn = Frame::canvas(ui.style()) - .inner_margin(Margin::same(4)) - .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) - }; - - btn.fill(fill_color) - .stroke(stroke) - .stroke(stroke) - .show(ui, |ui| { - ui.set_width(ui.available_width()); - ui.horizontal(|ui| add_contents(ui)) - }); - - if response.clicked() { - info!("Clicked!"); - } - }) - .response - } - } + let aperture_field_focus = self.id.with("aperture_field_focus"); + let timing_field_focus = self.id.with("timing_field_focus"); let valid_fill = ui .visuals() @@ -121,22 +82,26 @@ impl ValveControlView { .bg_fill .lerp_to_gamma(Color32::RED, 0.3); - fn shortcut_ui(ui: &Ui, key: &Key) -> ShortcutCard { + fn shortcut_ui(ui: &Ui, key: &Key, upper_response: &Response) -> ShortcutCard { let vis = ui.visuals(); + let uvis = ui.style().interact(upper_response); ShortcutCard::new(map_key_to_shortcut(*key)) .text_color(vis.strong_text_color()) - .fill_color(vis.gray_out(vis.widgets.inactive.bg_fill)) + .fill_color(vis.gray_out(uvis.bg_fill)) .margin(Margin::symmetric(5, 2)) .text_size(12.) } fn add_parameter_btn(ui: &mut Ui, key: Key) -> Response { ui.scope_builder(UiBuilder::new().id_salt(key).sense(Sense::click()), |ui| { + let visuals = *ui.style().interact(&ui.response()); + let shortcut_card = shortcut_ui(ui, &key, &ui.response()); + Frame::canvas(ui.style()) .inner_margin(Margin::symmetric(4, 2)) .outer_margin(0) .corner_radius(ui.visuals().noninteractive().corner_radius) - .fill(ui.visuals().widgets.inactive.bg_fill) + .fill(visuals.bg_fill) .stroke(Stroke::new(1., Color32::TRANSPARENT)) .show(ui, |ui| { ui.set_height(ui.available_height()); @@ -144,13 +109,11 @@ impl ValveControlView { ui.set_height(21.); ui.add_space(1.); Label::new( - RichText::new("SET") - .size(16.) - .color(ui.visuals().widgets.inactive.text_color()), + RichText::new("SET").size(16.).color(visuals.text_color()), ) .selectable(false) .ui(ui); - shortcut_ui(ui, &key).ui(ui); + shortcut_card.ui(ui); }); }); }) @@ -158,91 +121,117 @@ impl ValveControlView { } // set aperture and timing buttons - let aperture_btn: Box<dyn FnOnce(&mut Ui) -> Response> = match self.state { - ValveViewState::Open => Box::new(|ui| add_parameter_btn(ui, FOCUS_APERTURE_KEY)), - ValveViewState::ApertureFocused => { - Box::new(|ui| add_parameter_btn(ui, SET_PAR_KEY)) - } - ValveViewState::TimingFocused | ValveViewState::Closed => { - Box::new(|ui| ui.response()) + fn show_aperture_btn( + state: &ValveViewState, + action: &mut Option<WindowAction>, + ui: &mut Ui, + ) -> Response { + let res = match state { + ValveViewState::Open => Some(add_parameter_btn(ui, FOCUS_APERTURE_KEY)), + ValveViewState::ApertureFocused => Some(add_parameter_btn(ui, SET_PAR_KEY)), + ValveViewState::TimingFocused | ValveViewState::Closed => None, + }; + if let Some(res) = &res { + if res.clicked() { + // set the focus on the aperture field + action.replace(WindowAction::SetAperture); + } } - }; + res.unwrap_or_else(|| ui.response()) + } // set timing button - let timing_btn: Box<dyn FnOnce(&mut Ui) -> Response> = match self.state { - ValveViewState::Open => Box::new(|ui| add_parameter_btn(ui, FOCUS_TIMING_KEY)), - ValveViewState::TimingFocused => Box::new(|ui| add_parameter_btn(ui, SET_PAR_KEY)), - ValveViewState::ApertureFocused | ValveViewState::Closed => { - Box::new(|ui| ui.response()) + fn show_timing_btn( + state: &ValveViewState, + action: &mut Option<WindowAction>, + ui: &mut Ui, + ) -> Response { + let res = match state { + ValveViewState::Open => Some(add_parameter_btn(ui, FOCUS_TIMING_KEY)), + ValveViewState::TimingFocused => Some(add_parameter_btn(ui, SET_PAR_KEY)), + ValveViewState::ApertureFocused | ValveViewState::Closed => None, + }; + if let Some(res) = &res { + if res.clicked() { + // set the focus on the aperture field + action.replace(WindowAction::SetTiming); + } } - }; + res.unwrap_or_else(|| ui.response()) + } // wiggle button with shortcut - let wiggle_btn = |ui: &mut Ui| { - ui.scope_builder( - UiBuilder::new().id_salt(WIGGLE_KEY).sense(Sense::click()), - |ui| { - Frame::canvas(ui.style()) - .inner_margin(Margin::symmetric(4, 2)) - .outer_margin(0) - .corner_radius(ui.visuals().noninteractive().corner_radius) - .fill(ui.visuals().widgets.inactive.bg_fill) - .stroke(Stroke::new(1., Color32::TRANSPARENT)) - .show(ui, |ui| { - ui.set_height(ui.available_height()); - ui.horizontal_centered(|ui| { - ui.set_height(21.); - ui.add_space(1.); - Label::new( - RichText::new("WIGGLE") - .size(16.) - .color(ui.visuals().widgets.inactive.text_color()), - ) - .selectable(false) - .ui(ui); - ui.add( - Icon::Wiggle - .as_image(ui.ctx().theme()) - .fit_to_exact_size(Vec2::splat(22.)), - ); - shortcut_ui(ui, &WIGGLE_KEY).ui(ui); + fn wiggle_btn(ui: &mut Ui, action: &mut Option<WindowAction>) { + let res = ui + .scope_builder( + UiBuilder::new().id_salt(WIGGLE_KEY).sense(Sense::click()), + |ui| { + let visuals = *ui.style().interact(&ui.response()); + let shortcut_card = shortcut_ui(ui, &WIGGLE_KEY, &ui.response()); + + Frame::canvas(ui.style()) + .inner_margin(Margin::symmetric(4, 2)) + .outer_margin(0) + .corner_radius(ui.visuals().noninteractive().corner_radius) + .fill(visuals.bg_fill) + .stroke(Stroke::new(1., Color32::TRANSPARENT)) + .show(ui, |ui| { + ui.set_height(ui.available_height()); + ui.horizontal_centered(|ui| { + ui.set_height(21.); + ui.add_space(1.); + Label::new( + RichText::new("WIGGLE") + .size(16.) + .color(visuals.text_color()), + ) + .selectable(false) + .ui(ui); + ui.add( + Icon::Wiggle + .as_image(ui.ctx().theme()) + .fit_to_exact_size(Vec2::splat(22.)), + ); + shortcut_card.ui(ui); + }); }); - }); - }, - ); + }, + ) + .response; + + if res.clicked() { + // set the focus on the aperture field + action.replace(WindowAction::Wiggle); + } + } + + // valve header + let valve_header = |ui: &mut Ui| { + ui.with_layout(Layout::right_to_left(Align::Min), |ui| { + Label::new( + RichText::new(self.valve.to_string().to_uppercase()) + .color(ui.visuals().strong_text_color()) + .size(16.), + ) + .ui(ui); + Label::new(RichText::new("VALVE: ").size(16.)) + .selectable(false) + .ui(ui); + }); }; ui.with_layout(Layout::centered_and_justified(Direction::TopDown), |ui| { - ui.set_max_width(300.); + ui.set_max_width(350.); ui.set_min_height(50.); StripBuilder::new(ui) .size(Size::exact(5.)) .sizes(Size::initial(5.), 3) .vertical(|mut strip| { strip.empty(); - // strip.cell(|ui| { - // // ui.add_sized( - // // Vec2::new(ui.available_width(), 0.0), - // // Button::new("Wiggle"), - // // ); - // let wiggle_btn_response = btn_ui(&self.state, WIGGLE_KEY, |ui| { - // shortcut_ui(ui, &WIGGLE_KEY).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); - // }); strip.strip(|builder| { builder - // .size(Size::exact(230.)) - .size(Size::initial(10.)) - .size(Size::exact(25.)) + .size(Size::exact(206.)) + .size(Size::initial(50.)) .horizontal(|mut strip| { strip.strip(|builder| { builder @@ -251,35 +240,11 @@ impl ValveControlView { .size(Size::remainder()) .vertical(|mut strip| { strip.empty(); - strip.cell(|ui| { - ui.with_layout( - Layout::right_to_left(Align::Min), - |ui| { - Label::new( - RichText::new( - self.valve - .to_string() - .to_uppercase(), - ) - .color( - ui.visuals() - .strong_text_color(), - ) - .size(16.), - ) - .ui(ui); - Label::new( - RichText::new("VALVE: ").size(16.), - ) - .selectable(false) - .ui(ui); - }, - ); - }); + strip.cell(valve_header); strip.empty(); }); }); - strip.cell(wiggle_btn); + strip.cell(|ui| wiggle_btn(ui, action)); }); }); strip.strip(|builder| { @@ -335,22 +300,66 @@ impl ValveControlView { .fill(ui.visuals().widgets.inactive.bg_fill) .stroke(Stroke::new(1., Color32::TRANSPARENT)) .show(ui, |ui| { + // caveat used to clear the field and fill with the current value + if let Some(WindowAction::SetAperture) = + action.as_ref() + { + ui.ctx().input_mut(|input| { + input.events.push(egui::Event::Key { + key: Key::A, + physical_key: None, + pressed: true, + repeat: false, + modifiers: Modifiers::COMMAND, + }); + input.events.push(egui::Event::Text( + self.aperture_perc.to_string(), + )); + input.events.push(egui::Event::Key { + key: Key::A, + physical_key: None, + pressed: true, + repeat: false, + modifiers: Modifiers::COMMAND, + }); + }); + } + let res = ui.add_sized( Vec2::new(ui.available_width(), 0.0), DragValue::new(&mut self.aperture_perc) .speed(0.5) .range(0.0..=100.0) .fixed_decimals(0) - .update_while_editing(false) + .update_while_editing(true) .suffix("%"), ); + + let command_focus = ui.ctx().memory(|m| { + m.data.get_temp(aperture_field_focus) + }); + + // needed for making sure the state changes even + // if the pointer clicks inside the field if res.gained_focus() { - self.state = ValveViewState::ApertureFocused; + action.replace(WindowAction::FocusOnAperture); + } else if res.lost_focus() { + action.replace(WindowAction::LooseFocus); + } + + match (command_focus, res.has_focus()) { + (Some(true), false) => { + res.request_focus(); + } + (Some(false), true) => { + res.surrender_focus(); + } + _ => {} } }); }); strip.cell(|ui| { - aperture_btn(ui); + show_aperture_btn(&self.state, action, ui); }); }); }); @@ -404,99 +413,75 @@ impl ValveControlView { .fill(ui.visuals().widgets.inactive.bg_fill) .stroke(Stroke::new(1., Color32::TRANSPARENT)) .show(ui, |ui| { - ui.add_sized( + // caveat used to clear the field and fill with the current value + if let Some(WindowAction::SetTiming) = + action.as_ref() + { + ui.ctx().input_mut(|input| { + input.events.push(egui::Event::Key { + key: Key::A, + physical_key: None, + pressed: true, + repeat: false, + modifiers: Modifiers::COMMAND, + }); + input.events.push(egui::Event::Text( + self.timing_ms.to_string(), + )); + input.events.push(egui::Event::Key { + key: Key::A, + physical_key: None, + pressed: true, + repeat: false, + modifiers: Modifiers::COMMAND, + }); + }); + } + + let res = ui.add_sized( Vec2::new(ui.available_width(), 0.0), DragValue::new(&mut self.timing_ms) .speed(1) .range(1..=10000) .fixed_decimals(0) - .update_while_editing(false) + .update_while_editing(true) .suffix(" [ms]"), ); + + let command_focus = ui.ctx().memory(|m| { + m.data.get_temp(timing_field_focus) + }); + + // needed for making sure the state changes even + // if the pointer clicks inside the field + if res.gained_focus() { + action.replace(WindowAction::FocusOnTiming); + } else if res.lost_focus() { + action.replace(WindowAction::LooseFocus); + } + + match (command_focus, res.has_focus()) { + (Some(true), false) => { + res.request_focus(); + } + (Some(false), true) => { + res.surrender_focus(); + } + _ => {} + } }); }); strip.cell(|ui| { - timing_btn(ui); + show_timing_btn(&self.state, action, ui); }); }); }); }); }); - - // ui.horizontal(|ui| { - // let wiggle_btn_response = btn_ui(&self.state, WIGGLE_KEY, |ui| { - // shortcut_ui(ui, &WIGGLE_KEY).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| { - // shortcut_ui(ui, &APERTURE_KEY).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, ValveViewState::ApertureFocused) { - // ui.ctx().memory_mut(|m| { - // m.request_focus(drag_value_id); - // }); - // } - // })(ui); - - // let timing_btn_response = btn_ui(&self.state, TIMING_KEY, |ui| { - // shortcut_ui(ui, &TIMING_KEY).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, ValveViewState::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> { + fn handle_actions(&mut self, action: Option<WindowAction>, ctx: &Context) -> Option<Command> { match action { // If the action close is called, close the window Some(WindowAction::CloseWindow) => { @@ -505,6 +490,12 @@ impl ValveControlView { } Some(WindowAction::LooseFocus) => { self.state = ValveViewState::Open; + let aperture_field_focus = self.id.with("aperture_field_focus"); + let timing_field_focus = self.id.with("timing_field_focus"); + ctx.memory_mut(|m| { + m.data.insert_temp(aperture_field_focus, false); + m.data.insert_temp(timing_field_focus, false); + }); None } Some(WindowAction::Wiggle) => { @@ -516,7 +507,7 @@ impl ValveControlView { "Issued command to set timing for valve {:?} to {} ms", self.valve, self.timing_ms ); - self.state = ValveViewState::Open; + self.handle_actions(Some(WindowAction::LooseFocus), ctx); Some(Command::set_atomic_valve_timing(self.valve, self.timing_ms)) } Some(WindowAction::SetAperture) => { @@ -524,7 +515,7 @@ impl ValveControlView { "Issued command to set aperture for valve {:?} to {}%", self.valve, self.aperture_perc ); - self.state = ValveViewState::Open; + self.handle_actions(Some(WindowAction::LooseFocus), ctx); Some(Command::set_valve_maximum_aperture( self.valve, self.aperture_perc / 100., @@ -532,10 +523,18 @@ impl ValveControlView { } Some(WindowAction::FocusOnTiming) => { self.state = ValveViewState::TimingFocused; + let timing_field_focus = self.id.with("timing_field_focus"); + ctx.memory_mut(|m| { + m.data.insert_temp(timing_field_focus, true); + }); None } Some(WindowAction::FocusOnAperture) => { self.state = ValveViewState::ApertureFocused; + let aperture_field_focus = self.id.with("aperture_field_focus"); + ctx.memory_mut(|m| { + m.data.insert_temp(aperture_field_focus, true); + }); None } _ => None, @@ -553,22 +552,26 @@ impl ValveControlView { ValveViewState::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, + FOCUS_TIMING_KEY, + WindowAction::FocusOnTiming, + )); + key_action_pairs.push(( + Modifiers::NONE, + FOCUS_APERTURE_KEY, + WindowAction::FocusOnAperture, + )); key_action_pairs.push((Modifiers::NONE, Key::Escape, WindowAction::CloseWindow)); } ValveViewState::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, SET_PAR_KEY, WindowAction::SetTiming)); key_action_pairs.push((Modifiers::NONE, Key::Escape, WindowAction::LooseFocus)); } ValveViewState::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, SET_PAR_KEY, WindowAction::SetAperture)); key_action_pairs.push((Modifiers::NONE, Key::Escape, WindowAction::LooseFocus)); } ValveViewState::Closed => {} @@ -585,13 +588,6 @@ enum ValveViewState { ApertureFocused, } -impl ValveViewState { - #[inline] - fn is_open(&self) -> bool { - matches!(self, Self::Open) - } -} - #[derive(Debug, Clone, Copy)] enum WindowAction { // window actions -- GitLab From 0c10a034214cf8df3e90950f9aef78e29d3f2af5 Mon Sep 17 00:00:00 2001 From: Federico Lolli <federico.lolli@skywarder.eu> Date: Fri, 11 Apr 2025 18:22:01 +0200 Subject: [PATCH 24/24] fixed button clicking with keyboard shortcut --- .../valve_control/ui/valve_control_window.rs | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/src/ui/panes/valve_control/ui/valve_control_window.rs b/src/ui/panes/valve_control/ui/valve_control_window.rs index 470b6a2..c1c71f0 100644 --- a/src/ui/panes/valve_control/ui/valve_control_window.rs +++ b/src/ui/panes/valve_control/ui/valve_control_window.rs @@ -92,9 +92,15 @@ impl ValveControlView { .text_size(12.) } - fn add_parameter_btn(ui: &mut Ui, key: Key) -> Response { + fn add_parameter_btn(ui: &mut Ui, key: Key, action_override: bool) -> Response { ui.scope_builder(UiBuilder::new().id_salt(key).sense(Sense::click()), |ui| { - let visuals = *ui.style().interact(&ui.response()); + let mut visuals = *ui.style().interact(&ui.response()); + + // override the visuals if the button is pressed + if action_override { + visuals = ui.visuals().widgets.active; + } + let shortcut_card = shortcut_ui(ui, &key, &ui.response()); Frame::canvas(ui.style()) @@ -127,8 +133,12 @@ impl ValveControlView { ui: &mut Ui, ) -> Response { let res = match state { - ValveViewState::Open => Some(add_parameter_btn(ui, FOCUS_APERTURE_KEY)), - ValveViewState::ApertureFocused => Some(add_parameter_btn(ui, SET_PAR_KEY)), + ValveViewState::Open => Some(add_parameter_btn(ui, FOCUS_APERTURE_KEY, false)), + ValveViewState::ApertureFocused => Some(add_parameter_btn( + ui, + SET_PAR_KEY, + action.is_some_and(|a| a == WindowAction::SetAperture), + )), ValveViewState::TimingFocused | ValveViewState::Closed => None, }; if let Some(res) = &res { @@ -147,8 +157,12 @@ impl ValveControlView { ui: &mut Ui, ) -> Response { let res = match state { - ValveViewState::Open => Some(add_parameter_btn(ui, FOCUS_TIMING_KEY)), - ValveViewState::TimingFocused => Some(add_parameter_btn(ui, SET_PAR_KEY)), + ValveViewState::Open => Some(add_parameter_btn(ui, FOCUS_TIMING_KEY, false)), + ValveViewState::TimingFocused => Some(add_parameter_btn( + ui, + SET_PAR_KEY, + action.is_some_and(|a| a == WindowAction::SetTiming), + )), ValveViewState::ApertureFocused | ValveViewState::Closed => None, }; if let Some(res) = &res { @@ -166,7 +180,13 @@ impl ValveControlView { .scope_builder( UiBuilder::new().id_salt(WIGGLE_KEY).sense(Sense::click()), |ui| { - let visuals = *ui.style().interact(&ui.response()); + let mut visuals = *ui.style().interact(&ui.response()); + + // override the visuals if the button is pressed + if let Some(WindowAction::Wiggle) = action.as_ref() { + visuals = ui.visuals().widgets.active; + } + let shortcut_card = shortcut_ui(ui, &WIGGLE_KEY, &ui.response()); Frame::canvas(ui.style()) @@ -588,7 +608,7 @@ enum ValveViewState { ApertureFocused, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] enum WindowAction { // window actions CloseWindow, -- GitLab