use ahash::HashMap;
use re_data_store::StoreDb;
use re_log_types::{LogMsg, StoreId, TimeRangeF};
use re_smart_channel::ReceiveSet;
use re_viewer_context::{
AppOptions, Caches, CommandSender, ComponentUiRegistry, PlayState, RecordingConfig,
SelectionState, SpaceViewClassRegistry, StoreContext, ViewerContext,
};
use re_viewport::{SpaceInfoCollection, Viewport, ViewportState};
use crate::ui::recordings_panel_ui;
use crate::{app_blueprint::AppBlueprint, store_hub::StoreHub, ui::blueprint_panel_ui};
const WATERMARK: bool = false;
#[derive(Default, serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct AppState {
pub(crate) app_options: AppOptions,
#[serde(skip)]
pub(crate) cache: Caches,
recording_configs: HashMap<StoreId, RecordingConfig>,
selection_panel: crate::selection_panel::SelectionPanel,
time_panel: re_time_panel::TimePanel,
#[serde(skip)]
welcome_screen: crate::ui::WelcomeScreen,
#[serde(skip)]
viewport_state: ViewportState,
}
impl AppState {
pub fn app_options(&self) -> &AppOptions {
&self.app_options
}
pub fn app_options_mut(&mut self) -> &mut AppOptions {
&mut self.app_options
}
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
pub fn loop_selection(
&self,
store_context: Option<&StoreContext<'_>>,
) -> Option<(re_data_store::Timeline, TimeRangeF)> {
store_context
.as_ref()
.and_then(|ctx| ctx.recording)
.map(|rec| rec.store_id())
.and_then(|rec_id| {
self.recording_configs
.get(rec_id)
.and_then(|rec_cfg| {
rec_cfg
.time_ctrl
.loop_selection()
.map(|q| (*rec_cfg.time_ctrl.timeline(), q))
})
})
}
#[allow(clippy::too_many_arguments)]
pub fn show(
&mut self,
app_blueprint: &AppBlueprint<'_>,
ui: &mut egui::Ui,
render_ctx: &mut re_renderer::RenderContext,
store_db: &StoreDb,
store_context: &StoreContext<'_>,
re_ui: &re_ui::ReUi,
component_ui_registry: &ComponentUiRegistry,
space_view_class_registry: &SpaceViewClassRegistry,
rx: &ReceiveSet<LogMsg>,
command_sender: &CommandSender,
) {
re_tracing::profile_function!();
let Self {
app_options,
cache,
recording_configs,
selection_panel,
time_panel,
welcome_screen,
viewport_state,
} = self;
let mut viewport = Viewport::from_db(store_context.blueprint, viewport_state);
recording_config_entry(recording_configs, store_db.store_id().clone(), store_db)
.selection_state
.on_frame_start(|item| viewport.is_item_valid(item));
let rec_cfg =
recording_config_entry(recording_configs, store_db.store_id().clone(), store_db);
let mut ctx = ViewerContext {
app_options,
cache,
space_view_class_registry,
component_ui_registry,
store_db,
store_context,
rec_cfg,
re_ui,
render_ctx,
command_sender,
};
let spaces_info = SpaceInfoCollection::new(&ctx.store_db.entity_db);
if viewport.blueprint.is_invalid() {
re_log::warn!("Incompatible blueprint detected. Resetting to default.");
viewport.blueprint.reset(&mut ctx, &spaces_info);
}
viewport.on_frame_start(&mut ctx, &spaces_info);
time_panel.show_panel(&mut ctx, ui, app_blueprint.time_panel_expanded);
selection_panel.show_panel(
&mut ctx,
ui,
&mut viewport,
app_blueprint.selection_panel_expanded,
);
let central_panel_frame = egui::Frame {
fill: ui.style().visuals.panel_fill,
inner_margin: egui::Margin::same(0.0),
..Default::default()
};
egui::CentralPanel::default()
.frame(central_panel_frame)
.show_inside(ui, |ui| {
let left_panel = egui::SidePanel::left("blueprint_panel")
.resizable(true)
.frame(egui::Frame {
fill: ui.visuals().panel_fill,
..Default::default()
})
.min_width(120.0)
.default_width((0.35 * ui.ctx().screen_rect().width()).min(200.0).round());
left_panel.show_animated_inside(
ui,
app_blueprint.blueprint_panel_expanded,
|ui: &mut egui::Ui| {
ui.set_clip_rect(ui.max_rect());
ui.spacing_mut().item_spacing.y = 0.0;
let recording_shown = recordings_panel_ui(&mut ctx, rx, ui);
if recording_shown {
ui.add_space(4.0);
}
blueprint_panel_ui(&mut viewport.blueprint, &mut ctx, ui, &spaces_info);
},
);
let viewport_frame = egui::Frame {
fill: ui.style().visuals.panel_fill,
..Default::default()
};
let show_welcome =
store_context.blueprint.app_id() == Some(&StoreHub::welcome_screen_app_id());
egui::CentralPanel::default()
.frame(viewport_frame)
.show_inside(ui, |ui| {
if show_welcome {
welcome_screen.ui(ui, rx, command_sender);
} else {
viewport.viewport_ui(ui, &mut ctx);
}
});
});
viewport.sync_blueprint_changes(command_sender);
{
let dt = ui.ctx().input(|i| i.stable_dt);
let more_data_is_coming = if let Some(store_source) = &store_db.data_source {
rx.sources().iter().any(|s| s.as_ref() == store_source)
} else {
false
};
let needs_repaint = ctx.rec_cfg.time_ctrl.update(
store_db.times_per_timeline(),
dt,
more_data_is_coming,
);
if needs_repaint == re_viewer_context::NeedsRepaint::Yes {
ui.ctx().request_repaint();
}
}
if WATERMARK {
re_ui.paint_watermark();
}
check_for_clicked_hyperlinks(&re_ui.egui_ctx, &mut rec_cfg.selection_state);
}
pub fn recording_config_mut(&mut self, rec_id: &StoreId) -> Option<&mut RecordingConfig> {
self.recording_configs.get_mut(rec_id)
}
pub fn cleanup(&mut self, store_hub: &StoreHub) {
re_tracing::profile_function!();
self.recording_configs
.retain(|store_id, _| store_hub.contains_recording(store_id));
}
}
fn recording_config_entry<'cfgs>(
configs: &'cfgs mut HashMap<StoreId, RecordingConfig>,
id: StoreId,
store_db: &'_ StoreDb,
) -> &'cfgs mut RecordingConfig {
fn new_recording_config(store_db: &'_ StoreDb) -> RecordingConfig {
let play_state = if let Some(data_source) = &store_db.data_source {
match data_source {
re_smart_channel::SmartChannelSource::File(_)
| re_smart_channel::SmartChannelSource::RrdHttpStream { .. }
| re_smart_channel::SmartChannelSource::RrdWebEventListener => PlayState::Playing,
re_smart_channel::SmartChannelSource::Sdk
| re_smart_channel::SmartChannelSource::WsClient { .. }
| re_smart_channel::SmartChannelSource::TcpServer { .. } => PlayState::Following,
}
} else {
PlayState::Following };
let mut rec_cfg = RecordingConfig::default();
rec_cfg
.time_ctrl
.set_play_state(store_db.times_per_timeline(), play_state);
rec_cfg
}
configs
.entry(id)
.or_insert_with(|| new_recording_config(store_db))
}
fn check_for_clicked_hyperlinks(egui_ctx: &egui::Context, selection_state: &mut SelectionState) {
let recording_scheme = "recording://";
let mut path = None;
egui_ctx.output_mut(|o| {
if let Some(open_url) = &o.open_url {
if let Some(path_str) = open_url.url.strip_prefix(recording_scheme) {
path = Some(path_str.to_owned());
o.open_url = None;
}
}
});
if let Some(path) = path {
match path.parse() {
Ok(item) => {
selection_state.set_single_selection(item);
}
Err(err) => {
re_log::warn!("Failed to parse entity path {path:?}: {err}");
}
}
}
}