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
+    }
+}