use egui::NumExt as _;
use re_format::format_number;
use re_renderer::WgpuResourcePoolStatistics;
use re_ui::UICommand;
use re_viewer_context::StoreContext;
use crate::{app_blueprint::AppBlueprint, App};
pub fn top_panel(
app_blueprint: &AppBlueprint<'_>,
store_context: Option<&StoreContext<'_>>,
ui: &mut egui::Ui,
frame: &mut eframe::Frame,
app: &mut App,
gpu_resource_stats: &WgpuResourcePoolStatistics,
) {
re_tracing::profile_function!();
let native_pixels_per_point = frame.info().native_pixels_per_point;
let fullscreen = {
#[cfg(target_arch = "wasm32")]
{
false
}
#[cfg(not(target_arch = "wasm32"))]
{
frame.info().window_info.fullscreen
}
};
let style_like_web = app.is_screenshotting();
let top_bar_style =
app.re_ui()
.top_bar_style(native_pixels_per_point, fullscreen, style_like_web);
egui::TopBottomPanel::top("top_bar")
.frame(app.re_ui().top_panel_frame())
.exact_height(top_bar_style.height)
.show_inside(ui, |ui| {
let _response = egui::menu::bar(ui, |ui| {
ui.set_height(top_bar_style.height);
ui.add_space(top_bar_style.indent);
top_bar_ui(
app_blueprint,
store_context,
ui,
frame,
app,
gpu_resource_stats,
);
})
.response;
#[cfg(not(target_arch = "wasm32"))]
if !re_ui::NATIVE_WINDOW_BAR {
let title_bar_response = _response.interact(egui::Sense::click());
if title_bar_response.double_clicked() {
frame.set_maximized(!frame.info().window_info.maximized);
} else if title_bar_response.is_pointer_button_down_on() {
frame.drag_window();
}
}
});
}
fn top_bar_ui(
app_blueprint: &AppBlueprint<'_>,
store_context: Option<&StoreContext<'_>>,
ui: &mut egui::Ui,
frame: &mut eframe::Frame,
app: &mut App,
gpu_resource_stats: &WgpuResourcePoolStatistics,
) {
app.rerun_menu_button_ui(store_context, ui, frame);
ui.add_space(12.0);
website_link_ui(ui);
if app.app_options().show_metrics {
ui.separator();
frame_time_label_ui(ui, app);
memory_use_label_ui(ui, gpu_resource_stats);
input_latency_label_ui(ui, app);
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if re_ui::CUSTOM_WINDOW_DECORATIONS && !cfg!(target_arch = "wasm32") {
ui.add_space(8.0);
#[cfg(not(target_arch = "wasm32"))]
re_ui::native_window_buttons_ui(frame, ui);
ui.separator();
} else {
let extra_margin = (ui.available_height() - 24.0) / 2.0;
ui.add_space(extra_margin);
}
let mut selection_panel_expanded = app_blueprint.selection_panel_expanded;
if app
.re_ui()
.medium_icon_toggle_button(
ui,
&re_ui::icons::RIGHT_PANEL_TOGGLE,
&mut selection_panel_expanded,
)
.on_hover_text(format!(
"Toggle Selection View{}",
UICommand::ToggleSelectionPanel.format_shortcut_tooltip_suffix(ui.ctx())
))
.clicked()
{
app_blueprint.toggle_selection_panel(&app.command_sender);
}
let mut time_panel_expanded = app_blueprint.time_panel_expanded;
if app
.re_ui()
.medium_icon_toggle_button(
ui,
&re_ui::icons::BOTTOM_PANEL_TOGGLE,
&mut time_panel_expanded,
)
.on_hover_text(format!(
"Toggle Timeline View{}",
UICommand::ToggleTimePanel.format_shortcut_tooltip_suffix(ui.ctx())
))
.clicked()
{
app_blueprint.toggle_time_panel(&app.command_sender);
}
let mut blueprint_panel_expanded = app_blueprint.blueprint_panel_expanded;
if app
.re_ui()
.medium_icon_toggle_button(
ui,
&re_ui::icons::LEFT_PANEL_TOGGLE,
&mut blueprint_panel_expanded,
)
.on_hover_text(format!(
"Toggle Blueprint View{}",
UICommand::ToggleBlueprintPanel.format_shortcut_tooltip_suffix(ui.ctx())
))
.clicked()
{
app_blueprint.toggle_blueprint_panel(&app.command_sender);
}
if cfg!(debug_assertions) && app.app_options().show_metrics {
ui.vertical_centered(|ui| {
ui.style_mut().wrap = Some(false);
ui.add_space(6.0); egui::warn_if_debug_build(ui);
});
}
});
}
fn website_link_ui(ui: &mut egui::Ui) {
let desired_height = ui.max_rect().height();
let desired_height = desired_height.at_most(28.0);
let image = re_ui::icons::RERUN_IO_TEXT
.as_image()
.max_height(desired_height);
let url = "https://rerun.io/";
let response = ui
.add(egui::ImageButton::new(image))
.on_hover_cursor(egui::CursorIcon::PointingHand)
.on_hover_text(url);
if response.clicked() {
ui.ctx().output_mut(|o| {
o.open_url = Some(egui::output::OpenUrl {
url: url.to_owned(),
new_tab: true,
});
});
}
}
fn frame_time_label_ui(ui: &mut egui::Ui, app: &App) {
if let Some(frame_time) = app.frame_time_history.average() {
let ms = frame_time * 1e3;
let visuals = ui.visuals();
let color = if ms < 15.0 {
visuals.weak_text_color()
} else {
visuals.warn_fg_color
};
let text = format!("{ms:.1} ms");
ui.label(egui::RichText::new(text).monospace().color(color))
.on_hover_text("CPU time used by Rerun Viewer each frame. Lower is better.");
}
}
fn memory_use_label_ui(ui: &mut egui::Ui, gpu_resource_stats: &WgpuResourcePoolStatistics) {
const CODE: &str = "use re_memory::AccountingAllocator;\n\
#[global_allocator]\n\
static GLOBAL: AccountingAllocator<std::alloc::System> =\n \
AccountingAllocator::new(std::alloc::System);";
fn click_to_copy(
ui: &mut egui::Ui,
text: impl Into<String>,
add_contents_on_hover: impl FnOnce(&mut egui::Ui),
) {
#[allow(clippy::blocks_in_if_conditions)]
let text = text.into();
if ui
.add(
egui::Label::new(
egui::RichText::new(text)
.monospace()
.color(ui.visuals().weak_text_color()),
)
.sense(egui::Sense::click()),
)
.on_hover_ui(|ui| add_contents_on_hover(ui))
.clicked()
{
ui.ctx().output_mut(|o| o.copied_text = CODE.to_owned());
}
}
let mem = re_memory::MemoryUse::capture();
if let Some(count) = re_memory::accounting_allocator::global_allocs() {
let bytes_used_text = re_format::format_bytes(count.size as _);
ui.label(
egui::RichText::new(&bytes_used_text)
.monospace()
.color(ui.visuals().weak_text_color()),
)
.on_hover_text(format!(
"Rerun Viewer is using {} of RAM in {} separate allocations,\n\
plus {} of GPU memory in {} textures and {} buffers.",
bytes_used_text,
format_number(count.count),
re_format::format_bytes(gpu_resource_stats.total_bytes() as _),
format_number(gpu_resource_stats.num_textures),
format_number(gpu_resource_stats.num_buffers),
));
} else if let Some(rss) = mem.resident {
let bytes_used_text = re_format::format_bytes(rss as _);
click_to_copy(ui, &bytes_used_text, |ui| {
ui.label(format!(
"Rerun Viewer is using {} of Resident memory (RSS),\n\
plus {} of GPU memory in {} textures and {} buffers.",
bytes_used_text,
re_format::format_bytes(gpu_resource_stats.total_bytes() as _),
format_number(gpu_resource_stats.num_textures),
format_number(gpu_resource_stats.num_buffers),
));
ui.label(
"To get more accurate memory reportings, consider configuring your Rerun \n\
viewer to use an AccountingAllocator by adding the following to your \n\
code's main entrypoint:",
);
ui.code(CODE);
ui.label("(click to copy to clipboard)");
});
} else {
click_to_copy(ui, "N/A MiB", |ui| {
ui.label(
"The Rerun viewer was not configured to run with an AccountingAllocator,\n\
consider adding the following to your code's main entrypoint:",
);
ui.code(CODE);
ui.label("(click to copy to clipboard)");
});
}
}
fn input_latency_label_ui(ui: &mut egui::Ui, app: &mut App) {
let rx = app.msg_receive_set();
if rx.is_empty() {
return;
}
let is_latency_interesting = rx.sources().iter().any(|s| s.is_network());
let queue_len = rx.queue_len();
let latency_sec = rx.latency_ns() as f32 / 1e9;
if queue_len > 0 && (!is_latency_interesting || app.app_options().warn_latency < latency_sec) {
app.latest_queue_interest = web_time::Instant::now();
}
if app.latest_queue_interest.elapsed().as_secs_f32() < 1.0 {
ui.separator();
if is_latency_interesting {
let text = format!(
"Latency: {:.2}s, queue: {}",
latency_sec,
format_number(queue_len),
);
let hover_text =
"When more data is arriving over network than the Rerun Viewer can index, a queue starts building up, leading to latency and increased RAM use.\n\
This latency does NOT include network latency.";
if latency_sec < app.app_options().warn_latency {
ui.weak(text).on_hover_text(hover_text);
} else {
ui.label(app.re_ui().warning_text(text))
.on_hover_text(hover_text);
}
} else {
ui.weak(format!("Queue: {}", format_number(queue_len)))
.on_hover_text("Number of messages in the inbound queue");
}
}
}