Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
62 changes: 57 additions & 5 deletions crates/viewer/re_recording_panel/src/data.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
//! Data structures describing the contents of the recording panel.

use std::collections::BTreeMap;
use std::iter;
use std::sync::Arc;
use std::task::Poll;

use ahash::HashMap;
use itertools::Itertools as _;
use itertools::{Either, Itertools as _};

use re_entity_db::EntityDb;
use re_entity_db::entity_db::EntityDbClass;
Expand All @@ -20,6 +21,9 @@ use re_viewer_context::{DisplayMode, Item, ViewerContext};
#[derive(Debug)]
#[cfg_attr(feature = "testing", derive(serde::Serialize))]
pub struct RecordingPanelData<'a> {
/// All the configured servers.
pub servers: Vec<ServerData<'a>>,

/// All the locally loaded application IDs and the corresponding recordings.
pub local_apps: Vec<AppIdData<'a>>,

Expand All @@ -32,9 +36,6 @@ pub struct RecordingPanelData<'a> {
/// Should the example section be displayed at all?
pub show_example_section: bool,

/// All the configured servers.
pub servers: Vec<ServerData<'a>>,

/// Recordings that are currently being loaded that we cannot attribute yet to a specific
/// section.
pub loading_receivers: Vec<Arc<SmartChannelSource>>,
Expand Down Expand Up @@ -148,11 +149,11 @@ impl<'a> RecordingPanelData<'a> {
.collect();

Self {
servers,
local_apps,
local_tables,
example_apps,
show_example_section,
servers,
loading_receivers,
}
}
Expand All @@ -163,6 +164,57 @@ impl<'a> RecordingPanelData<'a> {
&& self.example_apps.is_empty()
&& self.servers.is_empty()
}

/// Iterate over all items in the order they are displayed in the recording panel.
pub fn iter_items_in_display_order(&'a self) -> impl Iterator<Item = Item> + 'a {
let servers = self.servers.iter().flat_map(|s| {
let server_iter = iter::once(Item::RedapServer(s.origin.clone()));

let items = match &s.entries_data {
ServerEntriesData::Loaded {
dataset_entries,
table_entries,
failed_entries: _,
} => {
let dataset = dataset_entries.iter().flat_map(|dataset| {
let entry = iter::once(Item::RedapEntry(dataset.entry_data.entry_uri()));
let partitions =
dataset.displayed_partitions.iter().filter_map(|partition| {
partition
.entity_db()
.map(|entity_db| Item::StoreId(entity_db.store_id().clone()))
});
entry.chain(partitions)
});
let tables = table_entries
.iter()
.map(|table| Item::RedapEntry(table.entry_data.entry_uri()));
Either::Left(dataset.chain(tables))
}
_ => Either::Right(iter::empty()),
};

server_iter.chain(items)
});

let local_apps = self.local_apps.iter().flat_map(|app| {
iter::once(app.item()).chain(
app.loaded_recordings
.iter()
.map(|rec| Item::StoreId(rec.entity_db.store_id().clone())),
)
});

let example_apps = self.example_apps.iter().flat_map(|app| {
iter::once(app.item()).chain(
app.loaded_recordings
.iter()
.map(|rec| Item::StoreId(rec.entity_db.store_id().clone())),
)
});

servers.chain(local_apps).chain(example_apps)
}
}

// ---
Expand Down
2 changes: 2 additions & 0 deletions crates/viewer/re_recording_panel/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ pub mod data;

#[cfg(not(feature = "testing"))]
mod data;
mod recording_panel_command;
mod recording_panel_ui;

pub use self::recording_panel_ui::recordings_panel_ui;
pub use recording_panel_command::RecordingPanelCommand;
88 changes: 88 additions & 0 deletions crates/viewer/re_recording_panel/src/recording_panel_command.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
use crate::data::RecordingPanelData;
use egui::Id;
use itertools::Itertools;
use re_redap_browser::RedapServers;
use re_viewer_context::{DisplayMode, SystemCommand, SystemCommandSender, ViewerContext};

/// Commands that need to be handled in the context of the recording panel UI.
///
/// Why do we need another command kind?
/// So the Next / Previous recording action should be a `UiCommand` (for discoverability).
/// The order of recordings is defined by the recording panel, and the order of the next / previous
/// commands should match that. There is the nice [`RecordingPanelData`] struct that we can use
/// to iterate over the recordings in the same display order as the panel UI.
/// But to construct this, we need a [`ViewerContext`] and the usual `UiCommand` handler doesn't
/// have access to that, so we need to handle these commands "within" the frame, where we have
/// access to the context.
#[derive(Clone, Debug)]
pub enum RecordingPanelCommand {
/// Switch to the next recording in the recording panel.
SelectNextRecording,

/// Switch to the previous recording in the recording panel.
SelectPreviousRecording,
}

