From 17e321caa44c5ec86543f87cda5b320ecceb0381 Mon Sep 17 00:00:00 2001 From: Antoine Beyeler Date: Wed, 7 Aug 2024 14:28:29 +0200 Subject: [PATCH] New UI and lots of clean-up --- .../src/space_view_class.rs | 152 ++-- .../src/time_range_table.rs | 3 +- .../re_space_view_dataframe/src/view_query.rs | 857 ++++++++---------- .../re_viewer_context/src/time_drag_value.rs | 2 +- 4 files changed, 452 insertions(+), 562 deletions(-) diff --git a/crates/viewer/re_space_view_dataframe/src/space_view_class.rs b/crates/viewer/re_space_view_dataframe/src/space_view_class.rs index b43689ce50455..a031effde836a 100644 --- a/crates/viewer/re_space_view_dataframe/src/space_view_class.rs +++ b/crates/viewer/re_space_view_dataframe/src/space_view_class.rs @@ -130,33 +130,24 @@ mode sets the default time range to _everything_. You can override this in the s _space_origin: &EntityPath, space_view_id: SpaceViewId, ) -> Result<(), SpaceViewSystemExecutionError> { - // let settings = ViewProperty::from_archetype::( - // ctx.blueprint_db(), - // ctx.blueprint_query, - // space_view_id, - // ); - // - // let mode = - // settings.component_or_fallback::(ctx, self, state)?; + crate::view_query::query_ui(ctx, ui, state, space_view_id)?; list_item::list_item_scope(ui, "dataframe_view_selection_ui", |ui| { - //TODO(ab): ideally we'd drop the "Dataframe" part in the UI label - //view_property_ui::(ctx, ui, space_view_id, self, state); - - view_property_ui::(ctx, ui, space_view_id, self, state); - super::view_query::Query::ui(ctx, ui, self, state, space_view_id); - //TODO: fix this :scream: - let view_query = Query::try_from_blueprint(ctx, space_view_id, self, state)?; - ui.add_enabled_ui(matches!(view_query.mode, QueryMode::Range { .. }), |ui| { - view_property_ui::( - ctx, - ui, - space_view_id, - self, - state, - ); - }); + let view_query = Query::try_from_blueprint(ctx, space_view_id)?; + + ui.add_enabled_ui( + matches!(view_query.mode(ctx), QueryMode::Range { .. }), + |ui| { + view_property_ui::( + ctx, + ui, + space_view_id, + self, + state, + ); + }, + ); Ok(()) }) @@ -172,56 +163,21 @@ mode sets the default time range to _everything_. You can override this in the s ) -> Result<(), SpaceViewSystemExecutionError> { re_tracing::profile_function!(); - // let settings = ViewProperty::from_archetype::( - // ctx.blueprint_db(), - // ctx.blueprint_query, - // query.space_view_id, - // ); - // - // let mode = - // settings.component_or_fallback::(ctx, self, state)?; - // - // // update state - // let state = state.downcast_mut::()?; - // state.mode = mode; - // - // match mode { - // components::DataframeViewMode::LatestAt => latest_at_table_ui(ctx, ui, query), - // - // components::DataframeViewMode::TimeRange => { - // let time_range_table_order = - // ViewProperty::from_archetype::( - // ctx.blueprint_db(), - // ctx.blueprint_query, - // query.space_view_id, - // ); - // let sort_key = time_range_table_order - // .component_or_fallback::(ctx, self, state)?; - // let sort_order = time_range_table_order - // .component_or_fallback::(ctx, self, state)?; - // - // time_range_table_ui(ctx, ui, query, sort_key, sort_order); - // } - // }; - - let view_query = - super::view_query::Query::try_from_blueprint(ctx, query.space_view_id, self, state)?; + let view_query = super::view_query::Query::try_from_blueprint(ctx, query.space_view_id)?; + let timeline_name = view_query.timeline_name(ctx); + let query_mode = view_query.mode(ctx); let Some(timeline) = ctx .recording() .timelines() - .find(|t| t.name() == &view_query.timeline) + .find(|t| t.name() == &timeline_name) else { - //TODO: create dummy timeline instead? - re_log::warn_once!( - "Could not find timeline {:?}.", - view_query.timeline.as_str() - ); + re_log::warn_once!("Could not find timeline {:?}.", timeline_name.as_str()); //TODO(ab): we should have an error for that return Ok(()); }; - match view_query.mode { + match query_mode { QueryMode::LatestAt { time } => { latest_at_table_ui(ctx, ui, query, LatestAtQuery::new(*timeline, time)) } @@ -253,35 +209,37 @@ mode sets the default time range to _everything_. You can override this in the s } } -impl TypedComponentFallbackProvider for DataframeSpaceView { - fn fallback_for(&self, ctx: &re_viewer_context::QueryContext<'_>) -> Timeline { - //TODO: add helper to Timeline component - Timeline(Utf8::from( - ctx.viewer_ctx - .rec_cfg - .time_ctrl - .read() - .timeline() - .name() - .as_str(), - )) - } -} - -impl TypedComponentFallbackProvider for DataframeSpaceView { - fn fallback_for(&self, ctx: &QueryContext<'_>) -> LatestAtQueries { - let current_time = ctx.viewer_ctx.rec_cfg.time_ctrl.read(); - - let latest_at_query = datatypes::LatestAtQuery { - timeline: Utf8::from(current_time.timeline().name().as_str()), - time: re_types_core::datatypes::TimeInt::from( - current_time - .time_int() - .unwrap_or(re_log_types::TimeInt::MAX), - ), - }; - LatestAtQueries::from(vec![latest_at_query]) - } -} - -re_viewer_context::impl_component_fallback_provider!(DataframeSpaceView => [Timeline, LatestAtQueries]); +// //TODO: probably no longer needed +// impl TypedComponentFallbackProvider for DataframeSpaceView { +// fn fallback_for(&self, ctx: &re_viewer_context::QueryContext<'_>) -> Timeline { +// //TODO: add helper to Timeline component +// Timeline(Utf8::from( +// ctx.viewer_ctx +// .rec_cfg +// .time_ctrl +// .read() +// .timeline() +// .name() +// .as_str(), +// )) +// } +// } +// +// //TODO: probably no longer needed +// impl TypedComponentFallbackProvider for DataframeSpaceView { +// fn fallback_for(&self, ctx: &QueryContext<'_>) -> LatestAtQueries { +// let current_time = ctx.viewer_ctx.rec_cfg.time_ctrl.read(); +// +// let latest_at_query = datatypes::LatestAtQuery { +// timeline: Utf8::from(current_time.timeline().name().as_str()), +// time: re_types_core::datatypes::TimeInt::from( +// current_time +// .time_int() +// .unwrap_or(re_log_types::TimeInt::MAX), +// ), +// }; +// LatestAtQueries::from(vec![latest_at_query]) +// } +// } +// +// re_viewer_context::impl_component_fallback_provider!(DataframeSpaceView => [Timeline, LatestAtQueries]); diff --git a/crates/viewer/re_space_view_dataframe/src/time_range_table.rs b/crates/viewer/re_space_view_dataframe/src/time_range_table.rs index 190e4c4473b41..839d72cde17d1 100644 --- a/crates/viewer/re_space_view_dataframe/src/time_range_table.rs +++ b/crates/viewer/re_space_view_dataframe/src/time_range_table.rs @@ -6,9 +6,8 @@ use re_data_ui::item_ui::entity_path_button; use re_entity_db::InstancePath; use re_log_types::{EntityPath, ResolvedTimeRange, TimeInt, Timeline}; use re_types::blueprint::components::{SortKey, SortOrder}; -use re_types_core::datatypes::TimeRange; use re_types_core::ComponentName; -use re_viewer_context::{Item, QueryRange, UiLayout, ViewQuery, ViewerContext}; +use re_viewer_context::{Item, UiLayout, ViewQuery, ViewerContext}; use crate::table_ui::{row_id_ui, table_ui}; diff --git a/crates/viewer/re_space_view_dataframe/src/view_query.rs b/crates/viewer/re_space_view_dataframe/src/view_query.rs index 3eb46fa549474..7825c9651368d 100644 --- a/crates/viewer/re_space_view_dataframe/src/view_query.rs +++ b/crates/viewer/re_space_view_dataframe/src/view_query.rs @@ -1,18 +1,17 @@ -use egui::{NumExt, Response}; -use re_entity_db::TimeHistogram; -use re_log_types::{TimeInt, TimeType, TimeZone, TimelineName}; +use crate::visualizer_system::EmptySystem; +use re_log_types::{TimeInt, TimeType, TimelineName}; use re_types::blueprint::components::QueryKind; -use re_types::blueprint::datatypes::LatestAtQuery; use re_types::blueprint::{archetypes, components, datatypes}; -use re_types_core::{Archetype as _, ComponentName, Loggable as _}; +use re_types_core::{Archetype as _, Loggable as _}; use re_ui::{list_item, UiExt as _}; use re_viewer_context::{ - ComponentFallbackProvider, QueryContext, SpaceViewId, SpaceViewState, - SpaceViewSystemExecutionError, ViewQuery, ViewerContext, + QueryContext, SpaceViewId, SpaceViewState, SpaceViewSystemExecutionError, TimeDragValue, + ViewerContext, }; use re_viewport_blueprint::{entity_path_for_view_property, ViewProperty}; -use std::ops::RangeInclusive; +/// The query mode for the dataframe view. +#[derive(Debug, Clone, Copy)] pub(crate) enum QueryMode { LatestAt { time: TimeInt, @@ -25,17 +24,20 @@ pub(crate) enum QueryMode { //TODO(#7067): add selected components } -pub(crate) struct Query { - pub(crate) timeline: TimelineName, - pub(crate) mode: QueryMode, +/// Helper for handling the dataframe view query blueprint. +pub(crate) enum Query { + FollowTimeline, + + Override { + timeline: TimelineName, + mode: QueryMode, + }, } impl Query { pub(crate) fn try_from_blueprint( ctx: &ViewerContext<'_>, space_view_id: SpaceViewId, - fallback_provider: &dyn ComponentFallbackProvider, - state: &mut dyn SpaceViewState, ) -> Result { let property = ViewProperty::from_archetype::( ctx.blueprint_db(), @@ -43,48 +45,35 @@ impl Query { space_view_id, ); - let timeline = TimelineName::from( - property - .component_or_fallback::(ctx, fallback_provider, state)? - .0 - .as_str(), - ); + let Some(timeline) = property + .component_or_empty::()? + .map(|t| t.timeline_name()) + else { + // if the timeline is not set, it means that we must follow the time panel settings + return Ok(Self::FollowTimeline); + }; - let mode = property.component_or_fallback::( - ctx, - fallback_provider, - state, - )?; + let mode = property + .component_or_empty::()? + .unwrap_or(QueryKind::LatestAt); let mode = match mode { QueryKind::LatestAt => { let time = property - .component_or_fallback::( - ctx, - fallback_provider, - state, - )? + .component_or_empty::()? + .unwrap_or_default() .0 .into_iter() .find(|q| q.timeline.as_str() == timeline) .map(|q| q.time.into()) - .unwrap_or_else(|| { - ctx.rec_cfg - .time_ctrl - .read() - .time_int() - .unwrap_or(TimeInt::MAX) - }); + .unwrap_or(TimeInt::MAX); QueryMode::LatestAt { time } } QueryKind::TimeRange => { let (from, to) = property - .component_or_fallback::( - ctx, - fallback_provider, - state, - )? + .component_or_empty::()? + .unwrap_or_default() .0 .into_iter() .find(|q| q.timeline.as_str() == timeline) @@ -95,476 +84,420 @@ impl Query { } }; - Ok(Self { timeline, mode }) + Ok(Self::Override { timeline, mode }) + } + + /// Get the timeline name for the query + #[inline] + pub(crate) fn timeline_name(&self, ctx: &ViewerContext<'_>) -> TimelineName { + match self { + Query::FollowTimeline => *ctx.rec_cfg.time_ctrl.read().timeline().name(), + Query::Override { timeline, .. } => *timeline, + } + } + + /// Get the mode for the query + #[inline] + pub(crate) fn mode(&self, ctx: &ViewerContext<'_>) -> QueryMode { + match self { + Query::FollowTimeline => { + let time_ctrl = ctx.rec_cfg.time_ctrl.read(); + QueryMode::LatestAt { + time: time_ctrl.time_int().unwrap_or(TimeInt::MAX), + } + } + Query::Override { mode, .. } => *mode, + } } - pub(crate) fn ui( + /// Save the query mode for the given timeline to the blueprint. + pub(crate) fn save_mode_for_timeline( ctx: &ViewerContext<'_>, - ui: &mut egui::Ui, - fallback_provider: &dyn ComponentFallbackProvider, - state: &mut dyn SpaceViewState, space_view_id: SpaceViewId, + timeline_name: &TimelineName, + query_mode: &QueryMode, ) -> Result<(), SpaceViewSystemExecutionError> { - let name = archetypes::DataframeQuery::name(); - let Some(reflection) = ctx.reflection.archetypes.get(&name) else { - // The `ArchetypeReflectionMarker` bound should make this impossible. - re_log::warn_once!("Missing reflection data for archetype {name:?}."); - //TODO(ab): we should have an error for that - return Ok(()); - }; - - let blueprint_path = - entity_path_for_view_property(space_view_id, ctx.blueprint_db().tree(), name); - let query_context = QueryContext { - viewer_ctx: ctx, - target_entity_path: &blueprint_path, - archetype_name: Some(name), - query: ctx.blueprint_query, - view_state: state, - view_ctx: None, - }; - let property = ViewProperty::from_archetype::( ctx.blueprint_db(), ctx.blueprint_query, space_view_id, ); - let current_mode = property.component_or_fallback::( - ctx, - fallback_provider, - state, - )?; + match query_mode { + QueryMode::LatestAt { time } => { + let mut latest_at_queries = property + .component_or_empty::()? + .unwrap_or_default(); + + latest_at_queries.set_query_for_timeline( + timeline_name.as_str(), + Some(datatypes::LatestAtQuery { + timeline: timeline_name.as_str().into(), + time: (*time).into(), + }), + ); + + ctx.save_blueprint_component(&property.blueprint_store_path, &latest_at_queries); + ctx.save_blueprint_component(&property.blueprint_store_path, &QueryKind::LatestAt); + } + QueryMode::Range { from, to } => { + let mut time_range_queries = property + .component_or_empty::()? + .unwrap_or_default(); + + time_range_queries.set_query_for_timeline( + timeline_name.as_str(), + Some(datatypes::TimeRangeQuery { + timeline: timeline_name.as_str().into(), + start: (*from).into(), + end: (*to).into(), + }), + ); + + ctx.save_blueprint_component(&property.blueprint_store_path, &time_range_queries); + ctx.save_blueprint_component(&property.blueprint_store_path, &QueryKind::TimeRange); + } + }; - let timeline_name = property - .component_or_fallback::(ctx, fallback_provider, state) - .map(|t| t.timeline_name())?; + Ok(()) + } +} - let Some(timeline) = ctx - .recording() +pub(crate) fn query_ui( + ctx: &ViewerContext<'_>, + ui: &mut egui::Ui, + state: &mut dyn SpaceViewState, + space_view_id: SpaceViewId, +) -> Result<(), SpaceViewSystemExecutionError> { + let property = ViewProperty::from_archetype::( + ctx.blueprint_db(), + ctx.blueprint_query, + space_view_id, + ); + + // The existence of a timeline component determines if we are in follow time panel or + // override mode. + let timeline_component = property.component_or_empty::()?; + let timeline_name = timeline_component.as_ref().map(|t| t.timeline_name()); + + let timeline = timeline_name.and_then(|timeline_name| { + ctx.recording() .timelines() .find(|t| t.name() == &timeline_name) - else { - re_log::warn_once!("Could not find timeline {:?}.", timeline_name.as_str()); - //TODO(ab): we should have an error for that - return Ok(()); - }; - - let inner_ui = |ui: &mut egui::Ui| -> Result<(), SpaceViewSystemExecutionError> { - let component_results = ctx.blueprint_db().latest_at( - ctx.blueprint_query, - &blueprint_path, - reflection.fields.iter().map(|field| field.component_name), + }); + + let mut override_query = timeline.is_some(); + let changed = ui.selectable_toggle(|ui| { + ui.selectable_value(&mut override_query, false, "Follow timeline") + .changed() + || ui + .selectable_value(&mut override_query, true, "Override") + .changed() + }); + + if changed { + if override_query { + let time_ctrl = ctx.rec_cfg.time_ctrl.read(); + let timeline = time_ctrl.timeline(); + + // UX least surprising behavior: when switching from "follow" to "override", we ensure + // that the override configuration defaults to the current timeline configuration, so + // the table content remains stable. + property.save_blueprint_component( + ctx, + &components::Timeline::from(timeline.name().as_str()), ); - - // - // Timeline - // - - let component_name = components::Timeline::name(); - ui.list_item_flat_noninteractive(list_item::PropertyContent::new("Timeline").value_fn( - |ui, _| { - ctx.component_ui_registry.singleline_edit_ui( - &query_context, - ui, - ctx.blueprint_db(), - &blueprint_path, - component_name, - component_results - .component_batch_raw(&component_name) - .as_deref(), - fallback_provider, - ); - }, - )); - - // - // Mode - // - - let component_name = components::QueryKind::name(); - ui.list_item_flat_noninteractive(list_item::PropertyContent::new("Mode").value_fn( - |ui, _| { - ctx.component_ui_registry.singleline_edit_ui( - &query_context, - ui, - ctx.blueprint_db(), - &blueprint_path, - component_name, - component_results - .component_batch_raw(&component_name) - .as_deref(), - fallback_provider, - ); + Query::save_mode_for_timeline( + ctx, + space_view_id, + timeline.name(), + &QueryMode::LatestAt { + time: time_ctrl.time_int().unwrap_or(TimeInt::MAX), }, - )); - - let time_spec = if let Some(time_histogram) = ctx.recording().time_histogram(&timeline) - { - TimelineSpec::from_time_histogram(time_histogram) - } else { - // shouldn't happen, `timeline` existence was already checked - TimelineSpec::from_time_range(0..=0) - }; + )?; + } else { + property.reset_blueprint_component::(ctx); + } + } - match current_mode { - QueryKind::LatestAt => { - // - // Latest At time - // TODO(ab): we can't use edit ui because we dont have the required context - // there, aka the currently chosen timeline. - // - - let mut latest_at_queries = property - .component_or_fallback::( - ctx, - fallback_provider, - state, - )?; - - let mut latest_at_query = latest_at_queries - .query_for_timeline(timeline_name.as_str()) - .cloned() - .unwrap_or_else(|| datatypes::LatestAtQuery { - timeline: timeline_name.as_str().into(), - time: TimeInt::MAX.into(), - }); + if override_query { + override_ui(ctx, ui, state, space_view_id, property) + } else { + Ok(()) + } +} - ui.list_item_flat_noninteractive( - list_item::PropertyContent::new("At time").value_fn(|ui, _| { - let resp = match timeline.typ() { - TimeType::Time => { - time_spec - .temporal_drag_value( - ui, - &mut latest_at_query.time, - true, - None, - ctx.app_options.time_zone, - ) - .0 - } - TimeType::Sequence => time_spec.sequence_drag_value( - ui, - &mut latest_at_query.time, - true, - None, - ), - }; +fn override_ui( + ctx: &ViewerContext<'_>, + ui: &mut egui::Ui, + state: &mut dyn SpaceViewState, + space_view_id: SpaceViewId, + property: ViewProperty<'_>, +) -> Result<(), SpaceViewSystemExecutionError> { + let name = archetypes::DataframeQuery::name(); + let Some(reflection) = ctx.reflection.archetypes.get(&name) else { + // The `ArchetypeReflectionMarker` bound should make this impossible. + re_log::warn_once!("Missing reflection data for archetype {name:?}."); + return Ok(()); + }; + + let timeline = property + .component_or_empty::()? + .map(|t| t.timeline_name()) + .and_then(|timeline_name| { + ctx.recording() + .timelines() + .find(|t| t.name() == &timeline_name) + .cloned() + }) + .unwrap_or(ctx.rec_cfg.time_ctrl.read().timeline().clone()); + let timeline_name = timeline.name(); + + let blueprint_path = + entity_path_for_view_property(space_view_id, ctx.blueprint_db().tree(), name); + let query_context = QueryContext { + viewer_ctx: ctx, + target_entity_path: &blueprint_path, + archetype_name: Some(name), + query: ctx.blueprint_query, + view_state: state, + view_ctx: None, + }; + + let component_results = ctx.blueprint_db().latest_at( + ctx.blueprint_query, + &blueprint_path, + reflection.fields.iter().map(|field| field.component_name), + ); + + ui.selection_grid("dataframe_view_query_ui") + .show(ui, |ui| { + ui.grid_left_hand_label("Timeline"); - if resp.changed() { - latest_at_queries.set_query_for_timeline( - timeline_name.as_str(), - Some(latest_at_query), - ); - ctx.save_blueprint_component(&blueprint_path, &latest_at_queries); - } - }), - ); - } - QueryKind::TimeRange => { - // - // Range times - - let mut time_range_queries = property - .component_or_fallback::( - ctx, - fallback_provider, - state, - )?; - - let mut time_range_query = time_range_queries - .query_for_timeline(timeline_name.as_str()) - .cloned() - .unwrap_or_else(|| datatypes::TimeRangeQuery { - timeline: timeline_name.as_str().into(), - start: TimeInt::MIN.into(), - end: TimeInt::MAX.into(), - }); + let component_name = components::Timeline::name(); - let mut changed = false; - ui.list_item_flat_noninteractive( - list_item::PropertyContent::new("From").value_fn(|ui, _| { - let resp = match timeline.typ() { - TimeType::Time => { - time_spec - .temporal_drag_value( - ui, - &mut time_range_query.start, - true, - None, - ctx.app_options.time_zone, - ) - .0 - } - TimeType::Sequence => time_spec.sequence_drag_value( - ui, - &mut time_range_query.start, - true, - None, - ), - }; + //TODO(ab, andreas): ideally it would be _much_ easier to call if a fallback provider is not needed + ctx.component_ui_registry.singleline_edit_ui( + &query_context, + ui, + ctx.blueprint_db(), + &blueprint_path, + component_name, + component_results + .component_batch_raw(&component_name) + .as_deref(), + // we don't need to provide a fallback here as the timeline should be present by definition + &EmptySystem {}, + ); - changed |= resp.changed(); - }), - ); - - let end_response = ui.list_item_flat_noninteractive( - list_item::PropertyContent::new("To").value_fn(|ui, _| { - let resp = match timeline.typ() { - TimeType::Time => { - time_spec - .temporal_drag_value( - ui, - &mut time_range_query.end, - true, - None, - ctx.app_options.time_zone, - ) - .0 - } - TimeType::Sequence => time_spec.sequence_drag_value( - ui, - &mut time_range_query.end, - true, - None, - ), - }; + ui.end_row(); - changed |= resp.changed(); - }), - ); + ui.grid_left_hand_label("Showing"); - if changed { - time_range_queries - .set_query_for_timeline(timeline_name.as_str(), Some(time_range_query)); - ctx.save_blueprint_component(&blueprint_path, &time_range_queries); - } - } + let query = Query::try_from_blueprint(ctx, space_view_id)?; + let mut ui_query_mode: UiQueryMode = query.mode(ctx).into(); + let time_drag_value = if let Some(times) = ctx.recording().time_histogram(&timeline) { + TimeDragValue::from_time_histogram(times) + } else { + TimeDragValue::from_time_range(0..=0) + }; + let changed = ui_query_mode.ui(ctx, ui, time_drag_value, timeline.typ()); + if changed { + Query::save_mode_for_timeline( + ctx, + space_view_id, + &timeline_name, + &ui_query_mode.into(), + )?; } Ok(()) - }; - - let result = ui - .list_item() - .interactive(false) - .show_hierarchical_with_children( - ui, - ui.make_persistent_id("view_query"), - true, - list_item::LabelContent::new("Query"), - |ui| inner_ui(ui), - ); - - result.body_response.map(|r| r.inner).unwrap_or(Ok(())) - } + }) + .inner } -// ================================================================================================= -// TODO: this is copied from visible_time_range_ui.rs. It should be extracted and cleaned-up. Also -// there is time histogram stuff here that is bound to be removed/fixed. - -/// Compute and store various information about a timeline related to how the UI should behave. -#[derive(Debug)] -struct TimelineSpec { - /// Actual range of logged data on the timelines (excluding timeless data). - range: RangeInclusive, - - /// For timelines with large offsets (e.g. `log_time`), this is a rounded time just before the - /// first logged data, which can be used as offset in the UI. - base_time: Option, +// -- - // used only for temporal timelines - /// For temporal timelines, this is a nice unit factor to use. - unit_factor: i64, - - /// For temporal timelines, this is the unit symbol to display. - unit_symbol: &'static str, - - /// This is a nice range of absolute times to use when editing an absolute time. The boundaries - /// are extended to the nearest rounded unit to minimize glitches. - abs_range: RangeInclusive, - - /// This is a nice range of relative times to use when editing an absolute time. The boundaries - /// are extended to the nearest rounded unit to minimize glitches. - rel_range: RangeInclusive, +/// Helper to handle the various query modes as they are offered in the UI. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum UiQueryMode { + LatestAt { time: TimeInt }, + TimeRangeAll, + TimeRange { from: TimeInt, to: TimeInt }, } -impl TimelineSpec { - //TODO: remove that - fn from_time_histogram(times: &TimeHistogram) -> Self { - Self::from_time_range( - times.min_key().unwrap_or_default()..=times.max_key().unwrap_or_default(), - ) - } - - fn from_time_range(range: RangeInclusive) -> Self { - let span = range.end() - range.start(); - let base_time = time_range_base_time(*range.start(), span); - let (unit_symbol, unit_factor) = unit_from_span(span); - - // `abs_range` is used by the DragValue when editing an absolute time, its bound expended to - // nearest unit to minimize glitches. - let abs_range = - round_down(*range.start(), unit_factor)..=round_up(*range.end(), unit_factor); - - // `rel_range` is used by the DragValue when editing a relative time offset. It must have - // enough margin either side to accommodate for all possible values of current time. - let rel_range = round_down(-span, unit_factor)..=round_up(2 * span, unit_factor); - - Self { - range, - base_time, - unit_factor, - unit_symbol, - abs_range, - rel_range, - } - } - - fn sequence_drag_value( - &self, +impl UiQueryMode { + /// Show the UI for the query mode selector. + fn ui( + &mut self, + ctx: &ViewerContext<'_>, ui: &mut egui::Ui, - value: &mut re_types_core::datatypes::TimeInt, - absolute: bool, - low_bound_override: Option, - ) -> Response { - let mut time_range = if absolute { - self.abs_range.clone() - } else { - self.rel_range.clone() - }; + time_drag_value: TimeDragValue, + time_type: TimeType, + ) -> bool { + let orig_self = self.clone(); - // speed must be computed before messing with time_range for consistency - let span = time_range.end() - time_range.start(); - let speed = (span as f32 * 0.005).at_least(1.0); + ui.vertical(|ui| { + // + // LATEST AT + // - if let Some(low_bound_override) = low_bound_override { - time_range = low_bound_override.0.at_least(*time_range.start())..=*time_range.end(); - } + ui.horizontal(|ui| { + let mut is_latest_at = matches!(self, Self::LatestAt { .. }); - ui.add( - egui::DragValue::new(&mut value.0) - .range(time_range) - .speed(speed), - ) - } + let mut changed = ui + .re_radio_value(&mut is_latest_at, true, "Latest at") + .changed(); - /// Show a temporal drag value. - /// - /// Feature rich: - /// - scale to the proper units - /// - display the base time if any - /// - etc. - /// - /// Returns a tuple of the [`egui::DragValue`]'s [`egui::Response`], and the base time label's - /// [`egui::Response`], if any. - fn temporal_drag_value( - &self, - ui: &mut egui::Ui, - value: &mut re_types_core::datatypes::TimeInt, - absolute: bool, - low_bound_override: Option, - time_zone_for_timestamps: TimeZone, - ) -> (Response, Option) { - let mut time_range = if absolute { - self.abs_range.clone() - } else { - self.rel_range.clone() - }; - - let factor = self.unit_factor as f32; - let offset = if absolute { - self.base_time.unwrap_or(0) - } else { - 0 - }; + if is_latest_at { + let mut time = if let Self::LatestAt { time } = self { + *time + } else { + TimeInt::MAX + } + .into(); + + changed |= match time_type { + TimeType::Time => time_drag_value + .temporal_drag_value_ui( + ui, + &mut time, + true, + None, + ctx.app_options.time_zone, + ) + .0 + .changed(), + TimeType::Sequence => time_drag_value + .sequence_drag_value_ui(ui, &mut time, true, None) + .changed(), + }; - // speed must be computed before messing with time_range for consistency - let speed = (time_range.end() - time_range.start()) as f32 / factor * 0.005; + if changed { + *self = Self::LatestAt { time: time.into() }; + } + } + }); - if let Some(low_bound_override) = low_bound_override { - time_range = low_bound_override.0.at_least(*time_range.start())..=*time_range.end(); - } + // + // TIME RANGE ALL + // - let mut time_unit = (value.0 - offset) as f32 / factor; - - let time_range = (*time_range.start() - offset) as f32 / factor - ..=(*time_range.end() - offset) as f32 / factor; - - let base_time_response = if absolute { - self.base_time.map(|base_time| { - ui.label(format!( - "{} + ", - TimeType::Time.format( - re_types_core::datatypes::TimeInt(base_time), - time_zone_for_timestamps - ) - )) - }) - } else { - None - }; + ui.horizontal(|ui| { + let mut is_time_range_all = matches!(self, Self::TimeRangeAll); + if ui + .re_radio_value(&mut is_time_range_all, true, "From –∞ to +∞") + .changed() + && is_time_range_all + { + *self = Self::TimeRangeAll; + } + }); - let drag_value_response = ui.add( - egui::DragValue::new(&mut time_unit) - .range(time_range) - .speed(speed) - .suffix(self.unit_symbol), - ); + // + // TIME RANGE CUSTOM + // - *value = re_types_core::datatypes::TimeInt((time_unit * factor).round() as i64 + offset); + ui.vertical(|ui| { + let mut is_time_range_custom = matches!(self, Self::TimeRange { .. }); + let mut changed = ui + .re_radio_value(&mut is_time_range_custom, true, "Define time range") + .changed(); + + if is_time_range_custom { + ui.spacing_mut().indent = ui.spacing().icon_width + ui.spacing().icon_spacing; + ui.indent("time_range_custom", |ui| { + let mut from = if let Self::TimeRange { from, .. } = self { + (*from).into() + } else { + (*time_drag_value.range.start()).into() + }; + + let mut to = if let Self::TimeRange { to, .. } = self { + (*to).into() + } else { + (*time_drag_value.range.end()).into() + }; + + list_item::list_item_scope(ui, "time_range_custom_scope", |ui| { + ui.list_item_flat_noninteractive( + list_item::PropertyContent::new("Start").value_fn(|ui, _| { + changed |= match time_type { + TimeType::Time => time_drag_value + .temporal_drag_value_ui( + ui, + &mut from, + true, + None, + ctx.app_options.time_zone, + ) + .0 + .changed(), + TimeType::Sequence => time_drag_value + .sequence_drag_value_ui(ui, &mut from, true, None) + .changed(), + }; + }), + ); + + ui.list_item_flat_noninteractive( + list_item::PropertyContent::new("End").value_fn(|ui, _| { + changed |= match time_type { + TimeType::Time => time_drag_value + .temporal_drag_value_ui( + ui, + &mut to, + true, + Some(from), + ctx.app_options.time_zone, + ) + .0 + .changed(), + TimeType::Sequence => time_drag_value + .sequence_drag_value_ui(ui, &mut to, true, Some(from)) + .changed(), + }; + }), + ); + }); - (drag_value_response, base_time_response) - } -} + if changed { + *self = Self::TimeRange { + from: from.into(), + to: to.into(), + }; + } + }); + } + }); + }); -fn unit_from_span(span: i64) -> (&'static str, i64) { - if span / 1_000_000_000 > 0 { - ("s", 1_000_000_000) - } else if span / 1_000_000 > 0 { - ("ms", 1_000_000) - } else if span / 1_000 > 0 { - ("μs", 1_000) - } else { - ("ns", 1) + *self != orig_self } } -/// Value of the start time over time span ratio above which an explicit offset is handled. -static SPAN_TO_START_TIME_OFFSET_THRESHOLD: i64 = 10; - -fn time_range_base_time(min_time: i64, span: i64) -> Option { - if min_time <= 0 { - return None; - } - - if span.saturating_mul(SPAN_TO_START_TIME_OFFSET_THRESHOLD) < min_time { - let factor = if span / 1_000_000 > 0 { - 1_000_000_000 - } else if span / 1_000 > 0 { - 1_000_000 - } else { - 1_000 - }; - - Some(min_time - (min_time % factor)) - } else { - None +impl From for UiQueryMode { + fn from(value: QueryMode) -> Self { + match value { + QueryMode::LatestAt { time } => Self::LatestAt { time }, + QueryMode::Range { + from: TimeInt::MIN, + to: TimeInt::MAX, + } => Self::TimeRangeAll, + QueryMode::Range { from, to } => Self::TimeRange { from, to }, + } } } -fn round_down(value: i64, factor: i64) -> i64 { - value - (value.rem_euclid(factor)) -} - -fn round_up(value: i64, factor: i64) -> i64 { - let val = round_down(value, factor); - - if val == value { - val - } else { - val + factor +impl From for QueryMode { + fn from(value: UiQueryMode) -> Self { + match value { + UiQueryMode::LatestAt { time } => QueryMode::LatestAt { time }, + UiQueryMode::TimeRangeAll => QueryMode::Range { + from: TimeInt::MIN, + to: TimeInt::MAX, + }, + UiQueryMode::TimeRange { from, to } => QueryMode::Range { from, to }, + } } } diff --git a/crates/viewer/re_viewer_context/src/time_drag_value.rs b/crates/viewer/re_viewer_context/src/time_drag_value.rs index e43143b92deb8..84e834f9d3240 100644 --- a/crates/viewer/re_viewer_context/src/time_drag_value.rs +++ b/crates/viewer/re_viewer_context/src/time_drag_value.rs @@ -130,7 +130,7 @@ impl TimeDragValue { time_range = low_bound_override.0.at_least(*time_range.start())..=*time_range.end(); } - let mut time_unit = (value.0 - offset) as f32 / factor; + let mut time_unit = (value.0.saturating_sub(offset)) as f32 / factor; let time_range = (*time_range.start() - offset) as f32 / factor ..=(*time_range.end() - offset) as f32 / factor;