use std::collections::BTreeMap;
use re_data_ui::DataUi;
use re_log_types::LogMsg;
use re_smart_channel::{ReceiveSet, SmartChannelSource};
use re_viewer_context::{SystemCommand, SystemCommandSender, ViewerContext};
pub fn recordings_panel_ui(
ctx: &ViewerContext<'_>,
rx: &ReceiveSet<LogMsg>,
ui: &mut egui::Ui,
) -> bool {
ctx.re_ui.panel_content(ui, |re_ui, ui| {
re_ui.panel_title_bar_with_buttons(
ui,
"Recordings",
Some("These are the Recordings currently loaded in the Viewer"),
|ui| {
add_button_ui(ctx, ui);
},
);
});
egui::ScrollArea::both()
.id_source("recordings_scroll_area")
.auto_shrink([false, true])
.max_height(300.)
.show(ui, |ui| {
ctx.re_ui.panel_content(ui, |_re_ui, ui| {
let mut any_shown = false;
any_shown |= recording_list_ui(ctx, ui);
any_shown |= loading_receivers_ui(ctx, rx, ui);
any_shown
})
})
.inner
}
fn loading_receivers_ui(
ctx: &ViewerContext<'_>,
rx: &ReceiveSet<LogMsg>,
ui: &mut egui::Ui,
) -> bool {
let sources_with_stores: ahash::HashSet<SmartChannelSource> = 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 string = match source.as_ref() {
SmartChannelSource::File(path) => format!("Loading {}…", path.display()),
SmartChannelSource::RrdHttpStream { url } => format!("Loading {url}…"),
SmartChannelSource::RrdWebEventListener
| SmartChannelSource::Sdk
| SmartChannelSource::WsClient { .. }
| SmartChannelSource::TcpServer { .. }
| SmartChannelSource::Stdin => {
continue;
}
};
if !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
}
fn recording_list_ui(ctx: &ViewerContext<'_>, ui: &mut egui::Ui) -> bool {
let mut entity_dbs_map: BTreeMap<_, Vec<_>> = BTreeMap::new();
for entity_db in &ctx.store_context.all_recordings {
let key = entity_db
.store_info()
.map_or("<unknown>", |info| info.application_id.as_str());
entity_dbs_map.entry(key).or_default().push(*entity_db);
}
if entity_dbs_map.is_empty() {
return false;
}
for entity_dbs in entity_dbs_map.values_mut() {
entity_dbs.sort_by_key(|entity_db| entity_db.store_info().map(|info| info.started));
}
let active_recording = ctx.store_context.recording.map(|rec| rec.store_id());
for (app_id, entity_dbs) in entity_dbs_map {
if entity_dbs.len() == 1 {
let entity_db = entity_dbs[0];
recording_ui(ctx, ui, entity_db, Some(app_id), active_recording);
} else {
ctx.re_ui.list_item(app_id).active(false).show_collapsing(
ui,
ui.make_persistent_id(app_id),
true,
|_, ui| {
for entity_db in entity_dbs {
recording_ui(ctx, ui, entity_db, None, active_recording);
}
},
);
}
}
true
}
fn recording_ui(
ctx: &ViewerContext<'_>,
ui: &mut egui::Ui,
entity_db: &re_entity_db::EntityDb,
app_id_label: Option<&str>,
active_recording: Option<&re_log_types::StoreId>,
) {
let app_id_label =
app_id_label.map_or(String::new(), |app_id_label| format!("{app_id_label} - "));
let time = entity_db
.store_info()
.and_then(|info| {
info.started
.format_time_custom("[hour]:[minute]:[second]", ctx.app_options.time_zone)
})
.unwrap_or("<unknown time>".to_owned());
let title = format!("{app_id_label}{time}");
let store_id = entity_db.store_id().clone();
let item = re_viewer_context::Item::StoreId(store_id.clone());
let mut list_item = ctx
.re_ui
.list_item(title)
.selected(ctx.selection().contains_item(&item))
.with_icon_fn(|_re_ui, ui, rect, visuals| {
let color = if active_recording == Some(&store_id) {
visuals.fg_stroke.color
} else {
ui.visuals().widgets.noninteractive.fg_stroke.color
};
re_ui::icons::STORE
.as_image()
.tint(color)
.paint_at(ui, rect);
})
.with_buttons(|re_ui, ui| {
let resp = re_ui
.small_icon_button(ui, &re_ui::icons::REMOVE)
.on_hover_text("Close this Recording (unsaved data will be lost)");
if resp.clicked() {
ctx.command_sender
.send_system(SystemCommand::CloseRecordingId(store_id.clone()));
}
resp
});
if ctx.hovered().contains_item(&item) {
list_item = list_item.force_hovered(true);
}
let response = list_item.show(ui).on_hover_ui(|ui| {
entity_db.data_ui(
ctx,
ui,
re_viewer_context::UiVerbosity::Full,
&ctx.current_query(),
entity_db.store(),
);
});
if response.hovered() {
ctx.selection_state().set_hovered(item.clone());
}
if response.clicked() {
ctx.command_sender
.send_system(SystemCommand::SetRecordingId(store_id.clone()));
ctx.command_sender
.send_system(SystemCommand::SetSelection(store_id, item));
}
}
fn add_button_ui(ctx: &ViewerContext<'_>, ui: &mut egui::Ui) {
use re_ui::UICommandSender;
if ctx
.re_ui
.small_icon_button(ui, &re_ui::icons::ADD)
.on_hover_text(re_ui::UICommand::Open.tooltip_with_shortcut(ui.ctx()))
.clicked()
{
ctx.command_sender.send_ui(re_ui::UICommand::Open);
}
}