From 472f5442c1b4e120b58af54b99a7544f9aa3071b Mon Sep 17 00:00:00 2001 From: Federico Lolli <federico.lolli@skywarder.eu> Date: Sun, 13 Apr 2025 10:40:48 +0000 Subject: [PATCH] Implemented a first draft of the new Valve-control-pane --- Cargo.lock | 12 +- Cargo.toml | 1 + icons/valve_control/dark/aperture.svg | 7 + icons/valve_control/dark/timing.svg | 6 + icons/valve_control/dark/wiggle.svg | 45 ++ icons/valve_control/light/aperture.svg | 7 + icons/valve_control/light/timing.svg | 6 + icons/valve_control/light/wiggle.svg | 45 ++ src/mavlink.rs | 4 + src/message_broker.rs | 19 +- src/message_broker/message_bundle.rs | 36 +- src/ui/app.rs | 93 ++- src/ui/panes.rs | 37 +- src/ui/panes/default.rs | 31 +- src/ui/panes/messages_viewer.rs | 24 +- src/ui/panes/pid_drawing_tool.rs | 25 +- src/ui/panes/plot.rs | 22 +- src/ui/panes/plot/source_window.rs | 1 + src/ui/panes/valve_control.rs | 473 +++++++++++++ src/ui/panes/valve_control/commands.rs | 203 ++++++ src/ui/panes/valve_control/icons.rs | 70 ++ src/ui/panes/valve_control/ui.rs | 14 + .../panes/valve_control/ui/shortcut_widget.rs | 73 ++ .../valve_control/ui/valve_control_window.rs | 623 ++++++++++++++++++ src/ui/panes/valve_control/valves.rs | 149 +++++ src/ui/shortcuts.rs | 169 ++++- src/ui/utils.rs | 10 +- src/ui/widget_gallery.rs | 4 +- 28 files changed, 2041 insertions(+), 168 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/dark/wiggle.svg create mode 100644 icons/valve_control/light/aperture.svg create mode 100644 icons/valve_control/light/timing.svg create mode 100644 icons/valve_control/light/wiggle.svg create mode 100644 src/ui/panes/valve_control.rs create mode 100644 src/ui/panes/valve_control/commands.rs create mode 100644 src/ui/panes/valve_control/icons.rs create mode 100644 src/ui/panes/valve_control/ui.rs 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 create mode 100644 src/ui/panes/valve_control/valves.rs 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/icons/valve_control/dark/aperture.svg b/icons/valve_control/dark/aperture.svg new file mode 100644 index 0000000..55195e2 --- /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="#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" /> +</svg> diff --git a/icons/valve_control/dark/timing.svg b/icons/valve_control/dark/timing.svg new file mode 100644 index 0000000..a9d7e36 --- /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="#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/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/aperture.svg b/icons/valve_control/light/aperture.svg new file mode 100644 index 0000000..df46696 --- /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="#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" /> +</svg> diff --git a/icons/valve_control/light/timing.svg b/icons/valve_control/light/timing.svg new file mode 100644 index 0000000..49bcce6 --- /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="#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/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/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..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, @@ -49,11 +53,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 @@ -63,21 +63,23 @@ 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(Some(hovered_tile)), - ), - ((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[..])); + 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 - 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 +134,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 +172,6 @@ impl eframe::App for App { self.maximized_pane = None; } } - _ => panic!("Unable to handle action"), } } @@ -225,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, maximized_pane, 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); @@ -281,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(), } @@ -318,14 +325,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()); } } @@ -400,9 +405,20 @@ impl AppState { } /// Behavior for the tree of panes in the app -#[derive(Default)] pub struct AppBehavior { - pub action: Option<PaneAction>, + 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 { @@ -412,13 +428,20 @@ impl Behavior<Pane> for AppBehavior { tile_id: TileId, pane: &mut Pane, ) -> egui_tiles::UiResponse { + let res = ui.scope(|ui| pane.ui(ui, self.shortcut_handler.lock().log_unwrap().deref_mut())); let PaneResponse { action_called, drag_response, - } = pane.ui(ui, tile_id); + } = 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(action_called); + self.action = Some((tile_id, action_called)); } drag_response } @@ -458,8 +481,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 977fbda..3fa948c 100644 --- a/src/ui/panes.rs +++ b/src/ui/panes.rs @@ -1,16 +1,17 @@ mod default; mod messages_viewer; mod pid_drawing_tool; -pub mod plot; +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}; 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 { @@ -26,18 +27,15 @@ 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; - - /// Whether the pane contains the pointer. - fn contains_pointer(&self) -> bool; + 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. - 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. @@ -52,20 +50,16 @@ 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 contains_pointer(&self) -> bool { - self.pane.contains_pointer() + fn ui(&mut self, ui: &mut Ui, shortcut_handler: &mut ShortcutHandler) -> PaneResponse { + self.pane.ui(ui, shortcut_handler) } - 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 { @@ -90,7 +84,10 @@ pub enum PaneKind { Plot2D(plot::Plot2DPane), #[strum(message = "Pid")] - PidOld(pid_drawing_tool::PidPane), + Pid(pid_drawing_tool::PidPane), + + #[strum(message = "Valve Control")] + ValveControl(valve_control::ValveControlPane), } impl Default for PaneKind { diff --git a/src/ui/panes/default.rs b/src/ui/panes/default.rs index c3f7979..dd2406f 100644 --- a/src/ui/panes/default.rs +++ b/src/ui/panes/default.rs @@ -1,10 +1,15 @@ use super::PaneBehavior; +use egui::Ui; 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}, + shortcuts::ShortcutHandler, + utils::{SizingMemo, vertically_centered}, + }, }; #[derive(Clone, Debug, Default, Serialize, Deserialize)] @@ -23,7 +28,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, _shortcut_handler: &mut ShortcutHandler) -> PaneResponse { let mut response = PaneResponse::default(); let parent = vertically_centered(ui, &mut self.centering_memo, |ui| { @@ -37,7 +42,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 @@ -45,25 +50,17 @@ 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(); }; response } - fn contains_pointer(&self) -> bool { - 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/messages_viewer.rs b/src/ui/panes/messages_viewer.rs index 2c8a54f..eb96466 100644 --- a/src/ui/panes/messages_viewer.rs +++ b/src/ui/panes/messages_viewer.rs @@ -1,35 +1,21 @@ -use egui::Label; +use egui::{Label, Ui}; use serde::{Deserialize, Serialize}; -use crate::ui::app::PaneResponse; +use crate::ui::{app::PaneResponse, shortcuts::ShortcutHandler}; 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 egui::Ui, _tile_id: egui_tiles::TileId) -> 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")); - self.contains_pointer = label.contains_pointer(); if label.drag_started() { response.set_drag_started(); } 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 f4e980e..9257cf4 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; @@ -20,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; @@ -80,7 +81,9 @@ 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, _shortcut_handler: &mut ShortcutHandler) -> PaneResponse { + let mut pane_response = PaneResponse::default(); + let theme = PidPane::find_theme(ui.ctx()); if self.center_content && !self.editable { @@ -132,14 +135,16 @@ impl PaneBehavior for PidPane { self.reset_subscriptions(); } - PaneResponse::default() - } + // Check if the user is draqging the pane + let ctrl_pressed = ui.input(|i| i.modifiers.ctrl); + if response.dragged() && (ctrl_pressed || !self.editable) { + pane_response.set_drag_started(); + } - fn contains_pointer(&self) -> bool { - false + pane_response } - 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); @@ -147,8 +152,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 4a98372..e4616d1 100644 --- a/src/ui/panes/plot.rs +++ b/src/ui/panes/plot.rs @@ -8,12 +8,11 @@ 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, 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::{ @@ -32,8 +31,6 @@ pub struct Plot2DPane { state_valid: bool, #[serde(skip)] settings_visible: bool, - #[serde(skip)] - pub contains_pointer: bool, } impl PartialEq for Plot2DPane { @@ -44,7 +41,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, _shortcut_handler: &mut ShortcutHandler) -> PaneResponse { let mut response = PaneResponse::default(); let data_settings_digest = self.settings.data_digest(); @@ -146,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(); } @@ -181,12 +177,8 @@ impl PaneBehavior for Plot2DPane { response } - fn contains_pointer(&self) -> bool { - self.contains_pointer - } - #[profiling::function] - fn update(&mut self, messages: &[TimedMessage]) { + fn update(&mut self, messages: &[&TimedMessage]) { if !self.state_valid { self.line_data.clear(); } @@ -226,8 +218,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 { @@ -235,7 +227,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/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 new file mode 100644 index 0000000..22a86e6 --- /dev/null +++ b/src/ui/panes/valve_control.rs @@ -0,0 +1,473 @@ +mod commands; +mod icons; +mod ui; +mod valves; + +use std::{ + collections::HashMap, + time::{Duration, Instant}, +}; + +use egui::{ + 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}; +use skyward_mavlink::{ + mavlink::MessageData, + orion::{ACK_TM_DATA, NACK_TM_DATA, WACK_TM_DATA}, +}; +use strum::IntoEnumIterator; +use tracing::info; + +use crate::{ + mavlink::{MavMessage, TimedMessage}, + ui::{ + app::PaneResponse, + shortcuts::{ShortcutHandler, ShortcutMode}, + }, +}; + +use super::PaneBehavior; + +use commands::CommandSM; +use icons::Icon; +use ui::{ShortcutCard, ValveControlView, map_key_to_shortcut}; +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 + #[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, + #[serde(skip)] + valve_key_map: HashMap<Valve, Key>, + #[serde(skip)] + valve_view: Option<ValveControlView>, +} + +impl Default for ValveControlPane { + fn default() -> Self { + let symbols: Vec<char> = SYMBOL_LIST.chars().collect(); + let valve_key_map = Valve::iter() + .zip(symbols.into_iter().map(map_symbol_to_key)) + .collect(); + Self { + valves_state: ValveStateManager::default(), + commands: vec![], + auto_refresh: None, + manual_refresh: false, + last_refresh: None, + is_settings_window_open: false, + valve_key_map, + valve_view: None, + } + } +} + +impl PaneBehavior for ValveControlPane { + #[profiling::function] + 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)); + + if let Some(valve_view) = &mut self.valve_view { + if let Some(command) = valve_view.ui(ui, shortcut_handler) { + self.commands.push(command.into()); + } + + 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(); + } + + // capture actions from keyboard shortcuts + let action = self.keyboard_actions(shortcut_handler); + + 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, + ui.id().with(valve.to_string()), + )); + } + 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 + } + + #[profiling::function] + 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()) + } + + #[profiling::function] + 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, Some(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(); + } + + #[profiling::function] + 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 │ +// └────────────────────────┘ +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)); + Grid::new("valves_grid") + .num_columns(n) + .spacing(Vec2::splat(5.)) + .show(ui, |ui| { + for chunk in &valve_chunks { + for (symbol, valve) in chunk { + 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_view = Some(ValveControlView::new( + valve, + ui.id().with(valve.to_string()), + )); + } + } + ui.end_row(); + } + }); + } + } + + 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(); + } + if ui.button("Settings").clicked() { + self.is_settings_window_open = true; + ui.close_menu(); + } + } + } + + 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| { + 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_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) + .prefix("Every ") + .suffix(" s") + .ui(ui); + *auto_refresh_duration = Duration::from_secs_f32(auto_refresh_period); + } else { + *auto_refresh_setting = None; + } + }); + } + } + + 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(); + 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!("{:.2}%", value * 100.) + } + valves::ParameterValue::Missing => "N/A".to_owned(), + valves::ParameterValue::Invalid(err_id) => { + format!("ERROR({})", err_id) + } + }; + 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); + }; + + let labels_ui = |ui: &mut Ui| { + let icon_size = Vec2::splat(17.); + 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.set_min_width(80.); + ui.horizontal_top(|ui| { + 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()); + let galley = ui.fonts(|fonts| fonts.layout_job(layout_job)); + Label::new(galley).selectable(false).ui(ui); + }); + }); + ui.horizontal_top(|ui| { + 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)); + Label::new(galley).selectable(false).ui(ui); + }); + }); + }; + + ui.scope_builder( + UiBuilder::new() + .id_salt("valve_".to_owned() + &valve_str) + .sense(Sense::click()), + |ui| { + let response = ui.response(); + 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_view.is_none() + { + 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| { + 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) + .inner_margin(ui.spacing().menu_margin) + .corner_radius(visuals.corner_radius) + .show(ui, inside_frame); + }, + ) + .response + } + } + + #[profiling::function] + fn keyboard_actions(&self, shortcut_handler: &mut ShortcutHandler) -> Option<PaneAction> { + let mut key_action_pairs = Vec::new(); + 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[..]) + } +} + +// ┌───────────────────────────┐ +// │ 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; + } +} + +#[derive(Debug, Clone, Copy)] +enum PaneAction { + OpenValveControl(Valve), +} diff --git a/src/ui/panes/valve_control/commands.rs b/src/ui/panes/valve_control/commands.rs new file mode 100644 index 0000000..ed4d282 --- /dev/null +++ b/src/ui/panes/valve_control/commands.rs @@ -0,0 +1,203 @@ +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, +}; + +use super::valves::{ParameterValue, Valve, ValveParameter}; + +#[derive(Debug, Clone, PartialEq)] +pub enum CommandSM { + Request(Command), + WaitingForResponse((Instant, Command)), + Response((Valve, Option<ValveParameter>)), + Consumed, +} + +impl CommandSM { + pub fn pack_and_wait(&mut self) -> Option<MavMessage> { + match self { + Self::Request(command) => { + let message = MavMessage::from(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 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 => { + *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, Option<ValveParameter>)> { + match self { + Self::Response((valve, parameter)) => { + let res = Some((*valve, parameter.clone())); + *self = CommandSM::Consumed; + res + } + _ => None, + } + } + + pub fn is_waiting_for_response(&self) -> bool { + matches!(self, Self::WaitingForResponse(_)) + } + + pub fn is_consumed(&self) -> bool { + matches!(self, Self::Consumed) + } +} + +impl From<Command> for CommandSM { + fn from(value: Command) -> Self { + Self::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 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) => { + Self::SET_ATOMIC_VALVE_TIMING_TC(SET_ATOMIC_VALVE_TIMING_TC_DATA { + servo_id: value.valve.into(), + maximum_timing: timing, + }) + } + CommandKind::SetValveMaximumAperture(aperture) => { + Self::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 { + Wiggle, + SetAtomicValveTiming(u32), + SetValveMaximumAperture(f32), +} + +impl CommandKind { + fn message_id(&self) -> u32 { + match self { + 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) -> Option<ValveParameter> { + (*self).try_into().ok() + } + + fn to_invalid_parameter(&self, error: u16) -> Option<ValveParameter> { + match self { + Self::Wiggle => None, + Self::SetAtomicValveTiming(_) => Some(ValveParameter::AtomicValveTiming( + ParameterValue::Invalid(error), + )), + Self::SetValveMaximumAperture(_) => Some(ValveParameter::ValveMaximumAperture( + ParameterValue::Invalid(error), + )), + } + } +} + +impl TryFrom<CommandKind> for ValveParameter { + type Error = (); + + fn try_from(value: CommandKind) -> Result<Self, Self::Error> { + match value { + CommandKind::Wiggle => Err(()), + CommandKind::SetAtomicValveTiming(timing) => { + Ok(Self::AtomicValveTiming(ParameterValue::Valid(timing))) + } + CommandKind::SetValveMaximumAperture(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 new file mode 100644 index 0000000..654acae --- /dev/null +++ b/src/ui/panes/valve_control/icons.rs @@ -0,0 +1,70 @@ +use egui::{Context, Image, ImageSource, SizeHint, TextureOptions, Theme}; +use strum::IntoEnumIterator; +use strum_macros::EnumIter; +use tracing::error; + +#[derive(Debug, Clone, Copy, EnumIter)] +pub enum Icon { + Wiggle, + Aperture, + Timing, +} + +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"), + "/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" + )) + } + } + } + + 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)) + } +} diff --git a/src/ui/panes/valve_control/ui.rs b/src/ui/panes/valve_control/ui.rs new file mode 100644 index 0000000..6350f61 --- /dev/null +++ b/src/ui/panes/valve_control/ui.rs @@ -0,0 +1,14 @@ +mod shortcut_widget; +mod valve_control_window; + +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::ValveControlView}; + +#[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..212d895 --- /dev/null +++ b/src/ui/panes/valve_control/ui/shortcut_widget.rs @@ -0,0 +1,73 @@ +use egui::{ + Color32, FontId, Frame, KeyboardShortcut, Label, Margin, ModifierNames, RichText, Stroke, + Widget, +}; + +pub struct ShortcutCard { + shortcut: KeyboardShortcut, + text_size: f32, + margin: Margin, + 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(self.margin) + .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., + margin: Margin::same(5), + 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 + } + + 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 new file mode 100644 index 0000000..c1c71f0 --- /dev/null +++ b/src/ui/panes/valve_control/ui/valve_control_window.rs @@ -0,0 +1,623 @@ +use egui::{ + Align, Color32, Context, Direction, DragValue, Frame, Id, Key, Label, Layout, Margin, + Modifiers, Response, RichText, Sense, Stroke, Ui, UiBuilder, Vec2, Widget, +}; +use egui_extras::{Size, StripBuilder}; +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; +/// 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 ValveControlView { + valve: Valve, + state: ValveViewState, + timing_ms: u32, + aperture_perc: f32, + id: Id, +} + +impl ValveControlView { + pub fn new(valve: Valve, id: Id) -> ValveControlView { + ValveControlView { + valve, + state: ValveViewState::Open, + timing_ms: 0, + aperture_perc: 0.0, + id, + } + } + + pub fn is_closed(&self) -> bool { + matches!(self.state, ValveViewState::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 view inside the pane + ui.scope(self.draw_view_ui(&mut action)); + + // Handle the actions + 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 aperture_field_focus = self.id.with("aperture_field_focus"); + let timing_field_focus = self.id.with("timing_field_focus"); + + 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, 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(uvis.bg_fill)) + .margin(Margin::symmetric(5, 2)) + .text_size(12.) + } + + 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 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()) + .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("SET").size(16.).color(visuals.text_color()), + ) + .selectable(false) + .ui(ui); + shortcut_card.ui(ui); + }); + }); + }) + .response + } + + // set aperture and timing buttons + 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, 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 { + if res.clicked() { + // set the focus on the aperture field + action.replace(WindowAction::SetAperture); + } + } + res.unwrap_or_else(|| ui.response()) + } + + // set timing button + 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, 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 { + if res.clicked() { + // set the focus on the aperture field + action.replace(WindowAction::SetTiming); + } + } + res.unwrap_or_else(|| ui.response()) + } + + // wiggle button with shortcut + 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 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()) + .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(350.); + ui.set_min_height(50.); + StripBuilder::new(ui) + .size(Size::exact(5.)) + .sizes(Size::initial(5.), 3) + .vertical(|mut strip| { + strip.empty(); + strip.strip(|builder| { + builder + .size(Size::exact(206.)) + .size(Size::initial(50.)) + .horizontal(|mut strip| { + strip.strip(|builder| { + builder + .size(Size::remainder()) + .size(Size::initial(5.)) + .size(Size::remainder()) + .vertical(|mut strip| { + strip.empty(); + strip.cell(valve_header); + strip.empty(); + }); + }); + strip.cell(|ui| wiggle_btn(ui, action)); + }); + }); + 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| { + // 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(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() { + 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| { + show_aperture_btn(&self.state, action, 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| { + // 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(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| { + show_timing_btn(&self.state, action, ui); + }); + }); + }); + }); + }); + } + } + + 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) => { + self.state = ValveViewState::Closed; + None + } + 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) => { + 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.handle_actions(Some(WindowAction::LooseFocus), ctx); + 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.handle_actions(Some(WindowAction::LooseFocus), ctx); + Some(Command::set_valve_maximum_aperture( + self.valve, + self.aperture_perc / 100., + )) + } + 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, + } + } +} + +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 { + 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, + 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, 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, SET_PAR_KEY, WindowAction::SetAperture)); + key_action_pairs.push((Modifiers::NONE, Key::Escape, WindowAction::LooseFocus)); + } + ValveViewState::Closed => {} + } + shortcut_handler.consume_if_mode_is(ShortcutMode::valve_control(), &key_action_pairs[..]) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ValveViewState { + Closed, + Open, + TimingFocused, + ApertureFocused, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum WindowAction { + // window actions + CloseWindow, + LooseFocus, + // commands + Wiggle, + SetTiming, + SetAperture, + // UI focus + FocusOnTiming, + FocusOnAperture, +} diff --git a/src/ui/panes/valve_control/valves.rs b/src/ui/panes/valve_control/valves.rs new file mode 100644 index 0000000..cf9b15a --- /dev/null +++ b/src/ui/panes/valve_control/valves.rs @@ -0,0 +1,149 @@ +//! 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, Hash)] +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, 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 { + 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 0c2ae3f..7d1b193 100644 --- a/src/ui/utils.rs +++ b/src/ui/utils.rs @@ -1,17 +1,19 @@ use egui::containers::Frame; use egui::{Response, Shadow, Stroke, Style, Ui}; -use egui_tiles::TileId; -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, tile_id: TileId, 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, tile_id)); + .show(ui, |ui| pane.ui(ui, shortcut_handler)); } #[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