impl RecordingPanelCommand {
/// Send a command.
///
/// Since the recording panel has no state, commands are stored in egui context.
pub fn send(self, ctx: &egui::Context) {
ctx.data_mut(|d| {
let mut commands: &mut Vec<Self> = d.get_temp_mut_or_default(Id::NULL);
commands.push(self);
})
}

/// Read and clear all pending commands.
fn read(ctx: &egui::Context) -> Vec<Self> {
ctx.data_mut(|d| d.remove_temp(Id::NULL).unwrap_or_default())
}

/// Handle any pending recording panel commands.
pub fn handle_recording_panel_commands(ctx: &ViewerContext<'_>, servers: &'_ RedapServers) {
let commands = RecordingPanelCommand::read(ctx.egui_ctx());

let server_data = RecordingPanelData::new(ctx, servers, false);

for command in commands {
match command {
RecordingPanelCommand::SelectNextRecording => {
Self::shift_through_recordings(ctx, &server_data, 1);
}
RecordingPanelCommand::SelectPreviousRecording => {
Self::shift_through_recordings(ctx, &server_data, -1);
}
}
}
}

fn shift_through_recordings(
ctx: &ViewerContext<'_>,
server_data: &RecordingPanelData,
direction: isize,
) {
let recordings = server_data
.iter_items_in_display_order()
.filter(|item| DisplayMode::from_item(item).is_some())
.collect_vec();
let displayed_item = ctx.display_mode().item();

if let Some(displayed_item) = displayed_item {
let current_index = recordings.iter().position(|item| item == &displayed_item);

let previous_index = match current_index {
Some(idx) => {
let len = recordings.len() as isize;
((idx as isize + direction + len) % len) as usize
}
None => 0,
};

if let Some(previous_item) = recordings.get(previous_index) {
ctx.command_sender()
.send_system(SystemCommand::SetSelection(previous_item.clone().into()));
}
}
}
}
2 changes: 1 addition & 1 deletion crates/viewer/re_recording_panel/src/recording_panel_ui.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::sync::Arc;

use egui::{RichText, Widget as _};
use egui::{Modifiers, RichText, Widget as _};

