Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add hierarchical display in recordings panel #2971

Merged
merged 5 commits into from
Aug 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 1 addition & 3 deletions crates/re_ui/src/list_item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ impl<'a> ListItem<'a> {
) {
let mut state = egui::collapsing_header::CollapsingState::load_with_default_open(
ui.ctx(),
ui.make_persistent_id(self.text.text()),
ui.make_persistent_id(ui.id().with(self.text.text())),
default_open,
);

Expand All @@ -142,9 +142,7 @@ impl<'a> ListItem<'a> {
}

state.show_body_indented(&response.response, ui, |ui| {
ui.add_space(4.0); // Add space only if there is a body to make minimized headers stick together.
add_body(re_ui, ui);
ui.add_space(4.0); // Same here
});
}

Expand Down
1 change: 1 addition & 0 deletions crates/re_viewer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ egui.workspace = true
egui-wgpu.workspace = true
image = { workspace = true, default-features = false, features = ["png"] }
itertools = { workspace = true }
once_cell = { workspace = true }
poll-promise = "0.2"
rfd.workspace = true
ron = "0.8.0"
Expand Down
14 changes: 8 additions & 6 deletions crates/re_viewer/src/app_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,19 +165,21 @@ impl AppState {
// doesn't have inner margins.
ui.set_clip_rect(ui.max_rect());

// TODO(ab): this might be promoted higher in the hierarchy once list item are
// used in the blueprint panel section.
ui.scope(|ui| {
ui.spacing_mut().item_spacing.y = 0.0;
recordings_panel_ui(&mut ctx, ui);
});
// ListItem don't need vertical spacing so we disable it, but restore it
// before drawing the blueprint panel.
// TODO(ab): remove this once the blueprint tree uses list items
let v_space = ui.spacing().item_spacing.y;
ui.spacing_mut().item_spacing.y = 0.0;

recordings_panel_ui(&mut ctx, ui);

// TODO(ab): remove this frame once the blueprint tree uses list items
egui::Frame {
inner_margin: re_ui::ReUi::panel_margin(),
..Default::default()
}
.show(ui, |ui| {
ui.spacing_mut().item_spacing.y = v_space;
blueprint_panel_ui(&mut viewport.blueprint, &mut ctx, ui, &spaces_info);
});
},
Expand Down
116 changes: 75 additions & 41 deletions crates/re_viewer/src/ui/recordings_panel.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
use re_data_store::StoreDb;
use re_viewer_context::{SystemCommand, SystemCommandSender, ViewerContext};
use std::collections::BTreeMap;
use time::macros::format_description;

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.
pub fn recordings_panel_ui(ctx: &mut ViewerContext<'_>, ui: &mut egui::Ui) {
ctx.re_ui.panel_content(ui, |re_ui, ui| {
Expand Down Expand Up @@ -34,58 +38,88 @@ fn recording_list_ui(ctx: &mut ViewerContext<'_>, ui: &mut egui::Ui) {
..
} = ctx;

let mut store_dbs = store_context.alternate_recordings.clone();
if store_dbs.is_empty() {
return;
let mut store_dbs_map: BTreeMap<_, Vec<_>> = BTreeMap::new();
for store_db in &store_context.alternate_recordings {
let key = store_db
.store_info()
.map_or("<unknown>", |info| info.application_id.as_str());
store_dbs_map.entry(key).or_default().push(*store_db);
}

fn store_db_key(store_db: &StoreDb) -> impl Ord + '_ {
store_db
.store_info()
.map(|info| (info.application_id.0.as_str(), info.started))
if store_dbs_map.is_empty() {
return;
}

store_dbs.sort_by_key(|store_db| store_db_key(store_db));
for store_dbs in store_dbs_map.values_mut() {
store_dbs.sort_by_key(|store_db| store_db.store_info().map(|info| info.started));
}

let active_recording = store_context.recording.map(|rec| rec.store_id());

let desc = format_description!(version = 2, "[hour]:[minute]:[second]Z");
for store_db in &store_dbs {
let info = if let Some(store_info) = store_db.store_info() {
format!(
"{} - {}",
store_info.application_id,
store_info
.started
.to_datetime()
.and_then(|dt| dt.format(&desc).ok())
.unwrap_or("<unknown>".to_owned())
)
for (app_id, store_dbs) in store_dbs_map {
if store_dbs.len() == 1 {
let store_db = store_dbs[0];
if recording_ui(ctx.re_ui, ui, store_db, Some(app_id), active_recording).clicked() {
command_sender
.send_system(SystemCommand::SetRecordingId(store_db.store_id().clone()));
}
} else {
"<UNKNOWN>".to_owned()
};

if ctx
.re_ui
.list_item(info)
.with_icon_fn(|_re_ui, ui, rect, visuals| {
let color = if active_recording == Some(store_db.store_id()) {
visuals.fg_stroke.color
} else {
ui.visuals().widgets.noninteractive.fg_stroke.color
};

ui.painter()
.circle(rect.center(), 4.0, color, egui::Stroke::NONE);
})
.show(ui)
.clicked()
{
command_sender.send_system(SystemCommand::SetRecordingId(store_db.store_id().clone()));
ctx.re_ui
.list_item(app_id)
.active(false)
.show_collapsing(ui, true, |_, ui| {
for store_db in store_dbs {
if recording_ui(ctx.re_ui, ui, store_db, None, active_recording).clicked() {
command_sender.send_system(SystemCommand::SetRecordingId(
store_db.store_id().clone(),
));
}
}
});
}
}
}

/// Show the UI for a single recording.
///
/// If an `app_id_label` is provided, it will be shown in front of the recording time.
fn recording_ui(
re_ui: &re_ui::ReUi,
ui: &mut egui::Ui,
store_db: &re_data_store::StoreDb,
app_id_label: Option<&str>,
active_recording: Option<&re_log_types::StoreId>,
) -> egui::Response {
let prefix = if let Some(app_id_label) = app_id_label {
format!("{app_id_label} - ")
} else {
String::new()
};

let name = store_db
.store_info()
.and_then(|info| {
info.started
.to_datetime()
.and_then(|dt| dt.format(&TIME_FORMAT_DESCRIPTION).ok())
})
.unwrap_or("<unknown time>".to_owned());

re_ui
.list_item(format!("{prefix}{name}"))
.with_icon_fn(|_re_ui, ui, rect, visuals| {
let color = if active_recording == Some(store_db.store_id()) {
visuals.fg_stroke.color
} else {
ui.visuals().widgets.noninteractive.fg_stroke.color
};

ui.painter()
.circle(rect.center(), 4.0, color, egui::Stroke::NONE);
})
.show(ui)
}

#[cfg(not(target_arch = "wasm32"))]
fn add_button_ui(ctx: &mut ViewerContext<'_>, ui: &mut egui::Ui) {
use re_ui::UICommandSender;
Expand Down