use std::collections::HashSet;
use std::ops::RangeInclusive;
use egui::{NumExt as _, Response, Ui};
use re_entity_db::{TimeHistogram, VisibleHistory, VisibleHistoryBoundary};
use re_log_types::{TimeType, TimeZone};
use re_space_view::{
time_range_boundary_to_visible_history_boundary,
visible_history_boundary_to_time_range_boundary, visible_time_range_to_time_range,
};
use re_space_view_spatial::{SpatialSpaceView2D, SpatialSpaceView3D};
use re_space_view_time_series::TimeSeriesSpaceView;
use re_types::blueprint::components::VisibleTimeRange;
use re_types_core::Loggable as _;
use re_ui::{markdown_ui, ReUi};
use re_viewer_context::{SpaceViewClass, SpaceViewClassIdentifier, ViewerContext};
static VISIBLE_HISTORY_SUPPORTED_SPACE_VIEWS: once_cell::sync::Lazy<
HashSet<SpaceViewClassIdentifier>,
> = once_cell::sync::Lazy::new(|| {
[
SpatialSpaceView3D::identifier(),
SpatialSpaceView2D::identifier(),
TimeSeriesSpaceView::identifier(),
]
.map(Into::into)
.into()
});
fn space_view_with_visible_history(space_view_class: SpaceViewClassIdentifier) -> bool {
VISIBLE_HISTORY_SUPPORTED_SPACE_VIEWS.contains(&space_view_class)
}
pub fn visual_time_range_ui(
ctx: &ViewerContext<'_>,
ui: &mut Ui,
data_result_tree: Option<&re_viewer_context::DataResultTree>,
data_result: &re_viewer_context::DataResult,
space_view_class: SpaceViewClassIdentifier,
is_space_view: bool,
) {
let time_ctrl = ctx.rec_cfg.time_ctrl.read().clone();
if is_space_view && !space_view_with_visible_history(space_view_class) {
return;
}
let re_ui = ctx.re_ui;
let time_type = time_ctrl.timeline().typ();
let mut interacting_with_controls = false;
let space_view_class = ctx
.space_view_class_registry
.get_class_or_log_error(&space_view_class);
let mut resolved_range = data_result
.lookup_override::<VisibleTimeRange>(ctx)
.unwrap_or(space_view_class.default_visible_time_range());
let mut has_individual_range = if let Some(data_result_tree) = data_result_tree {
data_result
.component_override_source(data_result_tree, &VisibleTimeRange::name())
.as_ref()
== Some(&data_result.entity_path)
} else {
data_result
.lookup_override::<VisibleTimeRange>(ctx)
.is_some()
};
let collapsing_response = re_ui.collapsing_header(ui, "Visible time range", false, |ui| {
let has_individual_range_before = has_individual_range;
let resolved_range_before = resolved_range.clone();
ui.horizontal(|ui| {
re_ui
.radio_value(ui, &mut has_individual_range, false, "Default")
.on_hover_text(if is_space_view {
"Default visible time range settings for this kind of space view"
} else {
"Visible time range settings inherited from parent Entity or enclosing \
space view"
});
re_ui
.radio_value(ui, &mut has_individual_range, true, "Override")
.on_hover_text(if is_space_view {
"Set visible time range settings for the contents of this space view"
} else {
"Set visible time range settings for this entity"
});
});
let timeline_spec =
if let Some(times) = ctx.recording().time_histogram(time_ctrl.timeline()) {
TimelineSpec::from_time_histogram(times)
} else {
TimelineSpec::from_time_range(0..=0)
};
let current_time = time_ctrl
.time_i64()
.unwrap_or_default()
.at_least(*timeline_spec.range.start());
let (from, to) = match time_type {
TimeType::Time => (
&mut resolved_range.0.from_time,
&mut resolved_range.0.to_time,
),
TimeType::Sequence => (
&mut resolved_range.0.from_sequence,
&mut resolved_range.0.to_sequence,
),
};
let mut visible_history = VisibleHistory {
from: time_range_boundary_to_visible_history_boundary(from),
to: time_range_boundary_to_visible_history_boundary(to),
};
if has_individual_range {
let current_from = visible_history
.range_start_from_cursor(current_time.into())
.as_i64();
let current_to = visible_history
.range_end_from_cursor(current_time.into())
.as_i64();
egui::Grid::new("from_to_editable").show(ui, |ui| {
re_ui.grid_left_hand_label(ui, "From");
interacting_with_controls |= ui
.horizontal(|ui| {
visible_history_boundary_ui(
ctx,
ui,
&mut visible_history.from,
time_type,
current_time,
&timeline_spec,
true,
current_to,
)
})
.inner;
ui.end_row();
re_ui.grid_left_hand_label(ui, "To");
interacting_with_controls |= ui
.horizontal(|ui| {
visible_history_boundary_ui(
ctx,
ui,
&mut visible_history.to,
time_type,
current_time,
&timeline_spec,
false,
current_from,
)
})
.inner;
ui.end_row();
});
current_range_ui(ctx, ui, current_time, time_type, &visible_history);
} else {
if visible_history.from == VisibleHistoryBoundary::Infinite
&& visible_history.to == VisibleHistoryBoundary::Infinite
{
ui.label("Entire timeline");
} else if visible_history.from == VisibleHistoryBoundary::AT_CURSOR
&& visible_history.to == VisibleHistoryBoundary::AT_CURSOR
{
let current_time = time_type.format(current_time.into(), ctx.app_options.time_zone);
match time_type {
TimeType::Time => {
ui.label(format!("At current time: {current_time}"));
}
TimeType::Sequence => {
ui.label(format!("At current frame: {current_time}"));
}
}
} else {
egui::Grid::new("from_to_labels").show(ui, |ui| {
re_ui.grid_left_hand_label(ui, "From");
resolved_visible_history_boundary_ui(
ctx,
ui,
&visible_history.from,
time_type,
true,
);
ui.end_row();
re_ui.grid_left_hand_label(ui, "To");
resolved_visible_history_boundary_ui(
ctx,
ui,
&visible_history.to,
time_type,
false,
);
ui.end_row();
});
current_range_ui(ctx, ui, current_time, time_type, &visible_history);
}
}
*from = visible_history_boundary_to_time_range_boundary(&visible_history.from);
*to = visible_history_boundary_to_time_range_boundary(&visible_history.to);
if has_individual_range != has_individual_range_before
|| resolved_range != resolved_range_before
{
if has_individual_range {
re_log::debug!("override!");
data_result.save_recursive_override(ctx, &resolved_range);
} else {
re_log::debug!("clear!");
data_result.clear_recursive_override::<VisibleTimeRange>(ctx);
}
}
});
if collapsing_response.body_response.is_some() {
ui.add_space(ui.spacing().item_spacing.y / 2.0);
} else {
ui.add_space(-ui.spacing().item_spacing.y / 2.0);
}
ReUi::full_span_separator(ui);
ui.add_space(ui.spacing().item_spacing.y / 2.0);
let should_display_visible_history = interacting_with_controls
|| collapsing_response.header_response.hovered()
|| collapsing_response
.body_response
.map_or(false, |r| r.hovered());
if should_display_visible_history {
if let Some(current_time) = time_ctrl.time_int() {
let range = visible_time_range_to_time_range(&resolved_range, time_type, current_time);
ctx.rec_cfg.time_ctrl.write().highlighted_range = Some(range);
}
}
let markdown = format!("# Visible time range\n
This feature controls the time range used to display data in the space view.
The settings are inherited from the parent Entity or enclosing space view if not overridden.
Visible time range properties are stored separately for each _type_ of timelines. They may differ depending on \
whether the current timeline is temporal or a sequence. The current settings apply to all _{}_ timelines.
Notes that the data current as of the time range starting time is included.",
match time_type {
TimeType::Time => "temporal",
TimeType::Sequence => "sequence",
}
);
collapsing_response.header_response.on_hover_ui(|ui| {
markdown_ui(ui, egui::Id::new(markdown.as_str()), &markdown);
});
}
fn current_range_ui(
ctx: &ViewerContext<'_>,
ui: &mut Ui,
current_time: i64,
time_type: TimeType,
visible_history: &VisibleHistory,
) {
let time_range = visible_history.time_range(current_time.into());
let from_formatted = time_type.format(time_range.min, ctx.app_options.time_zone);
let to_formatted = time_type.format(time_range.max, ctx.app_options.time_zone);
ui.label(format!("{from_formatted} to {to_formatted}"))
.on_hover_text("Showing data in this range (inclusive).");
}
#[allow(clippy::too_many_arguments)]
fn resolved_visible_history_boundary_ui(
ctx: &ViewerContext<'_>,
ui: &mut egui::Ui,
visible_history_boundary: &VisibleHistoryBoundary,
time_type: TimeType,
low_bound: bool,
) {
let boundary_type = match visible_history_boundary {
VisibleHistoryBoundary::RelativeToTimeCursor(_) => match time_type {
TimeType::Time => "current time",
TimeType::Sequence => "current frame",
},
VisibleHistoryBoundary::Absolute(_) => match time_type {
TimeType::Time => "absolute time",
TimeType::Sequence => "frame",
},
VisibleHistoryBoundary::Infinite => {
if low_bound {
"beginning of timeline"
} else {
"end of timeline"
}
}
};
let mut label = boundary_type.to_owned();
match visible_history_boundary {
VisibleHistoryBoundary::RelativeToTimeCursor(offset) => {
if *offset != 0 {
match time_type {
TimeType::Time => {
let (unit, factor) = if offset % 1_000_000_000 == 0 {
("s", 1_000_000_000.)
} else if offset % 1_000_000 == 0 {
("ms", 1_000_000.)
} else if offset % 1_000 == 0 {
("μs", 1_000.)
} else {
("ns", 1.)
};
label += &format!(" with {} {} offset", *offset as f64 / factor, unit);
}
TimeType::Sequence => {
label += &format!(
" with {offset} frame{} offset",
if offset.abs() > 1 { "s" } else { "" }
);
}
}
}
}
VisibleHistoryBoundary::Absolute(time) => {
label += &format!(
" {}",
time_type.format((*time).into(), ctx.app_options.time_zone)
);
}
VisibleHistoryBoundary::Infinite => {}
}
ui.label(label);
}
fn visible_history_boundary_combo_label(
boundary: &VisibleHistoryBoundary,
time_type: TimeType,
low_bound: bool,
) -> &'static str {
match boundary {
VisibleHistoryBoundary::RelativeToTimeCursor(_) => match time_type {
TimeType::Time => "current time with offset",
TimeType::Sequence => "current frame with offset",
},
VisibleHistoryBoundary::Absolute(_) => match time_type {
TimeType::Time => "absolute time",
TimeType::Sequence => "absolute frame",
},
VisibleHistoryBoundary::Infinite => {
if low_bound {
"beginning of timeline"
} else {
"end of timeline"
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn visible_history_boundary_ui(
ctx: &ViewerContext<'_>,
ui: &mut egui::Ui,
visible_history_boundary: &mut VisibleHistoryBoundary,
time_type: TimeType,
current_time: i64,
timeline_spec: &TimelineSpec,
low_bound: bool,
other_boundary_absolute: i64,
) -> bool {
let (abs_time, rel_time) = match visible_history_boundary {
VisibleHistoryBoundary::RelativeToTimeCursor(value) => (*value + current_time, *value),
VisibleHistoryBoundary::Absolute(value) => (*value, *value - current_time),
VisibleHistoryBoundary::Infinite => (current_time, 0),
};
let abs_time = VisibleHistoryBoundary::Absolute(abs_time);
let rel_time = VisibleHistoryBoundary::RelativeToTimeCursor(rel_time);
egui::ComboBox::from_id_source(if low_bound {
"time_history_low_bound"
} else {
"time_history_high_bound"
})
.selected_text(visible_history_boundary_combo_label(
visible_history_boundary,
time_type,
low_bound,
))
.show_ui(ui, |ui| {
ui.set_min_width(160.0);
ui.selectable_value(
visible_history_boundary,
rel_time,
visible_history_boundary_combo_label(&rel_time, time_type, low_bound),
)
.on_hover_text(if low_bound {
"Show data from a time point relative to the current time."
} else {
"Show data until a time point relative to the current time."
});
ui.selectable_value(
visible_history_boundary,
abs_time,
visible_history_boundary_combo_label(&abs_time, time_type, low_bound),
)
.on_hover_text(if low_bound {
"Show data from an absolute time point."
} else {
"Show data until an absolute time point."
});
ui.selectable_value(
visible_history_boundary,
VisibleHistoryBoundary::Infinite,
visible_history_boundary_combo_label(
&VisibleHistoryBoundary::Infinite,
time_type,
low_bound,
),
)
.on_hover_text(if low_bound {
"Show data from the beginning of the timeline"
} else {
"Show data until the end of the timeline"
});
});
let response = match visible_history_boundary {
VisibleHistoryBoundary::RelativeToTimeCursor(value) => {
let low_bound_override = if low_bound {
None
} else {
Some(other_boundary_absolute.saturating_sub(current_time))
};
match time_type {
TimeType::Time => Some(
timeline_spec
.temporal_drag_value(
ui,
value,
false,
low_bound_override,
ctx.app_options.time_zone,
)
.0
.on_hover_text(
"Time duration before/after the current time to use as time range \
boundary",
),
),
TimeType::Sequence => Some(
timeline_spec
.sequence_drag_value(ui, value, false, low_bound_override)
.on_hover_text(
"Number of frames before/after the current time to use a time \
range boundary",
),
),
}
}
VisibleHistoryBoundary::Absolute(value) => {
let low_bound_override = if low_bound {
None
} else {
Some(other_boundary_absolute)
};
match time_type {
TimeType::Time => {
let (drag_resp, base_time_resp) = timeline_spec.temporal_drag_value(
ui,
value,
true,
low_bound_override,
ctx.app_options.time_zone,
);
if let Some(base_time_resp) = base_time_resp {
base_time_resp.on_hover_text("Base time used to set time range boundaries");
}
Some(drag_resp.on_hover_text("Absolute time to use as time range boundary"))
}
TimeType::Sequence => Some(
timeline_spec
.sequence_drag_value(ui, value, true, low_bound_override)
.on_hover_text("Absolute frame number to use as time range boundary"),
),
}
}
VisibleHistoryBoundary::Infinite => None,
};
response.map_or(false, |r| r.dragged() || r.has_focus())
}
#[derive(Debug)]
struct TimelineSpec {
range: RangeInclusive<i64>,
base_time: Option<i64>,
unit_factor: i64,
unit_symbol: &'static str,
abs_range: RangeInclusive<i64>,
rel_range: RangeInclusive<i64>,
}
impl TimelineSpec {
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<i64>) -> 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);
let abs_range =
round_down(*range.start(), unit_factor)..=round_up(*range.end(), unit_factor);
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,
ui: &mut egui::Ui,
value: &mut i64,
absolute: bool,
low_bound_override: Option<i64>,
) -> Response {
let mut time_range = if absolute {
self.abs_range.clone()
} else {
self.rel_range.clone()
};
let span = time_range.end() - time_range.start();
let speed = (span as f32 * 0.005).at_least(1.0);
if let Some(low_bound_override) = low_bound_override {
time_range = low_bound_override.at_least(*time_range.start())..=*time_range.end();
}
ui.add(
egui::DragValue::new(value)
.clamp_range(time_range)
.speed(speed),
)
}
fn temporal_drag_value(
&self,
ui: &mut egui::Ui,
value: &mut i64,
absolute: bool,
low_bound_override: Option<i64>,
time_zone_for_timestamps: TimeZone,
) -> (Response, Option<Response>) {
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
};
let speed = (time_range.end() - time_range.start()) as f32 / factor * 0.005;
if let Some(low_bound_override) = low_bound_override {
time_range = low_bound_override.at_least(*time_range.start())..=*time_range.end();
}
let mut time_unit = (*value - 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(base_time.into(), time_zone_for_timestamps)
))
})
} else {
None
};
let drag_value_response = ui.add(
egui::DragValue::new(&mut time_unit)
.clamp_range(time_range)
.speed(speed)
.suffix(self.unit_symbol),
);
*value = (time_unit * factor).round() as i64 + offset;
(drag_value_response, base_time_response)
}
}
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)
}
}
static SPAN_TO_START_TIME_OFFSET_THRESHOLD: i64 = 10;
fn time_range_base_time(min_time: i64, span: i64) -> Option<i64> {
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
}
}
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
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_round_down() {
assert_eq!(round_down(2200, 1000), 2000);
assert_eq!(round_down(2000, 1000), 2000);
assert_eq!(round_down(-2200, 1000), -3000);
assert_eq!(round_down(-3000, 1000), -3000);
assert_eq!(round_down(0, 1000), 0);
}
#[test]
fn test_round_up() {
assert_eq!(round_up(2200, 1000), 3000);
assert_eq!(round_up(2000, 1000), 2000);
assert_eq!(round_up(-2200, 1000), -2000);
assert_eq!(round_up(-3000, 1000), -3000);
assert_eq!(round_up(0, 1000), 0);
}
}