diff --git a/Cargo.lock b/Cargo.lock index 95df11c4ccfc5f95e55e257648e49b9c23c2f546..79aa3aeef6726adfbcf333daadc020a6598d9489 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1596,6 +1596,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.4.0" @@ -2804,6 +2810,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + [[package]] name = "ryu" version = "1.0.18" @@ -2864,10 +2876,12 @@ dependencies = [ "serde_json", "skyward_mavlink", "strum", + "strum_macros", "thiserror 2.0.9", "tokio", "tracing", "tracing-subscriber", + "uuid", ] [[package]] @@ -3139,6 +3153,19 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.95", +] + [[package]] name = "syn" version = "1.0.109" @@ -3467,6 +3494,16 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b" +dependencies = [ + "getrandom", + "serde", +] + [[package]] name = "valuable" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 9b369adc9d499ce5afb1e682140090e9da747bdd..74d48c05a7f7710372ada9a6f21f9ed8979fc846 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,8 @@ crossbeam-channel = "0.5" enum_dispatch = "0.3" egui_extras = "0.29.1" strum = "0.26" +strum_macros = "0.26" anyhow = "1.0" ring-channel = "0.12.0" thiserror = "2.0.7" +uuid = { version = "1.12.1", features = ["serde", "v7"] } diff --git a/src/mavlink.rs b/src/mavlink.rs index ab38cb5b543273413110c9c704bdea49313022eb..7fe41e95c6537002e60e15f68618153c08dca5e0 100644 --- a/src/mavlink.rs +++ b/src/mavlink.rs @@ -11,7 +11,7 @@ mod reflection; // Export all the types from the base module as if they were defined in this module pub use base::*; pub use error::{MavlinkError, Result as MavlinkResult}; -pub use message_broker::{MessageBroker, MessageView}; +pub use message_broker::{MessageBroker, MessageView, ViewId}; pub use reflection::ReflectionContext; /// Default port for the Ethernet connection diff --git a/src/mavlink/message_broker.rs b/src/mavlink/message_broker.rs index 94c0c0e6673e851db0ff3b2ac637ceac889e6cfc..2f611c806d0691163b1d3a4ed724685408ff7dd1 100644 --- a/src/mavlink/message_broker.rs +++ b/src/mavlink/message_broker.rs @@ -15,10 +15,11 @@ use std::{ }; use anyhow::{Context, Result}; -use egui::{ahash::HashMapExt, IdMap}; use ring_channel::{ring_channel, RingReceiver, RingSender}; +use serde::{Deserialize, Serialize}; use tokio::{net::UdpSocket, task::JoinHandle}; use tracing::{debug, trace}; +use uuid::Uuid; use crate::mavlink::byte_parser; @@ -32,8 +33,8 @@ const UDP_BUFFER_SIZE: usize = 65527; /// This trait should be implemented by any view that wants to interact with the /// `MessageBroker` and get updates on the messages it is interested in. pub trait MessageView { - /// Returns the widget ID of the view - fn widget_id(&self) -> &egui::Id; + /// Returns an hashable value as widget identifier + fn view_id(&self) -> ViewId; /// Returns the message ID of interest for the view fn id_of_interest(&self) -> u32; /// Returns whether the view is cache valid or not, i.e. if it can be @@ -59,7 +60,7 @@ pub struct MessageBroker { /// map(message ID -> vector of messages received so far) messages: HashMap<u32, Vec<TimedMessage>>, /// map(widget ID -> queue of messages left for update) - update_queues: IdMap<(u32, VecDeque<TimedMessage>)>, + update_queues: HashMap<ViewId, (u32, VecDeque<TimedMessage>)>, // == Internal == /// Flag to stop the listener running_flag: Arc<AtomicBool>, @@ -79,7 +80,7 @@ impl MessageBroker { let (tx, rx) = ring_channel(channel_size); Self { messages: HashMap::new(), - update_queues: IdMap::new(), + update_queues: HashMap::new(), tx, rx, ctx, @@ -92,7 +93,7 @@ impl MessageBroker { /// validity based on `is_valid` method of the view. pub fn refresh_view<V: MessageView>(&mut self, view: &mut V) -> MavlinkResult<()> { self.process_incoming_msgs(); - if !view.is_valid() || !self.is_view_subscribed(view.widget_id()) { + if !view.is_valid() || !self.is_view_subscribed(view.view_id()) { self.init_view(view)?; } else { self.update_view(view)?; @@ -159,25 +160,25 @@ impl MessageBroker { self.messages.clear(); } - fn is_view_subscribed(&self, widget_id: &egui::Id) -> bool { - self.update_queues.contains_key(widget_id) + fn is_view_subscribed(&self, view_id: ViewId) -> bool { + self.update_queues.contains_key(&view_id) } /// Init a view in case of cache invalidation or first time initialization. fn init_view<V: MessageView>(&mut self, view: &mut V) -> MavlinkResult<()> { - trace!("initializing view: {:?}", view.widget_id()); + trace!("initializing view: {:?}", view.view_id()); if let Some(messages) = self.messages.get(&view.id_of_interest()) { view.populate_view(messages)?; } self.update_queues - .insert(*view.widget_id(), (view.id_of_interest(), VecDeque::new())); + .insert(view.view_id(), (view.id_of_interest(), VecDeque::new())); Ok(()) } /// Update a view with new messages, used when the cache is valid. fn update_view<V: MessageView>(&mut self, view: &mut V) -> MavlinkResult<()> { - trace!("updating view: {:?}", view.widget_id()); - if let Some((_, queue)) = self.update_queues.get_mut(view.widget_id()) { + trace!("updating view: {:?}", view.view_id()); + if let Some((_, queue)) = self.update_queues.get_mut(&view.view_id()) { while let Some(msg) = queue.pop_front() { view.update_view(&[msg])?; } @@ -210,3 +211,18 @@ impl MessageBroker { // TODO: Implement a scheduler removal of old messages (configurable, must not hurt performance) // TODO: Add a Dashmap if performance is a problem (Personally don't think it will be) } + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct ViewId(Uuid); + +impl ViewId { + pub fn new() -> Self { + Self(Uuid::now_v7()) + } +} + +impl Default for ViewId { + fn default() -> Self { + Self(Uuid::now_v7()) + } +} diff --git a/src/ui.rs b/src/ui.rs index f276231768c6a750ac31d90e1a79a1eb5f42b1b0..1f697f59edac6fa792b3644b8b6d7d157678a53e 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -3,5 +3,6 @@ mod panes; mod persistency; mod shortcuts; mod utils; +mod widget_gallery; pub use composable_view::ComposableView; diff --git a/src/ui/composable_view.rs b/src/ui/composable_view.rs index aedc7ffd8edfaaf3abb03eb1da24b76fcdad85bf..2d5ecd5e0d887f9e5441dabf00fa947cdd067236 100644 --- a/src/ui/composable_view.rs +++ b/src/ui/composable_view.rs @@ -5,6 +5,7 @@ use super::{ persistency::{LayoutManager, LayoutManagerWindow}, shortcuts, utils::maximized_pane_ui, + widget_gallery::WidgetGallery, }; use std::{ fs, @@ -21,6 +22,7 @@ pub struct ComposableView { /// Persistent state of the app state: ComposableViewState, layout_manager: LayoutManager, + widget_gallery: WidgetGallery, behavior: ComposableBehavior, maximized_pane: Option<TileId>, @@ -44,84 +46,95 @@ impl eframe::App for ComposableView { trace!("Hovered pane: {:?}", hovered_pane); // Capture any pane action generated by pane children - let pane_action = self.behavior.action.take(); - let mut pane_action = pane_action.zip(hovered_pane); + let mut pane_action = self.behavior.action.take(); trace!("Pane action: {:?}", pane_action); - // Capture any pane action generated by keyboard shortcuts - if let Some(hovered_pane) = hovered_pane { + 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), ]; - pane_action = pane_action.or(shortcuts::map_to_action(ctx, &key_action_pairs[..]) - .map(|action| (action, hovered_pane))); + pane_action = pane_action.or(shortcuts::map_to_action(ctx, &key_action_pairs[..])); } // If an action was triggered, we consume it - if let Some((action, hovered_tile)) = pane_action.take() { + if let Some(action) = pane_action.take() { match action { PaneAction::SplitH => { - if self.maximized_pane.is_none() { - debug!("Called SplitH on tile {:?}", hovered_tile); - let hovered_tile_pane = panes_tree - .tiles - .remove(hovered_tile) - .log_expect("Hovered tile not found"); - let left_pane = panes_tree.tiles.insert_new(hovered_tile_pane); - let right_pane = panes_tree.tiles.insert_pane(Pane::default()); - panes_tree.tiles.insert( - hovered_tile, - Tile::Container(Container::Linear(Linear::new_binary( - LinearDir::Horizontal, - [left_pane, right_pane], - 0.5, - ))), - ); + if let Some(hovered_tile) = hovered_pane { + if self.maximized_pane.is_none() { + debug!("Called SplitH on tile {:?}", hovered_tile); + let hovered_tile_pane = panes_tree + .tiles + .remove(hovered_tile) + .log_expect("Hovered tile not found"); + let left_pane = panes_tree.tiles.insert_new(hovered_tile_pane); + let right_pane = panes_tree.tiles.insert_pane(Pane::default()); + panes_tree.tiles.insert( + hovered_tile, + Tile::Container(Container::Linear(Linear::new_binary( + LinearDir::Horizontal, + [left_pane, right_pane], + 0.5, + ))), + ); + } } } PaneAction::SplitV => { if self.maximized_pane.is_none() { - debug!("Called SplitV on tile {:?}", hovered_tile); - let hovered_tile_pane = panes_tree - .tiles - .remove(hovered_tile) - .log_expect("Hovered tile not found"); - let replaced = panes_tree.tiles.insert_new(hovered_tile_pane); - let lower_pane = panes_tree.tiles.insert_pane(Pane::default()); - panes_tree.tiles.insert( - hovered_tile, - Tile::Container(Container::Linear(Linear::new_binary( - LinearDir::Vertical, - [replaced, lower_pane], - 0.5, - ))), - ); + if let Some(hovered_tile) = hovered_pane { + debug!("Called SplitV on tile {:?}", hovered_tile); + let hovered_tile_pane = panes_tree + .tiles + .remove(hovered_tile) + .log_expect("Hovered tile not found"); + let replaced = panes_tree.tiles.insert_new(hovered_tile_pane); + let lower_pane = panes_tree.tiles.insert_pane(Pane::default()); + panes_tree.tiles.insert( + hovered_tile, + Tile::Container(Container::Linear(Linear::new_binary( + LinearDir::Vertical, + [replaced, lower_pane], + 0.5, + ))), + ); + } } } PaneAction::Close => { - debug!("Called Close on tile {:?}", hovered_tile); - // Ignore if the root pane is the only one - if panes_tree.tiles.len() != 1 && self.maximized_pane.is_none() { - panes_tree.remove_recursively(hovered_tile); + if let Some(hovered_tile) = hovered_pane { + debug!("Called Close on tile {:?}", hovered_tile); + // Ignore if the root pane is the only one + if panes_tree.tiles.len() != 1 && self.maximized_pane.is_none() { + panes_tree.remove_recursively(hovered_tile); + } } } - PaneAction::Replace(new_pane) => { + PaneAction::Replace(tile_id, new_pane) => { debug!( "Called Replace on tile {:?} with pane {:?}", - hovered_tile, new_pane + tile_id, new_pane ); - panes_tree.tiles.insert(hovered_tile, Tile::Pane(*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::Maximize => { // This is a toggle: if there is not currently a maximized pane, // maximize the hovered pane, otherwize remove the maximized pane. if self.maximized_pane.is_some() { self.maximized_pane = None; - } else { + } else if let Some(hovered_tile) = hovered_pane { let hovered_pane_is_default = panes_tree .tiles .get(hovered_tile) @@ -144,6 +157,7 @@ impl eframe::App for ComposableView { self.maximized_pane = None; } } + _ => panic!("Unable to handle action"), } } @@ -175,7 +189,7 @@ impl eframe::App for ComposableView { 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, maximized_pane, pane); } else { panic!("Maximized pane not found in tree!"); } @@ -186,6 +200,10 @@ impl eframe::App for ComposableView { self.layout_manager_window .show(ctx, &mut self.layout_manager, &mut self.state); + if let Some(action) = self.widget_gallery.show(ctx) { + debug!("Widget gallery returned action {action:?}"); + self.behavior.action = Some(action); + } } fn save(&mut self, storage: &mut dyn eframe::Storage) { @@ -314,13 +332,13 @@ impl Behavior<Pane> for ComposableBehavior { fn pane_ui( &mut self, ui: &mut egui::Ui, - _tile_id: TileId, + tile_id: TileId, pane: &mut Pane, ) -> egui_tiles::UiResponse { let PaneResponse { action_called, drag_response, - } = pane.ui(ui); + } = pane.ui(ui, 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); @@ -363,7 +381,8 @@ pub enum PaneAction { SplitH, SplitV, Close, - Replace(Box<Pane>), + Replace(TileId, Box<Pane>), + ReplaceThroughGallery(Option<TileId>), Maximize, Exit, } diff --git a/src/ui/panes.rs b/src/ui/panes.rs index c0ae378ce1336e592e85f93ed9d5177c2c3ac0ce..2dbcdd6eca3ca8546be24365104b8d33caaed26f 100644 --- a/src/ui/panes.rs +++ b/src/ui/panes.rs @@ -1,9 +1,11 @@ mod default; mod messages_viewer; -mod plot; +pub mod plot; +use egui_tiles::TileId; use enum_dispatch::enum_dispatch; use serde::{Deserialize, Serialize}; +use strum_macros::{self, EnumIter, EnumMessage}; use super::composable_view::PaneResponse; @@ -20,13 +22,13 @@ impl Pane { #[enum_dispatch(PaneKind)] pub trait PaneBehavior { - fn ui(&mut self, ui: &mut egui::Ui) -> PaneResponse; + fn ui(&mut self, ui: &mut egui::Ui, tile_id: TileId) -> PaneResponse; fn contains_pointer(&self) -> bool; } impl PaneBehavior for Pane { - fn ui(&mut self, ui: &mut egui::Ui) -> PaneResponse { - self.pane.ui(ui) + fn ui(&mut self, ui: &mut egui::Ui, tile_id: TileId) -> PaneResponse { + self.pane.ui(ui, tile_id) } fn contains_pointer(&self) -> bool { @@ -35,11 +37,15 @@ impl PaneBehavior for Pane { } // An enum to represent the diffent kinds of widget available to the user. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, EnumMessage, EnumIter)] #[enum_dispatch] pub enum PaneKind { Default(default::DefaultPane), + + #[strum(message = "Messages Viewer")] MessagesViewer(messages_viewer::MessagesViewerPane), + + #[strum(message = "Plot 2D")] Plot2D(plot::Plot2DPane), } diff --git a/src/ui/panes/default.rs b/src/ui/panes/default.rs index f3963eddf750d0da9be94d8035609b4ae1a8abfe..7d58c23e195dcda97a925a694d9de33f605ef6d6 100644 --- a/src/ui/panes/default.rs +++ b/src/ui/panes/default.rs @@ -1,4 +1,4 @@ -use super::{plot::Plot2DPane, Pane, PaneBehavior, PaneKind}; +use super::PaneBehavior; use serde::{Deserialize, Serialize}; use tracing::debug; @@ -22,7 +22,7 @@ impl PartialEq for DefaultPane { } impl PaneBehavior for DefaultPane { - fn ui(&mut self, ui: &mut egui::Ui) -> PaneResponse { + fn ui(&mut self, ui: &mut egui::Ui, tile_id: egui_tiles::TileId) -> PaneResponse { let mut response = PaneResponse::default(); let parent = vertically_centered(ui, &mut self.centering_memo, |ui| { @@ -35,10 +35,8 @@ impl PaneBehavior for DefaultPane { response.set_action(PaneAction::SplitH); debug!("Horizontal Split button clicked"); } - if ui.button("Plot").clicked() { - response.set_action(PaneAction::Replace(Pane::boxed(PaneKind::Plot2D( - Plot2DPane::new(ui.auto_id_with("plot_2d")), - )))); + if ui.button("Widget Gallery").clicked() { + response.set_action(PaneAction::ReplaceThroughGallery(Some(tile_id))); } }) .response diff --git a/src/ui/panes/messages_viewer.rs b/src/ui/panes/messages_viewer.rs index af279bc80da8100c4a044116fe6477b3ed2ef9d9..02bcc9947b1f7bed483ca05d6e98e071ac4018dc 100644 --- a/src/ui/panes/messages_viewer.rs +++ b/src/ui/panes/messages_viewer.rs @@ -18,7 +18,7 @@ impl PartialEq for MessagesViewerPane { } impl PaneBehavior for MessagesViewerPane { - fn ui(&mut self, ui: &mut egui::Ui) -> PaneResponse { + fn ui(&mut self, ui: &mut egui::Ui, _tile_id: egui_tiles::TileId) -> 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/plot.rs b/src/ui/panes/plot.rs index 446e0752efeae32ac6634a0c7a8be12bd849153b..e5279043de9b330ff2b36346ccaf581ffa183b12 100644 --- a/src/ui/panes/plot.rs +++ b/src/ui/panes/plot.rs @@ -2,13 +2,14 @@ mod source_window; use egui::{Color32, Vec2b}; use egui_plot::{Legend, Line, PlotPoints}; +use egui_tiles::TileId; use serde::{Deserialize, Serialize}; use source_window::{sources_window, SourceSettings}; use crate::{ error::ErrInstrument, mavlink::{ - extract_from_message, MavlinkResult, MessageData, MessageView, TimedMessage, + extract_from_message, MavlinkResult, MessageData, MessageView, TimedMessage, ViewId, ROCKET_FLIGHT_TM_DATA, }, msg_broker, @@ -17,7 +18,7 @@ use crate::{ use super::PaneBehavior; -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Default, Debug, Serialize, Deserialize)] pub struct Plot2DPane { // UI settings #[serde(skip)] @@ -29,18 +30,6 @@ pub struct Plot2DPane { view: PlotMessageView, } -impl Plot2DPane { - pub fn new(id: egui::Id) -> Self { - Self { - contains_pointer: false, - settings_visible: false, - line_settings: vec![], - plot_active: false, - view: PlotMessageView::new(id), - } - } -} - impl PartialEq for Plot2DPane { fn eq(&self, other: &Self) -> bool { self.view.settings == other.view.settings @@ -50,7 +39,7 @@ impl PartialEq for Plot2DPane { } impl PaneBehavior for Plot2DPane { - fn ui(&mut self, ui: &mut egui::Ui) -> PaneResponse { + fn ui(&mut self, ui: &mut egui::Ui, _: TileId) -> PaneResponse { let mut response = PaneResponse::default(); let Self { @@ -130,7 +119,7 @@ impl PaneBehavior for Plot2DPane { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Default, Clone, Serialize, Deserialize)] struct PlotMessageView { // == Settings from the UI == settings: MsgSources, @@ -138,25 +127,20 @@ struct PlotMessageView { #[serde(skip)] points: Vec<(f64, Vec<f64>)>, // == Internal == - id: egui::Id, + id: ViewId, #[serde(skip)] cache_valid: bool, } impl PlotMessageView { - fn new(id: egui::Id) -> Self { - Self { - settings: Default::default(), - points: Vec::new(), - id, - cache_valid: false, - } + fn new() -> Self { + Self::default() } } impl MessageView for PlotMessageView { - fn widget_id(&self) -> &egui::Id { - &self.id + fn view_id(&self) -> ViewId { + self.id } fn id_of_interest(&self) -> u32 { diff --git a/src/ui/utils.rs b/src/ui/utils.rs index 3172463e01bd62f94570e37218be9dd3447f730b..7ad863175181d3b06766858c500a07707bcefc79 100644 --- a/src/ui/utils.rs +++ b/src/ui/utils.rs @@ -1,16 +1,17 @@ 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, pane: &mut Pane) { +pub fn maximized_pane_ui(ui: &mut Ui, tile_id: TileId, pane: &mut Pane) { 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, tile_id)); } #[derive(Debug, Default, Clone)] diff --git a/src/ui/widget_gallery.rs b/src/ui/widget_gallery.rs new file mode 100644 index 0000000000000000000000000000000000000000..a5288191848c04212c0845c403ad12b1c8d1ea31 --- /dev/null +++ b/src/ui/widget_gallery.rs @@ -0,0 +1,52 @@ +use egui::Context; +use egui_tiles::TileId; +use strum::{EnumMessage, IntoEnumIterator}; + +use super::{ + composable_view::PaneAction, + panes::{Pane, PaneKind}, +}; + +#[derive(Default)] +pub struct WidgetGallery { + pub open: bool, + tile_id: Option<TileId>, +} + +impl WidgetGallery { + pub fn replace_tile(&mut self, tile_id: TileId) { + self.tile_id = Some(tile_id); + self.open = true; + } + + pub fn show(&mut self, ctx: &Context) -> Option<PaneAction> { + let mut window_visible = self.open; + let resp = egui::Window::new("Widget Gallery") + .collapsible(false) + .open(&mut window_visible) + .show(ctx, |ui| { + for pane in PaneKind::iter() { + if let PaneKind::Default(_) = pane { + continue; + } 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))); + } + } + } + } + None + }); + self.open = window_visible; + + let action = resp.and_then(|resp| resp.inner).flatten(); + + // If an action was taken, always close the window + if action.is_some() { + self.open = false; + } + + action + } +}