diff --git a/CHANGELOG.md b/CHANGELOG.md index 2771bf0f4a32..a94fc73a0c0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## [Unreleased](https://github.com/rerun-io/rerun/compare/latest...HEAD) +## [0.20.3](https://github.com/rerun-io/rerun/compare/0.20.2...0.20.3) - Web viewer fix + +### 🔎 Details + +#### 🪳 Bug fixes +- Fix web viewer feature flags [#8295](https://github.com/rerun-io/rerun/pull/8295) + ## [0.20.2](https://github.com/rerun-io/rerun/compare/0.20.1...0.20.2) - Build fix ### 🔎 Details diff --git a/Cargo.lock b/Cargo.lock index a5d7e947a821..7d4e35d51472 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -59,7 +59,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f47983a1084940ba9a39c077a8c63e55c619388be5476ac04c804cfbd1e63459" dependencies = [ "accesskit", - "hashbrown 0.15.0", + "hashbrown 0.15.1", "immutable-chunkmap", ] @@ -71,7 +71,7 @@ checksum = "7329821f3bd1101e03a7d2e03bd339e3ac0dc64c70b4c9f9ae1949e3ba8dece1" dependencies = [ "accesskit", "accesskit_consumer 0.26.0", - "hashbrown 0.15.0", + "hashbrown 0.15.1", "objc2", "objc2-app-kit", "objc2-foundation", @@ -103,7 +103,7 @@ checksum = "24fcd5d23d70670992b823e735e859374d694a3d12bfd8dd32bd3bd8bedb5d81" dependencies = [ "accesskit", "accesskit_consumer 0.26.0", - "hashbrown 0.15.0", + "hashbrown 0.15.1", "paste", "static_assertions", "windows 0.58.0", @@ -2998,9 +2998,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" dependencies = [ "allocator-api2", "equivalent", @@ -3409,7 +3409,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown 0.15.0", + "hashbrown 0.15.1", "serde", ] @@ -3826,7 +3826,7 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.15.0", + "hashbrown 0.15.1", ] [[package]] diff --git a/crates/viewer/re_space_view/src/lib.rs b/crates/viewer/re_space_view/src/lib.rs index e6620167efc8..c7e20d3ba711 100644 --- a/crates/viewer/re_space_view/src/lib.rs +++ b/crates/viewer/re_space_view/src/lib.rs @@ -30,7 +30,9 @@ pub use query::{ pub use results_ext::{ HybridLatestAtResults, HybridResults, HybridResultsChunkIter, RangeResultsExt, }; -pub use view_property_ui::view_property_ui; +pub use view_property_ui::{ + view_property_component_ui, view_property_component_ui_custom, view_property_ui, +}; pub mod external { pub use re_entity_db::external::*; diff --git a/crates/viewer/re_space_view/src/view_property_ui.rs b/crates/viewer/re_space_view/src/view_property_ui.rs index 5136494bc590..f9e174f7487d 100644 --- a/crates/viewer/re_space_view/src/view_property_ui.rs +++ b/crates/viewer/re_space_view/src/view_property_ui.rs @@ -1,14 +1,12 @@ -use re_chunk_store::{external::re_chunk::Arrow2Array, RowId}; use re_types_core::{ - reflection::{ArchetypeFieldReflection, ArchetypeReflection}, - Archetype, ArchetypeName, ArchetypeReflectionMarker, ComponentName, + reflection::ArchetypeFieldReflection, Archetype, ArchetypeReflectionMarker, ComponentName, }; use re_ui::{list_item, UiExt as _}; use re_viewer_context::{ ComponentFallbackProvider, ComponentUiTypes, QueryContext, SpaceViewId, SpaceViewState, ViewerContext, }; -use re_viewport_blueprint::entity_path_for_view_property; +use re_viewport_blueprint::ViewProperty; /// Display the UI for editing all components of a blueprint archetype. /// @@ -20,79 +18,49 @@ pub fn view_property_ui( fallback_provider: &dyn ComponentFallbackProvider, view_state: &dyn SpaceViewState, ) { - let name = A::name(); - if let Some(reflection) = ctx.reflection.archetypes.get(&name) { - view_property_ui_impl( - ctx, - ui, - view_id, - name, - reflection, - view_state, - fallback_provider, - ); - } else { - // The `ArchetypeReflectionMarker` bound should make this impossible. - re_log::warn_once!("Missing reflection data for archetype {name:?}."); - } + let view_property = + ViewProperty::from_archetype::(ctx.blueprint_db(), ctx.blueprint_query, view_id); + view_property_ui_impl(ctx, ui, &view_property, fallback_provider, view_state); } fn view_property_ui_impl( ctx: &ViewerContext<'_>, ui: &mut egui::Ui, - view_id: SpaceViewId, - name: ArchetypeName, - reflection: &ArchetypeReflection, - view_state: &dyn SpaceViewState, + property: &ViewProperty, fallback_provider: &dyn ComponentFallbackProvider, + view_state: &dyn SpaceViewState, ) { - let blueprint_path = entity_path_for_view_property(view_id, ctx.blueprint_db().tree(), name); - let query_ctx = QueryContext { - viewer_ctx: ctx, - target_entity_path: &blueprint_path, - archetype_name: Some(name), - query: ctx.blueprint_query, - view_state, - view_ctx: None, + let Some(reflection) = ctx.reflection.archetypes.get(&property.archetype_name) else { + // The `ArchetypeReflectionMarker` bound should make this impossible. + re_log::warn_once!( + "Missing reflection data for archetype {:?}.", + property.archetype_name + ); + return; }; - let component_results = ctx.blueprint_db().latest_at( - ctx.blueprint_query, - &blueprint_path, - reflection.fields.iter().map(|field| field.component_name), - ); - + let query_ctx = property.query_context(ctx, view_state); // If the property archetype only has a single component, don't show an additional hierarchy level! if reflection.fields.len() == 1 { let field = &reflection.fields[0]; - let component_array = component_results.component_batch_raw(&field.component_name); view_property_component_ui( &query_ctx, ui, - field.component_name, + property, reflection.display_name, field, - &blueprint_path, - component_results.component_row_id(&field.component_name), - component_array.as_deref(), fallback_provider, ); } else { let sub_prop_ui = |ui: &mut egui::Ui| { for field in &reflection.fields { - let display_name = &field.display_name; - - let component_array = component_results.component_batch_raw(&field.component_name); view_property_component_ui( &query_ctx, ui, - field.component_name, - display_name, + property, + field.display_name, field, - &blueprint_path, - component_results.component_row_id(&field.component_name), - component_array.as_deref(), fallback_provider, ); } @@ -102,7 +70,7 @@ fn view_property_ui_impl( .interactive(false) .show_hierarchical_with_children( ui, - ui.make_persistent_id(name.full_name()), + ui.make_persistent_id(property.archetype_name.full_name()), true, list_item::LabelContent::new(reflection.display_name), sub_prop_ui, @@ -111,36 +79,91 @@ fn view_property_ui_impl( } /// Draw view property ui for a single component of a view property archetype. -#[allow(clippy::too_many_arguments)] -fn view_property_component_ui( +/// +/// Use [`view_property_ui`] whenever possible to show the ui for all components of a view property archetype. +/// This function is only useful if you want to show custom ui for some of the components. +pub fn view_property_component_ui( ctx: &QueryContext<'_>, ui: &mut egui::Ui, - component_name: ComponentName, - root_item_display_name: &str, + property: &ViewProperty, + display_name: &str, field: &ArchetypeFieldReflection, - blueprint_path: &re_log_types::EntityPath, - row_id: Option, - component_array: Option<&dyn Arrow2Array>, fallback_provider: &dyn ComponentFallbackProvider, ) { - let singleline_list_item_content = singleline_list_item_content( - ctx, - root_item_display_name, - blueprint_path, - component_name, - row_id, - component_array, - fallback_provider, - ); + let component_array = property.component_raw(field.component_name); + let row_id = property.component_row_id(field.component_name); let ui_types = ctx .viewer_ctx .component_ui_registry - .registered_ui_types(component_name); + .registered_ui_types(field.component_name); + + let singleline_ui: &dyn Fn(&mut egui::Ui) = &|ui| { + ctx.viewer_ctx.component_ui_registry.singleline_edit_ui( + ctx, + ui, + ctx.viewer_ctx.blueprint_db(), + ctx.target_entity_path, + field.component_name, + row_id, + component_array.as_deref(), + fallback_provider, + ); + }; + + let multiline_ui: &dyn Fn(&mut egui::Ui) = &|ui| { + ctx.viewer_ctx.component_ui_registry.multiline_edit_ui( + ctx, + ui, + ctx.viewer_ctx.blueprint_db(), + ctx.target_entity_path, + field.component_name, + row_id, + component_array.as_deref(), + fallback_provider, + ); + }; + // Do this as a separate step to avoid borrowing issues. + let multiline_ui_ref: Option<&dyn Fn(&mut egui::Ui)> = + if ui_types.contains(ComponentUiTypes::MultiLineEditor) { + Some(multiline_ui) + } else { + None + }; - let list_item_response = if ui_types.contains(ComponentUiTypes::MultiLineEditor) { + view_property_component_ui_custom( + ctx, + ui, + property, + display_name, + field, + singleline_ui, + multiline_ui_ref, + ); +} + +/// Draw view property ui for a single component of a view property archetype with custom ui for singleline & multiline. +/// +/// Use [`view_property_ui`] whenever possible to show the ui for all components of a view property archetype. +/// This function is only useful if you want to show custom ui for some of the components. +pub fn view_property_component_ui_custom( + ctx: &QueryContext<'_>, + ui: &mut egui::Ui, + property: &ViewProperty, + display_name: &str, + field: &ArchetypeFieldReflection, + singleline_ui: &dyn Fn(&mut egui::Ui), + multiline_ui: Option<&dyn Fn(&mut egui::Ui)>, +) { + let singleline_list_item_content = list_item::PropertyContent::new(display_name) + .menu_button(&re_ui::icons::MORE, |ui| { + menu_more(ctx.viewer_ctx, ui, property, field.component_name); + }) + .value_fn(move |ui, _| singleline_ui(ui)); + + let list_item_response = if let Some(multiline_ui) = multiline_ui { let default_open = false; - let id = egui::Id::new((blueprint_path.hash(), component_name)); + let id = egui::Id::new((ctx.target_entity_path.hash(), field.component_name)); ui.list_item() .interactive(false) .show_hierarchical_with_children( @@ -149,16 +172,7 @@ fn view_property_component_ui( default_open, singleline_list_item_content, |ui| { - ctx.viewer_ctx.component_ui_registry.multiline_edit_ui( - ctx, - ui, - ctx.viewer_ctx.blueprint_db(), - blueprint_path, - component_name, - row_id, - component_array, - fallback_provider, - ); + multiline_ui(ui); }, ) .item_response @@ -177,14 +191,13 @@ fn view_property_component_ui( fn menu_more( ctx: &ViewerContext<'_>, ui: &mut egui::Ui, - blueprint_path: &re_log_types::EntityPath, + property: &ViewProperty, component_name: ComponentName, - component_array: Option<&dyn Arrow2Array>, ) { + let component_array = property.component_raw(component_name); + let property_differs_from_default = component_array - != ctx - .raw_latest_at_in_default_blueprint(blueprint_path, component_name) - .as_deref(); + != ctx.raw_latest_at_in_default_blueprint(&property.blueprint_store_path, component_name); let response = ui .add_enabled( @@ -199,7 +212,7 @@ If no default blueprint was set or it didn't set any value for this field, this "The property is already set to the same value it has in the default blueprint", ); if response.clicked() { - ctx.reset_blueprint_component_by_name(blueprint_path, component_name); + ctx.reset_blueprint_component_by_name(&property.blueprint_store_path, component_name); ui.close_menu(); } @@ -214,43 +227,10 @@ This has the same effect as not setting the value in the blueprint at all." ) .on_disabled_hover_text("The property is already unset."); if response.clicked() { - ctx.clear_blueprint_component_by_name(blueprint_path, component_name); + ctx.clear_blueprint_component_by_name(&property.blueprint_store_path, component_name); ui.close_menu(); } // TODO(andreas): The next logical thing here is now to save it to the default blueprint! // This should be fairly straight forward except that we need to make sure that a default blueprint exists in the first place. } - -fn singleline_list_item_content<'a>( - ctx: &'a QueryContext<'_>, - display_name: &str, - blueprint_path: &'a re_log_types::EntityPath, - component_name: ComponentName, - row_id: Option, - component_array: Option<&'a dyn Arrow2Array>, - fallback_provider: &'a dyn ComponentFallbackProvider, -) -> list_item::PropertyContent<'a> { - list_item::PropertyContent::new(display_name) - .menu_button(&re_ui::icons::MORE, move |ui| { - menu_more( - ctx.viewer_ctx, - ui, - blueprint_path, - component_name, - component_array, - ); - }) - .value_fn(move |ui, _| { - ctx.viewer_ctx.component_ui_registry.singleline_edit_ui( - ctx, - ui, - ctx.viewer_ctx.blueprint_db(), - blueprint_path, - component_name, - row_id, - component_array, - fallback_provider, - ); - }) -} diff --git a/crates/viewer/re_space_view_graph/src/graph/mod.rs b/crates/viewer/re_space_view_graph/src/graph/mod.rs index 8e6b29b7d47f..4ddb4c969087 100644 --- a/crates/viewer/re_space_view_graph/src/graph/mod.rs +++ b/crates/viewer/re_space_view_graph/src/graph/mod.rs @@ -114,7 +114,7 @@ impl Graph { nodes.push(Node::Implicit { id: edge.source_index, graph_node: edge.source.clone(), - label: DrawableLabel::implicit_circle(), + label: DrawableLabel::implicit_circle(ui), }); seen.insert(edge.source_index); } @@ -122,7 +122,7 @@ impl Graph { nodes.push(Node::Implicit { id: edge.target_index, graph_node: edge.target.clone(), - label: DrawableLabel::implicit_circle(), + label: DrawableLabel::implicit_circle(ui), }); seen.insert(edge.target_index); } diff --git a/crates/viewer/re_space_view_graph/src/layout/provider.rs b/crates/viewer/re_space_view_graph/src/layout/provider.rs index e6482ea1be1d..1036a2b20797 100644 --- a/crates/viewer/re_space_view_graph/src/layout/provider.rs +++ b/crates/viewer/re_space_view_graph/src/layout/provider.rs @@ -33,11 +33,26 @@ pub struct ForceLayoutProvider { impl ForceLayoutProvider { pub fn new(request: LayoutRequest) -> Self { + Self::new_impl(request, None) + } + + pub fn new_with_previous(request: LayoutRequest, layout: &Layout) -> Self { + Self::new_impl(request, Some(layout)) + } + + // TODO(grtlr): Consider consuming the old layout to avoid re-allocating the extents. + // That logic has to be revised when adding the blueprints anyways. + fn new_impl(request: LayoutRequest, layout: Option<&Layout>) -> Self { let nodes = request.graphs.iter().flat_map(|(_, graph_template)| { - graph_template - .nodes - .iter() - .map(|n| (n.0, fj::Node::from(n.1))) + graph_template.nodes.iter().map(|n| { + let mut fj_node = fj::Node::from(n.1); + if let Some(rect) = layout.and_then(|l| l.get_node(n.0)) { + let pos = rect.center(); + fj_node = fj_node.position(pos.x as f64, pos.y as f64); + } + + (n.0, fj_node) + }) }); let mut node_index = ahash::HashMap::default(); diff --git a/crates/viewer/re_space_view_graph/src/ui/draw.rs b/crates/viewer/re_space_view_graph/src/ui/draw.rs index e8db060fda71..39440e4bc069 100644 --- a/crates/viewer/re_space_view_graph/src/ui/draw.rs +++ b/crates/viewer/re_space_view_graph/src/ui/draw.rs @@ -56,10 +56,10 @@ impl DrawableLabel { Self::Circle(CircleLabel { radius, color }) } - pub fn implicit_circle() -> Self { + pub fn implicit_circle(ui: &Ui) -> Self { Self::Circle(CircleLabel { radius: 4.0, - color: None, + color: Some(ui.style().visuals.weak_text_color()), }) } diff --git a/crates/viewer/re_space_view_graph/src/ui/state.rs b/crates/viewer/re_space_view_graph/src/ui/state.rs index dad235b3586a..749e3720f937 100644 --- a/crates/viewer/re_space_view_graph/src/ui/state.rs +++ b/crates/viewer/re_space_view_graph/src/ui/state.rs @@ -104,15 +104,25 @@ impl LayoutState { self // no op } // We need to recompute the layout. - Self::None | Self::Finished { .. } => { + Self::None => { let provider = ForceLayoutProvider::new(new_request); let layout = provider.init(); Self::InProgress { layout, provider } } - Self::InProgress { provider, .. } if provider.request != new_request => { - let provider = ForceLayoutProvider::new(new_request); - let layout = provider.init(); + Self::Finished { layout, .. } => { + let mut provider = ForceLayoutProvider::new_with_previous(new_request, &layout); + let mut layout = provider.init(); + provider.tick(&mut layout); + + Self::InProgress { layout, provider } + } + Self::InProgress { + layout, provider, .. + } if provider.request != new_request => { + let mut provider = ForceLayoutProvider::new_with_previous(new_request, &layout); + let mut layout = provider.init(); + provider.tick(&mut layout); Self::InProgress { layout, provider } } diff --git a/crates/viewer/re_viewport_blueprint/src/view_properties.rs b/crates/viewer/re_viewport_blueprint/src/view_properties.rs index 2216e84555a1..1accc8427470 100644 --- a/crates/viewer/re_viewport_blueprint/src/view_properties.rs +++ b/crates/viewer/re_viewport_blueprint/src/view_properties.rs @@ -33,10 +33,17 @@ pub struct ViewProperty { /// stored. pub blueprint_store_path: EntityPath, - archetype_name: ArchetypeName, - component_names: Vec, - query_results: LatestAtResults, - blueprint_query: LatestAtQuery, + /// Name of the property archetype. + pub archetype_name: ArchetypeName, + + /// List of all components in this property. + pub component_names: Vec, + + /// Query results for all queries of this property. + pub query_results: LatestAtResults, + + /// Blueprint query used for querying. + pub blueprint_query: LatestAtQuery, } impl ViewProperty { diff --git a/tests/python/release_checklist/check_graph_time_layout.py b/tests/python/release_checklist/check_graph_time_layout.py new file mode 100644 index 000000000000..3d469e1b72c5 --- /dev/null +++ b/tests/python/release_checklist/check_graph_time_layout.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +import os +import random +from argparse import Namespace +from uuid import uuid4 + +import rerun as rr +import rerun.blueprint as rrb + +README = """\ +# Time-varying graph view + +Please watch out for any twitching, jumping, or other wise unexpected changes to +the layout when navigating the timeline. + +Please check the following: +* Scrub the timeline to see how the graph layout changes over time. +""" + + +def log_readme() -> None: + rr.log("readme", rr.TextDocument(README, media_type=rr.MediaType.MARKDOWN), static=True) + + +def log_graphs() -> None: + nodes = ["root"] + edges = [] + + # Randomly add nodes and edges to the graph + for i in range(50): + existing = random.choice(nodes) + new_node = str(i) + nodes.append(new_node) + edges.append((existing, new_node)) + + rr.set_time_sequence("frame", i) + rr.log("graph", rr.GraphNodes(nodes, labels=nodes), rr.GraphEdges(edges, graph_type=rr.GraphType.Directed)) + + rr.send_blueprint( + rrb.Blueprint( + rrb.Grid( + rrb.GraphView(origin="graph", name="Graph"), + rrb.TextDocumentView(origin="readme", name="Instructions"), + ) + ) + ) + + +def run(args: Namespace) -> None: + rr.script_setup(args, f"{os.path.basename(__file__)}", recording_id=uuid4()) + + log_readme() + log_graphs() + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description="Interactive release checklist") + rr.script_add_args(parser) + args = parser.parse_args() + run(args)