use re_data_ui::DataUi as _;
use re_data_ui::item_ui::{entity_db_button_ui, table_id_button_ui};
Expand Down
12 changes: 12 additions & 0 deletions crates/viewer/re_ui/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ pub enum UICommand {
CloseCurrentRecording,
CloseAllEntries,

NextRecording,
PreviousRecording,

Undo,
Redo,

Expand Down Expand Up @@ -155,6 +158,12 @@ impl UICommand {
"Close all open current recording (unsaved data will be lost)",
),

UICommand::NextRecording => ("Next recording", "Switch to the next open recording"),
UICommand::PreviousRecording => (
"Previous recording",
"Switch to the previous open recording",
),

Self::Undo => (
"Undo",
"Undo the last blueprint edit for the open recording",
Expand Down Expand Up @@ -366,6 +375,9 @@ impl UICommand {
Self::CloseCurrentRecording => smallvec![],
Self::CloseAllEntries => smallvec![],

Self::NextRecording => smallvec![cmd(Key::CloseBracket)],
Self::PreviousRecording => smallvec![cmd(Key::OpenBracket)],
Comment on lines +378 to +379
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There needs to be some bike shedding on these shortcuts.

  • @pweids suggested it should be cmd + [ / ]
  • @lucasmerlin thinks it should be ctrl + tab / ctrl + shift + tab (which switches tabs in my browser, finder and recent files in my ide)
  • @abey79 would like something different

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: we can support multiple keyboard shortcuts (a primary and a backup; that's why this is a vec).

on macOS:
in my browser, I can cycle tabs with (at least) ctrl+[shift]+tab plus cmd+option+side-arrows plus ctrl+pageup/down.

on Windows and Linux, I think ctrl+[shift]+tab is the common one for switching between open tabs in an application?

Based on this, I suggest ctrl+[shift]+tab as the primary on all platforms, with cmd+option+down/up arrows as secondary on macOS.

But I'd like to know where @pweids got cmd + [ from? Is there some precedence there?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ctrl-tab is application switching in Windows, isn't it? I guess we could check what's the browser tab shortcut there.

I'm not sure where cmd + [ comes from, but I don't like it because it's not intl keyboard layout friendly. I need to do alt + 5 to get a [ on my swiss keyboard layout. (I guess never migrating to the US layout is top-3 reason I'll never be a 10x dev.)

Copy link
Member

@emilk emilk Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, in Windows Alt-Tab is application switching (like cmd-tab in Mac)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right, my bad!


Self::Undo => smallvec![cmd(Key::Z)],
Self::Redo => {
if os == OperatingSystem::Mac {
Expand Down
42 changes: 11 additions & 31 deletions crates/viewer/re_viewer/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1049,39 +1049,12 @@ impl App {

SystemCommand::SetSelection(items) => {
if let Some(item) = items.single_item() {
match item {
Item::RedapEntry(entry) => {
self.state
.navigation
.replace(DisplayMode::RedapEntry(entry.clone()));
}

Item::RedapServer(origin) => {
self.state
.navigation
.replace(DisplayMode::RedapServer(origin.clone()));
}

Item::TableId(table_id) => {
self.state
.navigation
.replace(DisplayMode::LocalTable(table_id.clone()));
}

Item::StoreId(store_id) => {
self.state
.navigation
.replace(DisplayMode::LocalRecordings(store_id.clone()));
// If the selected item has its own page, switch to it.
if let Some(display_mode) = DisplayMode::from_item(item) {
if let DisplayMode::LocalRecordings(store_id) = &display_mode {
store_hub.set_active_recording_id(store_id.clone());
}

Item::AppId(_)
| Item::DataSource(_)
| Item::InstancePath(_)
| Item::ComponentPath(_)
| Item::Container(_)
| Item::View(_)
| Item::DataResult(_, _) => {}
self.state.navigation.replace(display_mode);
}
}

Expand Down Expand Up @@ -1458,6 +1431,13 @@ impl App {
.send_system(SystemCommand::CloseAllEntries);
}

UICommand::NextRecording => {
re_recording_panel::RecordingPanelCommand::SelectNextRecording.send(egui_ctx);
}
UICommand::PreviousRecording => {
re_recording_panel::RecordingPanelCommand::SelectPreviousRecording.send(egui_ctx);
}

UICommand::Undo => {
if let Some(store_context) = store_context {
let blueprint_id = store_context.blueprint.store_id().clone();
Expand Down
5 changes: 5 additions & 0 deletions crates/viewer/re_viewer/src/app_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,11 @@ impl AppState {
self.share_modal
.ui(&ctx, ui, startup_options.web_viewer_base_url().as_ref());

re_recording_panel::RecordingPanelCommand::handle_recording_panel_commands(
&ctx,
&self.redap_servers,
);

// Only in integration tests: call the test hook if any.
#[cfg(feature = "testing")]
if let Some(test_hook) = self.test_hook.take() {
Expand Down
32 changes: 32 additions & 0 deletions crates/viewer/re_viewer_context/src/display_mode.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::Item;
use re_log_types::{StoreId, TableId};

/// Which display mode are we currently in?
Expand All @@ -23,3 +24,34 @@ pub enum DisplayMode {
/// The current recording's data store browser.
ChunkStoreBrowser,
}

impl DisplayMode {
pub fn item(&self) -> Option<Item> {
match self {
DisplayMode::LocalRecordings(store_id) => Some(Item::StoreId(store_id.clone())),
DisplayMode::LocalTable(table_id) => Some(Item::TableId(table_id.clone())),
DisplayMode::RedapEntry(entry_uri) => Some(Item::RedapEntry(entry_uri.clone())),
DisplayMode::RedapServer(origin) => Some(Item::RedapServer(origin.clone())),
DisplayMode::Settings | DisplayMode::Loading(_) | DisplayMode::ChunkStoreBrowser => {
None
}
}
}

pub fn from_item(item: &crate::Item) -> Option<Self> {
match item {
Item::StoreId(store_id) => Some(DisplayMode::LocalRecordings(store_id.clone())),
Item::TableId(table_id) => Some(DisplayMode::LocalTable(table_id.clone())),
Item::RedapEntry(entry_uri) => Some(DisplayMode::RedapEntry(entry_uri.clone())),
Item::RedapServer(origin) => Some(DisplayMode::RedapServer(origin.clone())),

Item::AppId(_)
| Item::DataSource(_)
| Item::InstancePath(_)
| Item::ComponentPath(_)
| Item::Container(_)
| Item::View(_)
| Item::DataResult(_, _) => None,
}
}
}
Loading