diff --git a/Cargo.lock b/Cargo.lock index 0860ec1d83481..5605710a43461 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5271,6 +5271,7 @@ dependencies = [ "anyhow", "arboard", "bit-vec", + "bitflags 2.5.0", "bytemuck", "egui", "egui-wgpu", diff --git a/crates/re_data_ui/src/component_ui_registry.rs b/crates/re_data_ui/src/component_ui_registry.rs index 1a28068bcbe3f..34a41bf95c83b 100644 --- a/crates/re_data_ui/src/component_ui_registry.rs +++ b/crates/re_data_ui/src/component_ui_registry.rs @@ -36,7 +36,7 @@ pub fn create_component_ui_registry() -> ComponentUiRegistry { /// Registers how to show a given component in the ui. pub fn add_to_registry(registry: &mut ComponentUiRegistry) { - registry.add( + registry.add_display_ui( C::name(), Box::new( |ctx, ui, ui_layout, query, db, entity_path, component, instance| { diff --git a/crates/re_edit_ui/src/lib.rs b/crates/re_edit_ui/src/lib.rs index f979cfc64f0bd..829f9370cb135 100644 --- a/crates/re_edit_ui/src/lib.rs +++ b/crates/re_edit_ui/src/lib.rs @@ -37,20 +37,25 @@ fn edit_color_ui(_ctx: &ViewerContext<'_>, ui: &mut egui::Ui, value: &mut Color) /// ⚠️ This is supposed to be the only export of this crate. /// This crate is meant to be a leaf crate in the viewer ecosystem and should only be used by the `re_viewer` crate itself. pub fn register_editors(registry: &mut re_viewer_context::ComponentUiRegistry) { - registry.add_editor(edit_color_ui); - registry.add_editor(marker_shape::edit_marker_shape_ui); - registry.add_editor(visual_bounds_2d::edit_visual_bounds_2d); + registry.add_singleline_editor_ui(edit_color_ui); + registry.add_singleline_editor_ui(marker_shape::edit_marker_shape_ui); - registry.add_editor::(datatype_editors::edit_bool); + registry.add_singleline_editor_ui::(datatype_editors::edit_bool); - registry.add_editor::(datatype_editors::edit_singleline_string); - registry.add_editor::(datatype_editors::edit_singleline_string); + registry.add_singleline_editor_ui::(datatype_editors::edit_singleline_string); + registry.add_singleline_editor_ui::(datatype_editors::edit_singleline_string); - registry.add_editor::(datatype_editors::edit_f32_zero_to_inf); - registry.add_editor::(datatype_editors::edit_f32_zero_to_inf); - registry.add_editor::(datatype_editors::edit_f32_zero_to_inf); + registry.add_singleline_editor_ui::(datatype_editors::edit_f32_zero_to_inf); + registry.add_singleline_editor_ui::(datatype_editors::edit_f32_zero_to_inf); + registry.add_singleline_editor_ui::(datatype_editors::edit_f32_zero_to_inf); - registry.add_editor(|_ctx, ui, value| edit_enum(ui, "corner2d", value, &Corner2D::ALL)); - registry - .add_editor(|_ctx, ui, value| edit_enum(ui, "backgroundkind", value, &BackgroundKind::ALL)); + registry.add_singleline_editor_ui(|_ctx, ui, value| { + edit_enum(ui, "corner2d", value, &Corner2D::ALL) + }); + registry.add_singleline_editor_ui(|_ctx, ui, value| { + edit_enum(ui, "backgroundkind", value, &BackgroundKind::ALL) + }); + + registry.add_multiline_editor_ui(visual_bounds_2d::multiline_edit_visual_bounds_2d); + registry.add_singleline_editor_ui(visual_bounds_2d::singleline_edit_visual_bounds_2d); } diff --git a/crates/re_edit_ui/src/visual_bounds_2d.rs b/crates/re_edit_ui/src/visual_bounds_2d.rs index bb59f05e187f1..79c987c870abc 100644 --- a/crates/re_edit_ui/src/visual_bounds_2d.rs +++ b/crates/re_edit_ui/src/visual_bounds_2d.rs @@ -1,51 +1,137 @@ use egui::NumExt as _; -use re_types::blueprint::components::VisualBounds2D; +use re_types::{blueprint::components::VisualBounds2D, datatypes::Range2D}; use re_viewer_context::ViewerContext; -pub fn edit_visual_bounds_2d( - _ctx: &ViewerContext<'_>, +pub fn multiline_edit_visual_bounds_2d( + ctx: &ViewerContext<'_>, ui: &mut egui::Ui, value: &mut VisualBounds2D, ) -> egui::Response { let speed_func = |start: f64, end: f64| ((end - start).abs() * 0.01).at_least(0.001); - ui.vertical(|ui| { - ui.horizontal(|ui| { - let [x_range_start, x_range_end] = &mut value.x_range.0; - let speed = speed_func(*x_range_start, *x_range_end); - - ui.label("x"); - ui.add( - egui::DragValue::new(x_range_start) - .clamp_range(f64::NEG_INFINITY..=*x_range_end) - .max_decimals(2) - .speed(speed), - ) | ui.add( - egui::DragValue::new(x_range_end) - .clamp_range(*x_range_start..=f64::INFINITY) - .max_decimals(2) - .speed(speed), - ) - }) - .inner - | ui.horizontal(|ui| { + let mut any_edit = false; + + let response_x = re_ui::list_item::ListItem::new(ctx.re_ui) + .interactive(false) + .show_hierarchical( + ui, + re_ui::list_item::PropertyContent::new("x").value_fn(|_, ui, _| { + let [x_range_start, x_range_end] = &mut value.x_range.0; + let speed = speed_func(*x_range_start, *x_range_end); + + let response = ui + .horizontal_centered(|ui| { + let response_min = ui.add( + egui::DragValue::new(x_range_start) + .clamp_range(f64::NEG_INFINITY..=*x_range_end) + .max_decimals(2) + .speed(speed), + ); + + ui.label("-"); + + let response_max = ui.add( + egui::DragValue::new(x_range_end) + .clamp_range(*x_range_start..=f64::INFINITY) + .max_decimals(2) + .speed(speed), + ); + + response_min | response_max + }) + .inner; + + if response.changed() { + any_edit = true; + } + }), + ); + + let response_y = re_ui::list_item::ListItem::new(ctx.re_ui) + .interactive(false) + .show_hierarchical( + ui, + re_ui::list_item::PropertyContent::new("y").value_fn(|_, ui, _| { let [y_range_start, y_range_end] = &mut value.y_range.0; let speed = speed_func(*y_range_start, *y_range_end); - ui.label("y"); - ui.add( - egui::DragValue::new(y_range_start) - .clamp_range(f64::NEG_INFINITY..=*y_range_end) - .max_decimals(2) - .speed(speed), - ) | ui.add( - egui::DragValue::new(y_range_end) - .clamp_range(*y_range_start..=f64::INFINITY) - .max_decimals(2) - .speed(speed), - ) - }) - .inner - }) - .inner + let response = ui + .horizontal_centered(|ui| { + let response_min = ui.add( + egui::DragValue::new(y_range_start) + .clamp_range(f64::NEG_INFINITY..=*y_range_end) + .max_decimals(2) + .speed(speed), + ); + + ui.label("-"); + + let response_max = ui.add( + egui::DragValue::new(y_range_end) + .clamp_range(*y_range_start..=f64::INFINITY) + .max_decimals(2) + .speed(speed), + ); + + response_min | response_max + }) + .inner; + + if response.changed() { + any_edit = true; + } + }), + ); + + let mut response = response_x | response_y; + if any_edit { + response.mark_changed(); + } + response +} + +pub fn singleline_edit_visual_bounds_2d( + _ctx: &ViewerContext<'_>, + ui: &mut egui::Ui, + value: &mut VisualBounds2D, +) -> egui::Response { + let width = value.x_range.0[1] - value.x_range.0[0]; + let height = value.y_range.0[1] - value.y_range.0[0]; + + let mut width_edit = width; + let mut height_edit = height; + + let speed_func = |v: f64| (v.abs() * 0.01).at_least(0.001); + + let response_width = ui.add( + egui::DragValue::new(&mut width_edit) + .clamp_range(f64::NEG_INFINITY..=f64::INFINITY) + .max_decimals(1) + .speed(speed_func(width)), + ); + ui.label("×"); + let response_height = ui.add( + egui::DragValue::new(&mut height_edit) + .clamp_range(f64::NEG_INFINITY..=f64::INFINITY) + .max_decimals(1) + .speed(speed_func(height)), + ); + + let d_width = width_edit - width; + let d_height = height_edit - height; + *value = Range2D { + x_range: [ + value.x_range.0[0] - d_width * 0.5, + value.x_range.0[1] + d_width * 0.5, + ] + .into(), + y_range: [ + value.y_range.0[0] - d_height * 0.5, + value.y_range.0[1] + d_height * 0.5, + ] + .into(), + } + .into(); + + response_height | response_width } diff --git a/crates/re_query/src/latest_at/helpers.rs b/crates/re_query/src/latest_at/helpers.rs index 523895f90aa39..29dfc1751bad1 100644 --- a/crates/re_query/src/latest_at/helpers.rs +++ b/crates/re_query/src/latest_at/helpers.rs @@ -60,6 +60,14 @@ impl LatestAtComponentResults { } } + /// Returns true if the component is missing, an empty array or still pending. + pub fn is_empty(&self, resolver: &PromiseResolver) -> bool { + match self.resolved(resolver) { + PromiseResult::Ready(cell) => cell.is_empty(), + PromiseResult::Error(_) | PromiseResult::Pending => true, + } + } + /// Returns the component data of the single instance. /// /// This assumes that the row we get from the store contains at most one instance for this diff --git a/crates/re_query/src/latest_at/results.rs b/crates/re_query/src/latest_at/results.rs index c1a2489f7cfc1..0d1c7d8ea3778 100644 --- a/crates/re_query/src/latest_at/results.rs +++ b/crates/re_query/src/latest_at/results.rs @@ -46,11 +46,6 @@ impl LatestAtResults { self.components.contains_key(&component_name.into()) } - pub fn contains_non_empty(&self, component_name: impl Into) -> bool { - self.get(component_name) - .map_or(false, |result| result.num_instances() != 0) - } - /// Returns the [`LatestAtComponentResults`] for the specified [`Component`]. #[inline] pub fn get( diff --git a/crates/re_selection_panel/src/override_ui.rs b/crates/re_selection_panel/src/override_ui.rs index e4b34319b4278..03e2e6e37b01e 100644 --- a/crates/re_selection_panel/src/override_ui.rs +++ b/crates/re_selection_panel/src/override_ui.rs @@ -7,8 +7,8 @@ use re_entity_db::{EntityDb, InstancePath}; use re_log_types::{DataCell, DataRow, RowId, StoreKind}; use re_types_core::{components::VisualizerOverrides, ComponentName}; use re_viewer_context::{ - DataResult, OverridePath, QueryContext, SpaceViewClassExt as _, SystemCommand, - SystemCommandSender as _, UiLayout, ViewSystemIdentifier, ViewerContext, + ComponentUiTypes, DataResult, OverridePath, QueryContext, SpaceViewClassExt as _, + SystemCommand, SystemCommandSender as _, ViewSystemIdentifier, ViewerContext, }; use re_viewport_blueprint::SpaceViewBlueprint; @@ -133,18 +133,16 @@ pub fn override_ui( .cloned(); /* arc */ if let Some(results) = component_data { - ctx.component_ui_registry.edit_ui( + ctx.component_ui_registry.singleline_edit_ui( &QueryContext { viewer_ctx: ctx, - target_entity_path: entity_path_overridden, + target_entity_path: &instance_path.entity_path, archetype_name: None, query: &query, view_state, }, ui, - UiLayout::List, origin_db, - &instance_path.entity_path, entity_path_overridden, *component_name, &results, @@ -227,7 +225,12 @@ pub fn add_new_override( }; // If there is no registered editor, don't let the user create an override - if !ctx.component_ui_registry.has_registered_editor(component) { + // TODO(andreas): Can only handle single line editors right now. + if !ctx + .component_ui_registry + .registered_ui_types(*component) + .contains(ComponentUiTypes::SingleLineEditor) + { continue; } diff --git a/crates/re_selection_panel/src/query_range_ui.rs b/crates/re_selection_panel/src/query_range_ui.rs index 7cab9a6bb537b..0d80ce22389f3 100644 --- a/crates/re_selection_panel/src/query_range_ui.rs +++ b/crates/re_selection_panel/src/query_range_ui.rs @@ -9,7 +9,7 @@ use re_space_view_spatial::{SpatialSpaceView2D, SpatialSpaceView3D}; use re_space_view_time_series::TimeSeriesSpaceView; use re_types::{ datatypes::{TimeInt, TimeRange, TimeRangeBoundary}, - SpaceViewClassIdentifier, + Archetype, SpaceViewClassIdentifier, }; use re_ui::{markdown_ui, ReUi}; use re_viewer_context::{QueryRange, SpaceViewClass, ViewerContext}; @@ -43,9 +43,11 @@ pub fn query_range_ui_space_view( return; } - let property_path = entity_path_for_view_property::< - re_types::blueprint::archetypes::VisibleTimeRanges, - >(space_view.id, ctx.store_context.blueprint.tree()); + let property_path = entity_path_for_view_property( + space_view.id, + ctx.store_context.blueprint.tree(), + re_types::blueprint::archetypes::VisibleTimeRanges::name(), + ); let query_range = space_view.query_range( ctx.store_context.blueprint, diff --git a/crates/re_space_view/src/view_property_ui.rs b/crates/re_space_view/src/view_property_ui.rs index 228074fb4d922..a4252ed143e37 100644 --- a/crates/re_space_view/src/view_property_ui.rs +++ b/crates/re_space_view/src/view_property_ui.rs @@ -1,39 +1,80 @@ +use std::borrow::Cow; + use ahash::HashMap; -use re_types_core::Archetype; +use re_types_core::{Archetype, ArchetypeFieldInfo, ComponentName}; use re_ui::list_item; -use re_viewer_context::{ComponentFallbackProvider, QueryContext, SpaceViewId, ViewerContext}; +use re_viewer_context::{ + ComponentFallbackProvider, ComponentUiTypes, QueryContext, SpaceViewId, SpaceViewState, + ViewerContext, +}; use re_viewport_blueprint::entity_path_for_view_property; +// Utility struct to make argument passing less excessive. +// TODO(andreas): This could be actually useful to be a public struct on the archetype. +struct ArchetypeInfo { + name: re_types_core::ArchetypeName, + display_name: &'static str, + component_names: Cow<'static, [ComponentName]>, + field_infos: Option>, +} + +impl ArchetypeInfo { + fn new() -> Self { + Self { + name: A::name(), + display_name: A::display_name(), + component_names: A::all_components(), + field_infos: A::field_infos(), + } + } +} + /// Display the UI for editing all components of a blueprint archetype. /// /// Note that this will show default values for components that are null. pub fn view_property_ui( ctx: &ViewerContext<'_>, ui: &mut egui::Ui, - space_view_id: SpaceViewId, + view_id: SpaceViewId, fallback_provider: &dyn ComponentFallbackProvider, - view_state: &dyn re_viewer_context::SpaceViewState, + view_state: &dyn SpaceViewState, ) { - let blueprint_db = ctx.store_context.blueprint; - let blueprint_query = ctx.blueprint_query; - let blueprint_path = entity_path_for_view_property::(space_view_id, blueprint_db.tree()); + view_property_ui_impl( + ctx, + ui, + view_id, + ArchetypeInfo::new::(), + view_state, + fallback_provider, + ); +} +fn view_property_ui_impl( + ctx: &ViewerContext<'_>, + ui: &mut egui::Ui, + view_id: SpaceViewId, + archetype: ArchetypeInfo, + view_state: &dyn SpaceViewState, + fallback_provider: &dyn ComponentFallbackProvider, +) { + let blueprint_path = + entity_path_for_view_property(view_id, ctx.blueprint_db().tree(), archetype.name); let query_ctx = QueryContext { viewer_ctx: ctx, target_entity_path: &blueprint_path, - archetype_name: Some(A::name()), - query: blueprint_query, + archetype_name: Some(archetype.name), + query: ctx.blueprint_query, view_state, }; - let component_names = A::all_components(); - let component_results = blueprint_db.latest_at( - blueprint_query, + let component_results = ctx.blueprint_db().latest_at( + ctx.blueprint_query, &blueprint_path, - component_names.iter().copied(), + archetype.component_names.iter().copied(), ); - let field_info_per_component: HashMap<_, _> = A::field_infos() + let field_info_per_component: HashMap<_, _> = archetype + .field_infos .map(|field_infos| { field_infos .iter() @@ -43,75 +84,183 @@ pub fn view_property_ui( }) .unwrap_or_default(); - let sub_prop_ui = |re_ui: &re_ui::ReUi, ui: &mut egui::Ui| { - for component_name in component_names.as_ref() { - if component_name.is_indicator_component() { - continue; - } + let non_indicator_components = archetype + .component_names + .as_ref() + .iter() + .filter(|component_name| !component_name.is_indicator_component()) + .collect::>(); + + // If the property archetype only has a single component, don't show an additional hierarchy level! + if non_indicator_components.len() == 1 { + let component_name = *non_indicator_components[0]; + let field_info = field_info_per_component.get(&component_name); - let field_info = field_info_per_component.get(component_name); - let display_name = - field_info.map_or_else(|| component_name.short_name(), |info| info.display_name); + view_property_component_ui( + &query_ctx, + ui, + component_name, + archetype.display_name, + field_info, + &blueprint_path, + component_results.get_or_empty(component_name), + fallback_provider, + ); + } else { + let sub_prop_ui = |_: &re_ui::ReUi, ui: &mut egui::Ui| { + for component_name in non_indicator_components { + let field_info = field_info_per_component.get(component_name); + let display_name = field_info + .map_or_else(|| component_name.short_name(), |info| info.display_name); - let mut list_item_response = list_item::ListItem::new(re_ui) - .interactive(false) - .show_flat( + view_property_component_ui( + &query_ctx, ui, - list_item::PropertyContent::new(display_name) - .action_button(&re_ui::icons::RESET, || { - ctx.reset_blueprint_component_by_name(&blueprint_path, *component_name); - }) - .value_fn(|_, ui, _| { - ctx.component_ui_registry.edit_ui( - &query_ctx, - ui, - re_viewer_context::UiLayout::List, - blueprint_db, - &blueprint_path, - &blueprint_path, - *component_name, - component_results.get_or_empty(*component_name), - fallback_provider, - ); - }), + *component_name, + display_name, + field_info, + &blueprint_path, + component_results.get_or_empty(*component_name), + fallback_provider, ); - - if let Some(tooltip) = field_info.map(|info| info.documentation) { - list_item_response = list_item_response.on_hover_text(tooltip); } + }; - list_item_response.context_menu(|ui| { - if ui.button("Reset to default blueprint.") - .on_hover_text("Resets this property to the value in the default blueprint.\n -If no default blueprint was set or it didn't set any value for this field, this is the same as resetting to empty.") - .clicked() { - ctx.reset_blueprint_component_by_name(&blueprint_path, *component_name); - ui.close_menu(); - } - ui.add_enabled_ui(component_results.contains_non_empty(*component_name), |ui| { - if ui.button("Reset to empty.") - .on_hover_text("Resets this property to an unset value, meaning that a heuristically determined value will be used instead.\n -This has the same effect as not setting the value in the blueprint at all.") - .on_disabled_hover_text("The property is already unset.") - .clicked() { - ctx.save_empty_blueprint_component_by_name(&blueprint_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. - }); - } + list_item::ListItem::new(ctx.re_ui) + .interactive(false) + .show_hierarchical_with_children( + ui, + ui.make_persistent_id(archetype.name.full_name()), + true, + list_item::LabelContent::new(archetype.display_name), + sub_prop_ui, + ); + } +} + +/// Draw view property ui for a single component of a view property archetype. +#[allow(clippy::too_many_arguments)] +fn view_property_component_ui( + ctx: &QueryContext<'_>, + ui: &mut egui::Ui, + component_name: ComponentName, + root_item_display_name: &str, + field_info: Option<&ArchetypeFieldInfo>, + blueprint_path: &re_log_types::EntityPath, + component_results: &re_query::LatestAtComponentResults, + fallback_provider: &dyn ComponentFallbackProvider, +) { + let singleline_list_item_content = singleline_list_item_content( + ctx, + root_item_display_name, + blueprint_path, + component_name, + component_results, + fallback_provider, + ); + + let ui_types = ctx + .viewer_ctx + .component_ui_registry + .registered_ui_types(component_name); + + let mut list_item_response = if ui_types.contains(ComponentUiTypes::MultiLineEditor) { + let default_open = false; + let id = egui::Id::new((blueprint_path.hash(), component_name)); + list_item::ListItem::new(ctx.viewer_ctx.re_ui) + .interactive(false) + .show_hierarchical_with_children( + ui, + id, + 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, + component_results, + fallback_provider, + ); + }, + ) + .item_response + } else { + list_item::ListItem::new(ctx.viewer_ctx.re_ui) + .interactive(false) + .show_flat(ui, singleline_list_item_content) }; - list_item::ListItem::new(ctx.re_ui) - .interactive(false) - .show_hierarchical_with_children( - ui, - ui.make_persistent_id(A::name().full_name()), - true, - list_item::LabelContent::new(A::display_name()), - sub_prop_ui, - ); + if let Some(tooltip) = field_info.map(|info| info.documentation) { + list_item_response = list_item_response.on_hover_text(tooltip); + } + + view_property_context_menu( + ctx.viewer_ctx, + &list_item_response, + blueprint_path, + component_name, + component_results, + ); +} + +fn view_property_context_menu( + ctx: &ViewerContext<'_>, + list_item_response: &egui::Response, + blueprint_path: &re_log_types::EntityPath, + component_name: ComponentName, + component_results: &re_query::LatestAtComponentResults, +) { + list_item_response.context_menu(|ui| { + if ui.button("Reset to default blueprint.") + .on_hover_text("Resets this property to the value in the default blueprint.\n + If no default blueprint was set or it didn't set any value for this field, this is the same as resetting to empty.") + .clicked() { + ctx.reset_blueprint_component_by_name(blueprint_path, component_name); + ui.close_menu(); + } + + let blueprint_db = ctx.blueprint_db(); + ui.add_enabled_ui(!component_results.is_empty(blueprint_db.resolver()), |ui| { + if ui.button("Reset to empty.") + .on_hover_text("Resets this property to an unset value, meaning that a heuristically determined value will be used instead.\n +This has the same effect as not setting the value in the blueprint at all.") + .on_disabled_hover_text("The property is already unset.") + .clicked() { + ctx.save_empty_blueprint_component_by_name(blueprint_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, + component_results: &'a re_query::LatestAtComponentResults, + fallback_provider: &'a dyn ComponentFallbackProvider, +) -> list_item::PropertyContent<'a> { + list_item::PropertyContent::new(display_name) + .action_button(&re_ui::icons::RESET, move || { + ctx.viewer_ctx + .reset_blueprint_component_by_name(blueprint_path, component_name); + }) + .value_fn(move |_, ui, _| { + ctx.viewer_ctx.component_ui_registry.singleline_edit_ui( + ctx, + ui, + ctx.viewer_ctx.blueprint_db(), + blueprint_path, + component_name, + component_results, + fallback_provider, + ); + }) } diff --git a/crates/re_types/src/datatypes/range2d_ext.rs b/crates/re_types/src/datatypes/range2d_ext.rs index 9bba22eb0ade3..cf233f61222bb 100644 --- a/crates/re_types/src/datatypes/range2d_ext.rs +++ b/crates/re_types/src/datatypes/range2d_ext.rs @@ -29,8 +29,12 @@ impl std::fmt::Display for Range2D { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "[{}, {}]×[{}, {}]", - self.x_range.0[0], self.x_range.0[1], self.y_range.0[0], self.y_range.0[1], + "[{:.prec$}, {:.prec$}]×[{:.prec$}, {:.prec$}]", + self.x_range.0[0], + self.x_range.0[1], + self.y_range.0[0], + self.y_range.0[1], + prec = crate::DISPLAY_PRECISION, ) } } diff --git a/crates/re_viewer_context/Cargo.toml b/crates/re_viewer_context/Cargo.toml index 18503e45de91b..0324d0111a48d 100644 --- a/crates/re_viewer_context/Cargo.toml +++ b/crates/re_viewer_context/Cargo.toml @@ -36,10 +36,11 @@ re_ui.workspace = true ahash.workspace = true anyhow.workspace = true bit-vec.workspace = true +bitflags.workspace = true bytemuck.workspace = true +egui_tiles.workspace = true egui-wgpu.workspace = true egui.workspace = true -egui_tiles.workspace = true glam = { workspace = true, features = ["serde"] } half.workspace = true indexmap = { workspace = true, features = ["std", "serde"] } diff --git a/crates/re_viewer_context/src/component_ui_registry.rs b/crates/re_viewer_context/src/component_ui_registry.rs index a7a001f30303d..9c3467db42c1f 100644 --- a/crates/re_viewer_context/src/component_ui_registry.rs +++ b/crates/re_viewer_context/src/component_ui_registry.rs @@ -42,6 +42,21 @@ pub enum UiLayout { SelectionPanelFull, } +bitflags::bitflags! { + /// Specifies which UI callbacks are available for a component. + #[derive(PartialEq, Eq, Debug, Copy, Clone)] + pub struct ComponentUiTypes: u8 { + /// Display the component in a read-only way. + const DisplayUi = 0b0000001; + + /// Edit the component in a single [`re_ui::list_item::ListItem`] line. + const SingleLineEditor = 0b0000010; + + /// Edit the component over multiple [`re_ui::list_item::ListItem`]s. + const MultiLineEditor = 0b0000100; + } +} + type ComponentUiCallback = Box< dyn Fn( &ViewerContext<'_>, @@ -75,8 +90,10 @@ type UntypedComponentEditCallback = Box< pub struct ComponentUiRegistry { /// Ui method to use if there was no specific one registered for a component. fallback_ui: ComponentUiCallback, + component_uis: BTreeMap, - component_editors: BTreeMap, + component_singleline_editors: BTreeMap, + component_multiline_editors: BTreeMap, } impl ComponentUiRegistry { @@ -84,54 +101,57 @@ impl ComponentUiRegistry { Self { fallback_ui, component_uis: Default::default(), - component_editors: Default::default(), + component_singleline_editors: Default::default(), + component_multiline_editors: Default::default(), } } /// Registers how to show a given component in the ui. /// - /// If the component was already registered, the new callback replaces the old one. - pub fn add(&mut self, name: ComponentName, callback: ComponentUiCallback) { + /// If the component has already a display ui registered, the new callback replaces the old one. + pub fn add_display_ui(&mut self, name: ComponentName, callback: ComponentUiCallback) { self.component_uis.insert(name, callback); } - /// Registers how to edit a given component in the ui. + /// Registers how to edit a given component in the ui in a single line. /// - /// If the component was already registered, the new callback replaces the old one. - /// Prefer [`ComponentUiRegistry::add_editor`] whenever possible - pub fn add_untyped_editor( + /// If the component has already a single- or multiline editor registered respectively, + /// the new callback replaces the old one. + /// Prefer [`ComponentUiRegistry::add_single_line_editor_ui`] whenever possible + pub fn add_untyped_editor_ui( &mut self, name: ComponentName, editor_callback: UntypedComponentEditCallback, + multiline: bool, ) { - self.component_editors.insert(name, editor_callback); + if multiline { + &mut self.component_multiline_editors + } else { + &mut self.component_singleline_editors + } + .insert(name, editor_callback); } /// Registers how to edit a given component in the ui. /// - /// If the component was already registered, the new callback replaces the old one. - /// - /// Typed editors do not handle absence of a value as well as lists of values and will be skipped in these cases. - /// (This means that there must always be at least a fallback value available.) - /// - /// The value is only updated if the editor callback returns a `egui::Response::changed`. - /// On the flip side, this means that even if the data has not changed it may be written back to the store. - /// This can be relevant for transitioning from a fallback or default value to a custom value even if they are equal. - /// - /// Design principles for writing editors: - /// * Don't show a tooltip, this is solved at a higher level. - /// * Try not to assume context of the component beyond its inherent semantics - /// (e.g. if you get a `Color` you can't assume whether it's a background color or a point color) - /// * The returned [`egui::Response`] should be for the widget that has the tooltip, not any pop-up content. - /// * Make sure that changes are propagated via [`egui::Response::mark_changed`] if necessary. - // - // TODO(andreas): Implement handling for ui elements that are expandable (e.g. 2D bounds is too complex for a single line). - pub fn add_editor( + /// If the component has already a multi line editor registered, the new callback replaces the old one. + /// Prefer [`ComponentUiRegistry::add_multi_line_editor_ui`] whenever possible + pub fn add_untyped_multiline_editor_ui( + &mut self, + name: ComponentName, + editor_callback: UntypedComponentEditCallback, + ) { + self.component_multiline_editors + .insert(name, editor_callback); + } + + fn add_typed_editor_ui( &mut self, editor_callback: impl Fn(&ViewerContext<'_>, &mut egui::Ui, &mut C) -> egui::Response + Send + Sync + 'static, + multiline: bool, ) { fn try_deserialize(value: &dyn arrow2::array::Array) -> Option { let component_name = C::name(); @@ -141,9 +161,9 @@ impl ComponentUiRegistry { if values.len() > 1 { // Whatever we did prior to calling this should have taken care if it! re_log::error_once!( - "Can only edit a single value at a time, got {} values for editing {component_name}", - values.len(), - ); + "Can only edit a single value at a time, got {} values for editing {component_name}", + values.len(), + ); } if let Some(v) = values.into_iter().next() { Some(v) @@ -176,12 +196,83 @@ impl ComponentUiRegistry { }) }); - self.add_untyped_editor(C::name(), untyped_callback); + self.add_untyped_editor_ui(C::name(), untyped_callback, multiline); + } + + /// Registers how to edit a given component in the ui in a single list item line. + /// + /// If the component already has a singleline editor registered, the new callback replaces the old one. + /// + /// Typed editors do not handle absence of a value as well as lists of values and will be skipped in these cases. + /// (This means that there must always be at least a fallback value available.) + /// + /// The value is only updated if the editor callback returns a `egui::Response::changed`. + /// On the flip side, this means that even if the data has not changed it may be written back to the store. + /// This can be relevant for transitioning from a fallback or default value to a custom value even if they are equal. + /// + /// Design principles for writing editors: + /// * This is the value function for a [`re_ui::list_item::ListItem`], behave accordingly! + /// * Don't show a tooltip, this is solved at a higher level. + /// * Try not to assume context of the component beyond its inherent semantics + /// (e.g. if you get a `Color` you can't assume whether it's a background color or a point color) + /// * The returned [`egui::Response`] should be for the widget that has the tooltip, not any pop-up content. + /// * Make sure that changes are propagated via [`egui::Response::mark_changed`] if necessary. + pub fn add_singleline_editor_ui( + &mut self, + editor_callback: impl Fn(&ViewerContext<'_>, &mut egui::Ui, &mut C) -> egui::Response + + Send + + Sync + + 'static, + ) { + let multiline = false; + self.add_typed_editor_ui(editor_callback, multiline); + } + + /// Registers how to edit a given component in the ui with multiple list items. + /// + /// If the component already has a singleline editor registered, the new callback replaces the old one. + /// + /// Typed editors do not handle absence of a value as well as lists of values and will be skipped in these cases. + /// (This means that there must always be at least a fallback value available.) + /// + /// The value is only updated if the editor callback returns a `egui::Response::changed`. + /// On the flip side, this means that even if the data has not changed it may be written back to the store. + /// This can be relevant for transitioning from a fallback or default value to a custom value even if they are equal. + /// + /// Design principles for writing editors: + /// * This is the content function for hierarchical [`re_ui::list_item::ListItem`], behave accordingly! + /// * Try not to assume context of the component beyond its inherent semantics + /// (e.g. if you get a `Color` you can't assume whether it's a background color or a point color) + /// * The returned [`egui::Response`] should be for the widget that has the tooltip, not any pop-up content. + /// * Make sure that changes are propagated via [`egui::Response::mark_changed`] if necessary. + pub fn add_multiline_editor_ui( + &mut self, + editor_callback: impl Fn(&ViewerContext<'_>, &mut egui::Ui, &mut C) -> egui::Response + + Send + + Sync + + 'static, + ) { + let multiline = true; + self.add_typed_editor_ui(editor_callback, multiline); } - /// Check if there is a registered editor for a given component - pub fn has_registered_editor(&self, name: &ComponentName) -> bool { - self.component_editors.contains_key(name) + /// Queries which ui types are registered for a component. + /// + /// Note that there's always a fallback display ui. + pub fn registered_ui_types(&self, name: ComponentName) -> ComponentUiTypes { + let mut types = ComponentUiTypes::empty(); + + if self.component_uis.contains_key(&name) { + types |= ComponentUiTypes::DisplayUi; + } + if self.component_singleline_editors.contains_key(&name) { + types |= ComponentUiTypes::SingleLineEditor; + } + if self.component_multiline_editors.contains_key(&name) { + types |= ComponentUiTypes::MultiLineEditor; + } + + types } /// Show a ui for this instance of this component. @@ -220,30 +311,88 @@ impl ComponentUiRegistry { ); } - /// Show an editor for this instance of this component. + /// Show a multi-line editor for this instance of this component. /// /// Changes will be written to the blueprint store at the given override path. /// Any change is expected to be effective next frame and passed in via the `component_query_result` parameter. /// (Otherwise, this method is agnostic to where the component data is stored.) #[allow(clippy::too_many_arguments)] - pub fn edit_ui( + pub fn multiline_edit_ui( &self, ctx: &QueryContext<'_>, ui: &mut egui::Ui, - ui_layout: UiLayout, origin_db: &EntityDb, - entity_path: &EntityPath, - blueprint_override_path: &EntityPath, + blueprint_write_path: &EntityPath, component_name: ComponentName, component_query_result: &LatestAtComponentResults, fallback_provider: &dyn ComponentFallbackProvider, + ) { + let multiline = true; + self.edit_ui( + ctx, + ui, + origin_db, + blueprint_write_path, + component_name, + component_query_result, + fallback_provider, + multiline, + ); + } + + /// Show a single-line editor for this instance of this component. + /// + /// Changes will be written to the blueprint store at the given override path. + /// Any change is expected to be effective next frame and passed in via the `component_query_result` parameter. + /// (Otherwise, this method is agnostic to where the component data is stored.) + #[allow(clippy::too_many_arguments)] + pub fn singleline_edit_ui( + &self, + ctx: &QueryContext<'_>, + ui: &mut egui::Ui, + origin_db: &EntityDb, + blueprint_write_path: &EntityPath, + component_name: ComponentName, + component_query_result: &LatestAtComponentResults, + fallback_provider: &dyn ComponentFallbackProvider, + ) { + let multiline = false; + self.edit_ui( + ctx, + ui, + origin_db, + blueprint_write_path, + component_name, + component_query_result, + fallback_provider, + multiline, + ); + } + + #[allow(clippy::too_many_arguments)] + fn edit_ui( + &self, + ctx: &QueryContext<'_>, + ui: &mut egui::Ui, + origin_db: &EntityDb, + blueprint_write_path: &EntityPath, + component_name: ComponentName, + component_query_result: &LatestAtComponentResults, + fallback_provider: &dyn ComponentFallbackProvider, + multiline: bool, ) { re_tracing::profile_function!(component_name.full_name()); // TODO(andreas, jleibs): Editors only show & edit the first instance of a component batch. let instance: Instance = 0.into(); - if let Some(edit_callback) = self.component_editors.get(&component_name) { + let editors = if multiline { + &self.component_multiline_editors + } else { + &self.component_singleline_editors + }; + + if let Some(edit_callback) = editors.get(&component_name) { let component_value_or_fallback = match component_value_or_fallback( ctx, component_query_result, @@ -264,7 +413,7 @@ impl ComponentUiRegistry { (*edit_callback)(ctx.viewer_ctx, ui, component_value_or_fallback.as_ref()) { ctx.viewer_ctx.save_blueprint_data_cell( - blueprint_override_path, + blueprint_write_path, re_log_types::DataCell::from_arrow(component_name, updated), ); } @@ -273,10 +422,10 @@ impl ComponentUiRegistry { self.ui( ctx.viewer_ctx, ui, - ui_layout, + UiLayout::List, ctx.query, origin_db, - entity_path, + ctx.target_entity_path, component_query_result, &instance, ); diff --git a/crates/re_viewer_context/src/lib.rs b/crates/re_viewer_context/src/lib.rs index 8cd9d13492d06..3ef3344c0b951 100644 --- a/crates/re_viewer_context/src/lib.rs +++ b/crates/re_viewer_context/src/lib.rs @@ -46,7 +46,7 @@ pub use component_fallbacks::{ ComponentFallbackError, ComponentFallbackProvider, ComponentFallbackProviderResult, ComponentPlaceholders, TypedComponentFallbackProvider, }; -pub use component_ui_registry::{ComponentUiRegistry, UiLayout}; +pub use component_ui_registry::{ComponentUiRegistry, ComponentUiTypes, UiLayout}; pub use contents::{blueprint_id_to_tile_id, Contents, ContentsName}; pub use item::Item; pub use query_context::{ diff --git a/crates/re_viewport_blueprint/src/view_properties.rs b/crates/re_viewport_blueprint/src/view_properties.rs index e166f4e559768..358aab98d5058 100644 --- a/crates/re_viewport_blueprint/src/view_properties.rs +++ b/crates/re_viewport_blueprint/src/view_properties.rs @@ -20,7 +20,7 @@ pub fn query_view_property( where LatestAtResults: ToArchetype, { - let path = entity_path_for_view_property::(space_view_id, blueprint_db.tree()); + let path = entity_path_for_view_property(space_view_id, blueprint_db.tree(), A::name()); ( blueprint_db .latest_at_archetype(&path, query) @@ -72,11 +72,8 @@ impl<'a> ViewProperty<'a> { ) -> Self { let blueprint_db = viewer_ctx.blueprint_db(); - let blueprint_store_path = entity_path_for_view_property_from_archetype_name( - space_view_id, - blueprint_db.tree(), - archetype_name, - ); + let blueprint_store_path = + entity_path_for_view_property(space_view_id, blueprint_db.tree(), archetype_name); let query_results = blueprint_db.latest_at( viewer_ctx.blueprint_query, @@ -169,19 +166,7 @@ impl<'a> ViewProperty<'a> { } } -// TODO(andreas): Replace all usages with `ViewProperty`. -pub fn entity_path_for_view_property( - space_view_id: SpaceViewId, - _blueprint_entity_tree: &EntityTree, -) -> EntityPath { - entity_path_for_view_property_from_archetype_name( - space_view_id, - _blueprint_entity_tree, - T::name(), - ) -} - -fn entity_path_for_view_property_from_archetype_name( +pub fn entity_path_for_view_property( space_view_id: SpaceViewId, _blueprint_entity_tree: &EntityTree, archetype_name: ArchetypeName,