diff --git a/crates/re_renderer/examples/framework.rs b/crates/re_renderer/examples/framework.rs index 678816802ad1..95631e2da1e3 100644 --- a/crates/re_renderer/examples/framework.rs +++ b/crates/re_renderer/examples/framework.rs @@ -246,7 +246,7 @@ impl Application { return; } Err(err) => { - re_log::warn!(%err, "dropped frame"); + re_log::warn!("Dropped frame: {err}"); return; } }; diff --git a/crates/re_smart_channel/src/receive_set.rs b/crates/re_smart_channel/src/receive_set.rs index d6afbb442f04..2ea606643a8c 100644 --- a/crates/re_smart_channel/src/receive_set.rs +++ b/crates/re_smart_channel/src/receive_set.rs @@ -31,6 +31,11 @@ impl ReceiveSet { rx.push(r); } + /// Disconnect from any channel with the given source. + pub fn remove(&self, source: &SmartChannelSource) { + self.receivers.lock().retain(|r| r.source() != source); + } + /// List of connected receiver sources. /// /// This gets culled after calling one of the `recv` methods. diff --git a/crates/re_ui/src/lib.rs b/crates/re_ui/src/lib.rs index 3b4ffeb85e2e..e6b423e80d6e 100644 --- a/crates/re_ui/src/lib.rs +++ b/crates/re_ui/src/lib.rs @@ -702,12 +702,13 @@ impl ReUi { } /// Two-column grid to be used in selection view. + /// + /// Use this when you expect the right column to have multi-line entries. #[allow(clippy::unused_self)] - pub fn selection_grid(&self, ui: &mut egui::Ui, id: &str) -> egui::Grid { + pub fn selection_grid(&self, _ui: &mut egui::Ui, id: &str) -> egui::Grid { // Spread rows a bit to make it easier to see the groupings - egui::Grid::new(id) - .num_columns(2) - .spacing(ui.style().spacing.item_spacing + egui::vec2(0.0, 8.0)) + let spacing = egui::vec2(8.0, 16.0); + egui::Grid::new(id).num_columns(2).spacing(spacing) } /// Draws a shadow into the given rect with the shadow direction given from dark to light diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index 2431b54f3902..20980cfdd8fe 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -712,9 +712,9 @@ impl App { re_smart_channel::SmartMessagePayload::Msg(msg) => msg, re_smart_channel::SmartMessagePayload::Quit(err) => { if let Some(err) = err { - re_log::warn!(%msg.source, err, "data source has left unexpectedly"); + re_log::warn!("Data source {} has left unexpectedly: {err}", msg.source); } else { - re_log::debug!(%msg.source, "data source has left"); + re_log::debug!("Data source {} has left", msg.source); } continue; } diff --git a/crates/re_viewer/src/app_state.rs b/crates/re_viewer/src/app_state.rs index 9bd473483879..e3a909aa4e2f 100644 --- a/crates/re_viewer/src/app_state.rs +++ b/crates/re_viewer/src/app_state.rs @@ -171,7 +171,7 @@ impl AppState { // before drawing the blueprint panel. ui.spacing_mut().item_spacing.y = 0.0; - let recording_shown = recordings_panel_ui(&mut ctx, ui); + let recording_shown = recordings_panel_ui(&mut ctx, rx, ui); if recording_shown { ui.add_space(4.0); diff --git a/crates/re_viewer/src/store_hub.rs b/crates/re_viewer/src/store_hub.rs index c8d86155f636..fbdaecb41e05 100644 --- a/crates/re_viewer/src/store_hub.rs +++ b/crates/re_viewer/src/store_hub.rs @@ -108,7 +108,7 @@ impl StoreHub { StoreContext { blueprint, recording, - alternate_recordings: self.store_dbs.recordings().collect_vec(), + all_recordings: self.store_dbs.recordings().collect_vec(), } }) } diff --git a/crates/re_viewer/src/ui/recordings_panel.rs b/crates/re_viewer/src/ui/recordings_panel.rs index 43b01f1256ad..95d8c9be92b7 100644 --- a/crates/re_viewer/src/ui/recordings_panel.rs +++ b/crates/re_viewer/src/ui/recordings_panel.rs @@ -1,15 +1,24 @@ -use re_viewer_context::{CommandSender, SystemCommand, SystemCommandSender, ViewerContext}; use std::collections::BTreeMap; + use time::macros::format_description; +use re_log_types::LogMsg; +use re_smart_channel::{ReceiveSet, SmartChannelSource}; +use re_viewer_context::{CommandSender, SystemCommand, SystemCommandSender, ViewerContext}; + static TIME_FORMAT_DESCRIPTION: once_cell::sync::Lazy< &'static [time::format_description::FormatItem<'static>], > = once_cell::sync::Lazy::new(|| format_description!(version = 2, "[hour]:[minute]:[second]Z")); /// Show the currently open Recordings in a selectable list. +/// Also shows the currently loading receivers. /// /// Returns `true` if any recordings were shown. -pub fn recordings_panel_ui(ctx: &mut ViewerContext<'_>, ui: &mut egui::Ui) -> bool { +pub fn recordings_panel_ui( + ctx: &mut ViewerContext<'_>, + rx: &ReceiveSet, + ui: &mut egui::Ui, +) -> bool { ctx.re_ui.panel_content(ui, |re_ui, ui| { re_ui.panel_title_bar_with_buttons( ui, @@ -26,16 +35,87 @@ pub fn recordings_panel_ui(ctx: &mut ViewerContext<'_>, ui: &mut egui::Ui) -> bo .auto_shrink([false, true]) .max_height(300.) .show(ui, |ui| { - ctx.re_ui - .panel_content(ui, |_re_ui, ui| recording_list_ui(ctx, ui)) + ctx.re_ui.panel_content(ui, |_re_ui, ui| { + let mut any_shown = false; + any_shown |= recording_list_ui(ctx, ui); + + // Show currently loading things after. + // They will likely end up here as recordings soon. + any_shown |= loading_receivers_ui(ctx, rx, ui); + + any_shown + }) }) .inner } +fn loading_receivers_ui( + ctx: &mut ViewerContext<'_>, + rx: &ReceiveSet, + ui: &mut egui::Ui, +) -> bool { + let sources_with_stores: ahash::HashSet = ctx + .store_context + .all_recordings + .iter() + .filter_map(|store| store.data_source.clone()) + .collect(); + + let mut any_shown = false; + + for source in rx.sources() { + let (always_show, string) = match source.as_ref() { + SmartChannelSource::File(path) => (false, format!("Loading {}…", path.display())), + + SmartChannelSource::RrdHttpStream { url } => (false, format!("Loading {url}…")), + + SmartChannelSource::RrdWebEventListener => { + (false, "Waiting on Web Event Listener…".to_owned()) + } + + SmartChannelSource::Sdk => (false, "Waiting on SDK…".to_owned()), + + SmartChannelSource::WsClient { ws_server_url } => { + (false, format!("Loading from {ws_server_url}…")) + } + + SmartChannelSource::TcpServer { port } => { + // We have a TcpServer when running just `cargo rerun` + (true, format!("Hosting a TCP Server on port {port}")) + } + }; + + // Only show if we don't have a recording for this source, + // i.e. if this source hasn't sent anything yet. + // Note that usually there is a one-to-one mapping between a source and a recording, + // but it is possible to send multiple recordings over the same channel. + if always_show || !sources_with_stores.contains(&source) { + any_shown = true; + let response = ctx + .re_ui + .list_item(string) + .with_buttons(|re_ui, ui| { + let resp = re_ui + .small_icon_button(ui, &re_ui::icons::REMOVE) + .on_hover_text("Disconnect from this source"); + if resp.clicked() { + rx.remove(&source); + } + resp + }) + .show(ui); + if let SmartChannelSource::TcpServer { .. } = source.as_ref() { + response.on_hover_text("You can connect to this viewer from a Rerun SDK"); + } + } + } + + any_shown +} + /// Draw the recording list. /// /// Returns `true` if any recordings were shown. -#[allow(clippy::blocks_in_if_conditions)] fn recording_list_ui(ctx: &mut ViewerContext<'_>, ui: &mut egui::Ui) -> bool { let ViewerContext { store_context, @@ -44,7 +124,7 @@ fn recording_list_ui(ctx: &mut ViewerContext<'_>, ui: &mut egui::Ui) -> bool { } = ctx; let mut store_dbs_map: BTreeMap<_, Vec<_>> = BTreeMap::new(); - for store_db in &store_context.alternate_recordings { + for store_db in &store_context.all_recordings { let key = store_db .store_info() .map_or("", |info| info.application_id.as_str()); @@ -133,7 +213,7 @@ fn recording_ui( }) .unwrap_or("".to_owned()); - re_ui + let response = re_ui .list_item(format!("{prefix}{name}")) .with_buttons(|re_ui, ui| { let resp = re_ui @@ -155,7 +235,68 @@ fn recording_ui( ui.painter() .circle(rect.center(), 4.0, color, egui::Stroke::NONE); }) - .show(ui) + .show(ui); + + response.on_hover_ui(|ui| { + recording_hover_ui(re_ui, ui, store_db); + }) +} + +fn recording_hover_ui(re_ui: &re_ui::ReUi, ui: &mut egui::Ui, store_db: &re_data_store::StoreDb) { + egui::Grid::new("recording_hover_ui") + .num_columns(2) + .show(ui, |ui| { + re_ui.grid_left_hand_label(ui, "Store ID"); + ui.label(store_db.store_id().to_string()); + ui.end_row(); + + if let Some(data_source) = &store_db.data_source { + re_ui.grid_left_hand_label(ui, "Data source"); + ui.label(data_source_string(data_source)); + ui.end_row(); + } + + if let Some(set_store_info) = store_db.recording_msg() { + let re_log_types::StoreInfo { + application_id, + store_id: _, + is_official_example: _, + started, + store_source, + store_kind, + } = &set_store_info.info; + + re_ui.grid_left_hand_label(ui, "Application ID"); + ui.label(application_id.to_string()); + ui.end_row(); + + re_ui.grid_left_hand_label(ui, "Recording started"); + ui.label(started.format()); + ui.end_row(); + + re_ui.grid_left_hand_label(ui, "Source"); + ui.label(store_source.to_string()); + ui.end_row(); + + // We are in the recordings menu, we know the kind + if false { + re_ui.grid_left_hand_label(ui, "Kind"); + ui.label(store_kind.to_string()); + ui.end_row(); + } + } + }); +} + +fn data_source_string(data_source: &re_smart_channel::SmartChannelSource) -> String { + match data_source { + SmartChannelSource::File(path) => path.display().to_string(), + SmartChannelSource::RrdHttpStream { url } => url.clone(), + SmartChannelSource::RrdWebEventListener => "Web Event Listener".to_owned(), + SmartChannelSource::Sdk => "SDK".to_owned(), + SmartChannelSource::WsClient { ws_server_url } => ws_server_url.clone(), + SmartChannelSource::TcpServer { port } => format!("TCP Server, port {port}"), + } } fn add_button_ui(ctx: &mut ViewerContext<'_>, ui: &mut egui::Ui) { diff --git a/crates/re_viewer_context/src/store_context.rs b/crates/re_viewer_context/src/store_context.rs index 00898d973fd0..5731f2b20215 100644 --- a/crates/re_viewer_context/src/store_context.rs +++ b/crates/re_viewer_context/src/store_context.rs @@ -4,5 +4,5 @@ use re_data_store::StoreDb; pub struct StoreContext<'a> { pub blueprint: &'a StoreDb, pub recording: Option<&'a StoreDb>, - pub alternate_recordings: Vec<&'a StoreDb>, + pub all_recordings: Vec<&'a StoreDb>, } diff --git a/crates/re_ws_comms/src/server.rs b/crates/re_ws_comms/src/server.rs index 76cd8d606897..7682412d7f4e 100644 --- a/crates/re_ws_comms/src/server.rs +++ b/crates/re_ws_comms/src/server.rs @@ -165,9 +165,9 @@ fn to_broadcast_stream( } re_smart_channel::SmartMessagePayload::Quit(err) => { if let Some(err) = err { - re_log::warn!(%msg.source, err, "sender has left unexpectedly"); + re_log::warn!("Sender {} has left unexpectedly: {err}", msg.source); } else { - re_log::debug!(%msg.source, "sender has left"); + re_log::debug!("Sender {} has left", msg.source); } } }