@@ -15,8 +15,8 @@ use re_ui::{ContextExt as _, UICommand, UICommandSender as _, UiExt as _, notifi
15
15
use re_viewer_context:: {
16
16
AppOptions , AsyncRuntimeHandle , BlueprintUndoState , CommandReceiver , CommandSender ,
17
17
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,
20
20
store_hub:: { BlueprintPersistence , StoreHub , StoreHubStats } ,
21
21
} ;
22
22
@@ -555,25 +555,19 @@ impl App {
555
555
let time_ctrl = rec_cfg. as_ref ( ) . map ( |cfg| cfg. time_ctrl . read ( ) ) ;
556
556
557
557
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 ( ) ;
559
559
560
- let Ok ( url) = crate :: open_url:: ViewerOpenUrl :: from_display_mode (
560
+ let Ok ( url) = crate :: open_url:: ViewerOpenUrl :: new (
561
561
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 ( ) ,
577
571
)
578
572
// History entries expect the url parameter, not the full url, therefore don't pass a base url.
579
573
. and_then ( |url| url. sharable_url ( None ) ) else {
@@ -625,6 +619,20 @@ impl App {
625
619
// This adds new system commands, which will be handled later in the loop.
626
620
self . go_to_dataset_data ( store_id, fragment) ;
627
621
}
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
+ }
628
636
SystemCommand :: ActivateApp ( app_id) => {
629
637
self . state . navigation . replace ( DisplayMode :: LocalRecordings ) ;
630
638
store_hub. set_active_app ( app_id) ;
@@ -1480,11 +1488,30 @@ impl App {
1480
1488
}
1481
1489
1482
1490
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
+ ) ;
1484
1495
}
1485
1496
1486
1497
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) ;
1488
1515
}
1489
1516
1490
1517
#[ cfg( target_arch = "wasm32" ) ]
@@ -1605,41 +1632,7 @@ impl App {
1605
1632
}
1606
1633
}
1607
1634
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 ) {
1643
1636
// TODO(rerun-io/dataplatform#2663): Should take into account dataplatform URLs if any are provided.
1644
1637
let base_url;
1645
1638
#[ cfg( target_arch = "wasm32" ) ]
@@ -1652,12 +1645,8 @@ impl App {
1652
1645
base_url = None ;
1653
1646
} ;
1654
1647
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 ( ) ) )
1661
1650
{
1662
1651
Ok ( url) => {
1663
1652
self . egui_ctx . copy_text ( url) ;
@@ -1670,71 +1659,6 @@ impl App {
1670
1659
}
1671
1660
}
1672
1661
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
-
1738
1662
fn copy_entity_hierarchy_to_clipboard (
1739
1663
& mut self ,
1740
1664
egui_ctx : & egui:: Context ,
0 commit comments