Skip to content

Commit 3b756d9

Browse files
authored
Add UrlContext which handles getting information for ViewerOpenUrl (#11183)
1 parent 5f59f7b commit 3b756d9

File tree

11 files changed

+230
-220
lines changed

11 files changed

+230
-220
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8993,7 +8993,6 @@ dependencies = [
89938993
"itertools 0.14.0",
89948994
"js-sys",
89958995
"parking_lot",
8996-
"percent-encoding",
89978996
"poll-promise",
89988997
"re_analytics",
89998998
"re_auth",
@@ -9107,6 +9106,7 @@ dependencies = [
91079106
"re_types",
91089107
"re_types_core",
91099108
"re_ui",
9109+
"re_uri",
91109110
"re_video",
91119111
"serde",
91129112
"slotmap",

crates/store/re_log_types/src/index/absolute_time_range.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,12 @@ impl AbsoluteTimeRangeF {
203203
pub fn length(&self) -> TimeReal {
204204
self.max - self.min
205205
}
206+
207+
/// Creates an [`AbsoluteTimeRange`] from self by rounding the start
208+
/// of the range down, and rounding the end of the range up.
209+
pub fn to_int(self) -> AbsoluteTimeRange {
210+
AbsoluteTimeRange::new(self.min.floor(), self.max.ceil())
211+
}
206212
}
207213

208214
impl From<AbsoluteTimeRangeF> for RangeInclusive<TimeReal> {

crates/viewer/re_global_context/src/command_sender.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,13 @@ pub enum SystemCommand {
9696
fragment: re_uri::Fragment,
9797
},
9898

99+
/// Copies an url to the clipboard to the given a display mode, selection range, and url fragment.
100+
CopyUrlWithContext {
101+
display_mode: crate::DisplayMode,
102+
time_range: Option<re_uri::TimeSelection>,
103+
fragment: re_uri::Fragment,
104+
},
105+
99106
/// Set the item selection.
100107
SetSelection(crate::ItemCollection),
101108

crates/viewer/re_test_context/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,9 @@ impl TestContext {
588588
// This adds new system commands, which will be handled later in the loop.
589589
self.go_to_dataset_data(store_id, fragment);
590590
}
591+
SystemCommand::CopyUrlWithContext { .. } => {
592+
// Ignore copying to clipboard here.
593+
}
591594
SystemCommand::AppendToStore(store_id, chunks) => {
592595
let store_hub = self.store_hub.get_mut();
593596
let db = store_hub.entity_db_mut(&store_id);

crates/viewer/re_viewer/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,6 @@ re_perf_telemetry = { workspace = true, features = ["tracy"], optional = true }
151151
# web dependencies:
152152
[target.'cfg(target_arch = "wasm32")'.dependencies]
153153
js-sys.workspace = true
154-
percent-encoding.workspace = true
155154
strum.workspace = true
156155
strum_macros.workspace = true
157156
wasm-bindgen-futures.workspace = true

crates/viewer/re_viewer/src/app.rs

Lines changed: 51 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ use re_ui::{ContextExt as _, UICommand, UICommandSender as _, UiExt as _, notifi
1515
use re_viewer_context::{
1616
AppOptions, AsyncRuntimeHandle, BlueprintUndoState, CommandReceiver, CommandSender,
1717
ComponentUiRegistry, DisplayMode, Item, PlayState, RecordingConfig, RecordingOrTable,
18-
StorageContext, StoreContext, SystemCommand, SystemCommandSender as _, TableStore, ViewClass,
19-
ViewClassRegistry, ViewClassRegistryError, command_channel, santitize_file_name,
18+
StorageContext, StoreContext, SystemCommand, SystemCommandSender as _, TableStore, UrlContext,
19+
ViewClass, ViewClassRegistry, ViewClassRegistryError, command_channel, santitize_file_name,
2020
store_hub::{BlueprintPersistence, StoreHub, StoreHubStats},
2121
};
2222

@@ -555,25 +555,19 @@ impl App {
555555
let time_ctrl = rec_cfg.as_ref().map(|cfg| cfg.time_ctrl.read());
556556

557557
let display_mode = self.state.navigation.peek();
558-
let selection = self.state.selection_state.selected_items().first_item();
558+
let selection = self.state.selection_state.selected_items();
559559

560-
let Ok(url) = crate::open_url::ViewerOpenUrl::from_display_mode(
560+
let Ok(url) = crate::open_url::ViewerOpenUrl::new(
561561
store_hub,
562-
display_mode.clone(),
563-
&re_uri::Fragment {
564-
selection: selection.and_then(|item| item.to_data_path()),
565-
when: time_ctrl
566-
.filter(|time_ctrl| matches!(time_ctrl.play_state(), PlayState::Paused))
567-
.and_then(|when| {
568-
Some((
569-
*when.timeline().name(),
570-
re_log_types::TimeCell {
571-
typ: when.timeline().typ(),
572-
value: when.time_int()?.into(),
573-
},
574-
))
575-
}),
576-
},
562+
UrlContext::from_context_expanded(
563+
display_mode,
564+
time_ctrl
565+
.as_deref()
566+
// Only update `when` fragment when paused.
567+
.filter(|time_ctrl| matches!(time_ctrl.play_state(), PlayState::Paused)),
568+
selection,
569+
)
570+
.without_time_range(),
577571
)
578572
// History entries expect the url parameter, not the full url, therefore don't pass a base url.
579573
.and_then(|url| url.sharable_url(None)) else {
@@ -625,6 +619,20 @@ impl App {
625619
// This adds new system commands, which will be handled later in the loop.
626620
self.go_to_dataset_data(store_id, fragment);
627621
}
622+
SystemCommand::CopyUrlWithContext {
623+
display_mode,
624+
time_range,
625+
fragment,
626+
} => {
627+
self.run_copy_link_command(
628+
store_hub,
629+
UrlContext {
630+
display_mode,
631+
time_range,
632+
fragment,
633+
},
634+
);
635+
}
628636
SystemCommand::ActivateApp(app_id) => {
629637
self.state.navigation.replace(DisplayMode::LocalRecordings);
630638
store_hub.set_active_app(app_id);
@@ -1480,11 +1488,30 @@ impl App {
14801488
}
14811489

14821490
UICommand::CopyDirectLink => {
1483-
self.run_copy_direct_link_command(storage_context, display_mode);
1491+
self.run_copy_link_command(
1492+
storage_context.hub,
1493+
UrlContext::new(display_mode.clone()),
1494+
);
14841495
}
14851496

14861497
UICommand::CopyTimeRangeLink => {
1487-
self.run_copy_time_range_link_command(store_context);
1498+
let mut url_context = UrlContext::new(display_mode.clone());
1499+
1500+
let rec_cfg = storage_context
1501+
.hub
1502+
.active_recording()
1503+
.and_then(|db| self.state.recording_config(db.store_id()));
1504+
let time_ctrl = rec_cfg.as_ref().map(|cfg| cfg.time_ctrl.read());
1505+
1506+
if let Some(time_ctrl) = &time_ctrl {
1507+
url_context = url_context.with_time_range(time_ctrl);
1508+
} else {
1509+
re_log::warn!("No timeline in current mode");
1510+
}
1511+
1512+
drop(time_ctrl);
1513+
1514+
self.run_copy_link_command(storage_context.hub, url_context);
14881515
}
14891516

14901517
#[cfg(target_arch = "wasm32")]
@@ -1605,41 +1632,7 @@ impl App {
16051632
}
16061633
}
16071634

1608-
/// Retrieve the link to the current viewer.
1609-
#[cfg(target_arch = "wasm32")]
1610-
fn get_viewer_url(&self) -> Result<String, wasm_bindgen::JsValue> {
1611-
let location = web_sys::window()
1612-
.ok_or_else(|| "failed to get window".to_owned())?
1613-
.location();
1614-
let origin = location.origin()?;
1615-
let host = location.host()?;
1616-
let pathname = location.pathname()?;
1617-
1618-
let hosted_viewer_path = if self.build_info.is_final() {
1619-
// final release, use version tag
1620-
format!("version/{}", self.build_info.version)
1621-
} else {
1622-
// not a final release, use commit hash
1623-
format!("commit/{}", self.build_info.short_git_hash())
1624-
};
1625-
1626-
// links to `app.rerun.io` can be made into permanent links:
1627-
let url = if host == "app.rerun.io" {
1628-
format!("https://app.rerun.io/{hosted_viewer_path}")
1629-
} else if host == "rerun.io" && pathname.starts_with("/viewer") {
1630-
format!("https://rerun.io/viewer/{hosted_viewer_path}")
1631-
} else {
1632-
format!("{origin}{pathname}")
1633-
};
1634-
1635-
Ok(url)
1636-
}
1637-
1638-
fn run_copy_direct_link_command(
1639-
&mut self,
1640-
storage_context: &StorageContext<'_>,
1641-
display_mode: &DisplayMode,
1642-
) {
1635+
fn run_copy_link_command(&mut self, store_hub: &StoreHub, context: UrlContext) {
16431636
// TODO(rerun-io/dataplatform#2663): Should take into account dataplatform URLs if any are provided.
16441637
let base_url;
16451638
#[cfg(target_arch = "wasm32")]
@@ -1652,12 +1645,8 @@ impl App {
16521645
base_url = None;
16531646
};
16541647

1655-
match crate::open_url::ViewerOpenUrl::from_display_mode(
1656-
storage_context.hub,
1657-
display_mode.clone(),
1658-
&re_uri::Fragment::default(),
1659-
)
1660-
.and_then(|content_url| content_url.sharable_url(base_url.as_ref()))
1648+
match crate::open_url::ViewerOpenUrl::new(store_hub, context)
1649+
.and_then(|content_url| content_url.sharable_url(base_url.as_ref()))
16611650
{
16621651
Ok(url) => {
16631652
self.egui_ctx.copy_text(url);
@@ -1670,71 +1659,6 @@ impl App {
16701659
}
16711660
}
16721661

1673-
fn run_copy_time_range_link_command(&mut self, store_context: Option<&StoreContext<'_>>) {
1674-
let Some(entity_db) = store_context.as_ref().map(|ctx| ctx.recording) else {
1675-
re_log::warn!("Could not copy time range link: No active recording");
1676-
return;
1677-
};
1678-
1679-
let Some(SmartChannelSource::RedapGrpcStream { mut uri, .. }) =
1680-
entity_db.data_source.clone()
1681-
else {
1682-
re_log::warn!("Could not copy time range link: Data source is not a gRPC stream");
1683-
return;
1684-
};
1685-
1686-
let rec_cfg = self.state.recording_config_mut(entity_db);
1687-
let time_ctrl = rec_cfg.time_ctrl.get_mut();
1688-
1689-
let Some(range) = time_ctrl.loop_selection() else {
1690-
// no loop selection
1691-
re_log::warn!(
1692-
"Could not copy time range link: No loop selection set. Use shift to drag a selection on the timeline"
1693-
);
1694-
return;
1695-
};
1696-
1697-
uri.time_range = Some(re_uri::TimeSelection {
1698-
timeline: *time_ctrl.timeline(),
1699-
range: re_log_types::AbsoluteTimeRange::new(range.min.floor(), range.max.ceil()),
1700-
});
1701-
1702-
// On web we can produce a link to the web viewer,
1703-
// which can be used to share the time range.
1704-
//
1705-
// On native we only produce a link to the time range
1706-
// which can be passed to `rerun-cli`.
1707-
#[cfg(target_arch = "wasm32")]
1708-
let url = {
1709-
use crate::web_tools::JsResultExt as _;
1710-
let Some(viewer_url) = self.get_viewer_url().ok_or_log_js_error() else {
1711-
// error was logged already
1712-
return;
1713-
};
1714-
1715-
let time_range_url = uri.to_string();
1716-
// %-encode the time range URL, because it's a url-within-a-url.
1717-
// This results in VERY ugly links.
1718-
// TODO(jan): Tweak the asciiset used here.
1719-
// Alternatively, use a better (shorter, simpler) format
1720-
// for linking to recordings that isn't a full url and
1721-
// can actually exist in a query value.
1722-
let url_query = percent_encoding::utf8_percent_encode(
1723-
&time_range_url,
1724-
percent_encoding::NON_ALPHANUMERIC,
1725-
);
1726-
1727-
format!("{viewer_url}?url={url_query}")
1728-
};
1729-
1730-
#[cfg(not(target_arch = "wasm32"))]
1731-
let url = uri.to_string();
1732-
1733-
self.egui_ctx.copy_text(url.clone());
1734-
self.notifications
1735-
.success(format!("Copied {url:?} to clipboard"));
1736-
}
1737-
17381662
fn copy_entity_hierarchy_to_clipboard(
17391663
&mut self,
17401664
egui_ctx: &egui::Context,

crates/viewer/re_viewer/src/app_state.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -690,7 +690,6 @@ impl AppState {
690690
self.focused_item = None;
691691
}
692692

693-
#[cfg(target_arch = "wasm32")] // Only used in Wasm
694693
pub fn recording_config(&self, rec_id: &StoreId) -> Option<&RecordingConfig> {
695694
self.recording_configs.get(rec_id)
696695
}

0 commit comments

Comments
 (0)