use egui::{CursorIcon, Id, NumExt as _, Rect};
use re_log_types::{Duration, ResolvedTimeRangeF, TimeInt, TimeReal, TimeType};
use re_viewer_context::{Looping, TimeControl};
use super::time_ranges_ui::TimeRangesUi;
pub fn loop_selection_ui(
time_ctrl: &mut TimeControl,
time_ranges_ui: &TimeRangesUi,
ui: &egui::Ui,
time_area_painter: &egui::Painter,
timeline_rect: &Rect,
) {
if time_ctrl.loop_selection().is_none() && time_ctrl.looping() == Looping::Selection {
if let Some(selection) = initial_time_selection(time_ranges_ui, time_ctrl.time_type()) {
time_ctrl.set_loop_selection(selection);
}
}
if time_ctrl.loop_selection().is_none() && time_ctrl.looping() == Looping::Selection {
time_ctrl.set_looping(Looping::Off);
}
let is_active = time_ctrl.looping() == Looping::Selection;
let selection_color = if is_active {
re_ui::DesignTokens::loop_selection_color().gamma_multiply(0.7)
} else {
re_ui::DesignTokens::loop_selection_color().gamma_multiply(0.5)
};
let pointer_pos = ui.input(|i| i.pointer.hover_pos());
let is_pointer_in_timeline =
pointer_pos.map_or(false, |pointer_pos| timeline_rect.contains(pointer_pos));
let left_edge_id = ui.id().with("selection_left_edge");
let right_edge_id = ui.id().with("selection_right_edge");
let middle_id = ui.id().with("selection_move");
let interact_radius = ui.style().interaction.resize_grab_radius_side;
if let Some(mut selected_range) = time_ctrl.loop_selection() {
let min_x = time_ranges_ui.x_from_time(selected_range.min);
let max_x = time_ranges_ui.x_from_time(selected_range.max);
if let (Some(min_x), Some(max_x)) = (min_x, max_x) {
let mut rect =
Rect::from_x_y_ranges((min_x as f32)..=(max_x as f32), timeline_rect.y_range());
if rect.width() < 2.0 {
rect = Rect::from_x_y_ranges(
(rect.center().x - 1.0)..=(rect.center().x - 1.0),
rect.y_range(),
);
}
let full_y_range = rect.top()..=time_area_painter.clip_rect().bottom();
if is_active {
let full_rect = Rect::from_x_y_ranges(rect.x_range(), full_y_range);
let rounding = re_ui::DesignTokens::normal_rounding();
time_area_painter.rect_filled(full_rect, rounding, selection_color);
} else {
let rounding = re_ui::DesignTokens::normal_rounding();
time_area_painter.rect_filled(rect, rounding, selection_color);
}
if is_active && !selected_range.is_empty() {
paint_range_text(time_ctrl, selected_range, ui, time_area_painter, rect);
}
if is_active {
let left_edge_rect =
Rect::from_x_y_ranges(rect.left()..=rect.left(), rect.y_range())
.expand(interact_radius);
let right_edge_rect =
Rect::from_x_y_ranges(rect.right()..=rect.right(), rect.y_range())
.expand(interact_radius);
let middle_response = ui
.interact(rect, middle_id, egui::Sense::click_and_drag())
.on_hover_and_drag_cursor(CursorIcon::Move);
let left_response = ui
.interact(left_edge_rect, left_edge_id, egui::Sense::drag())
.on_hover_and_drag_cursor(CursorIcon::ResizeWest);
let right_response = ui
.interact(right_edge_rect, right_edge_id, egui::Sense::drag())
.on_hover_and_drag_cursor(CursorIcon::ResizeEast);
if left_response.dragged() {
drag_right_loop_selection_edge(
ui,
time_ranges_ui,
&mut selected_range,
right_edge_id,
);
}
if right_response.dragged() {
drag_left_loop_selection_edge(
ui,
time_ranges_ui,
&mut selected_range,
left_edge_id,
);
}
if middle_response.dragged() {
on_drag_loop_selection(ui, time_ranges_ui, &mut selected_range);
}
} else {
ui.interact(rect, middle_id, egui::Sense::hover())
.on_hover_text("Click the loop button to turn on the loop selection, or use shift-drag to select a new loop selection");
}
}
if selected_range.is_empty() && ui.ctx().dragged_id().is_none() {
time_ctrl.remove_loop_selection();
} else {
time_ctrl.set_loop_selection(selected_range);
}
}
if let Some(pointer_pos) = pointer_pos {
let is_anything_being_dragged = ui.ctx().dragged_id().is_some();
if is_pointer_in_timeline
&& !is_anything_being_dragged
&& ui.input(|i| i.pointer.primary_down() && i.modifiers.shift_only())
{
if let Some(time) = time_ranges_ui.time_from_x_f32(pointer_pos.x) {
time_ctrl.set_loop_selection(ResolvedTimeRangeF::point(time));
time_ctrl.set_looping(Looping::Selection);
ui.ctx().set_dragged_id(right_edge_id);
}
}
}
}
fn initial_time_selection(
time_ranges_ui: &TimeRangesUi,
time_type: TimeType,
) -> Option<ResolvedTimeRangeF> {
let ranges = &time_ranges_ui.segments;
for min_duration in [2.0, 0.5, 0.0] {
for segment in ranges {
let range = &segment.tight_time;
if range.min() < range.max() {
match time_type {
TimeType::Time => {
let seconds = Duration::from(range.max() - range.min()).as_secs_f64();
if seconds > min_duration {
let one_sec =
TimeInt::new_temporal(Duration::from_secs(1.0).as_nanos());
return Some(ResolvedTimeRangeF::new(
range.min(),
range.min() + one_sec,
));
}
}
TimeType::Sequence => {
return Some(ResolvedTimeRangeF::new(
range.min(),
TimeReal::from(range.min())
+ TimeReal::from((range.max() - range.min()).as_f64() / 2.0),
));
}
}
}
}
}
if ranges.len() < 2 {
None } else {
let end = (ranges.len() / 2).at_least(1);
Some(ResolvedTimeRangeF::new(
ranges[0].tight_time.min(),
ranges[end].tight_time.max(),
))
}
}
fn drag_right_loop_selection_edge(
ui: &egui::Ui,
time_ranges_ui: &TimeRangesUi,
selected_range: &mut ResolvedTimeRangeF,
right_edge_id: Id,
) -> Option<()> {
use egui::emath::smart_aim::best_in_range_f64;
let pointer_pos = ui.input(|i| i.pointer.hover_pos())?;
let aim_radius = ui.input(|i| i.aim_radius());
let time_low = time_ranges_ui.time_from_x_f32(pointer_pos.x - aim_radius)?;
let time_high = time_ranges_ui.time_from_x_f32(pointer_pos.x + aim_radius)?;
let low_length = selected_range.max - time_low;
let high_length = selected_range.max - time_high;
let best_length = TimeReal::from(best_in_range_f64(low_length.as_f64(), high_length.as_f64()));
selected_range.min = selected_range.max - best_length;
if selected_range.min > selected_range.max {
std::mem::swap(&mut selected_range.min, &mut selected_range.max);
ui.ctx().set_dragged_id(right_edge_id);
}
Some(())
}
fn drag_left_loop_selection_edge(
ui: &egui::Ui,
time_ranges_ui: &TimeRangesUi,
selected_range: &mut ResolvedTimeRangeF,
left_edge_id: Id,
) -> Option<()> {
use egui::emath::smart_aim::best_in_range_f64;
let pointer_pos = ui.input(|i| i.pointer.hover_pos())?;
let aim_radius = ui.input(|i| i.aim_radius());
let time_low = time_ranges_ui.time_from_x_f32(pointer_pos.x - aim_radius)?;
let time_high = time_ranges_ui.time_from_x_f32(pointer_pos.x + aim_radius)?;
let low_length = time_low - selected_range.min;
let high_length = time_high - selected_range.min;
let best_length = TimeReal::from(best_in_range_f64(low_length.as_f64(), high_length.as_f64()));
selected_range.max = selected_range.min + best_length;
if selected_range.min > selected_range.max {
std::mem::swap(&mut selected_range.min, &mut selected_range.max);
ui.ctx().set_dragged_id(left_edge_id);
}
Some(())
}
fn on_drag_loop_selection(
ui: &egui::Ui,
time_ranges_ui: &TimeRangesUi,
selected_range: &mut ResolvedTimeRangeF,
) -> Option<()> {
let pointer_delta = ui.input(|i| i.pointer.delta());
let min_x = time_ranges_ui.x_from_time_f32(selected_range.min)? + pointer_delta.x;
let max_x = time_ranges_ui.x_from_time_f32(selected_range.max)? + pointer_delta.x;
let min_time = time_ranges_ui.time_from_x_f32(min_x)?;
let max_time = time_ranges_ui.time_from_x_f32(max_x)?;
let mut new_range = ResolvedTimeRangeF::new(min_time, max_time);
if egui::emath::almost_equal(
selected_range.length().as_f32(),
new_range.length().as_f32(),
1e-5,
) {
new_range.max = new_range.min + selected_range.length();
}
*selected_range = new_range;
Some(())
}
fn paint_range_text(
time_ctrl: &TimeControl,
selected_range: ResolvedTimeRangeF,
ui: &egui::Ui,
painter: &egui::Painter,
selection_rect: Rect,
) {
use egui::{Pos2, Stroke};
let text_color = ui.visuals().strong_text_color();
let arrow_color = text_color.gamma_multiply(0.75);
let arrow_stroke = Stroke::new(1.0, arrow_color);
fn paint_arrow_from_to(painter: &egui::Painter, origin: Pos2, to: Pos2, stroke: Stroke) {
use egui::emath::Rot2;
let vec = to - origin;
let rot = Rot2::from_angle(std::f32::consts::TAU / 10.0);
let tip_length = 6.0;
let tip = origin + vec;
let dir = vec.normalized();
painter.line_segment([origin, tip], stroke);
painter.line_segment([tip, tip - tip_length * (rot * dir)], stroke);
painter.line_segment([tip, tip - tip_length * (rot.inverse() * dir)], stroke);
}
let range_text = format_duration(time_ctrl.time_type(), selected_range.length().abs());
if range_text.is_empty() {
return;
}
let font_id = egui::TextStyle::Small.resolve(ui.style());
let text_rect = painter.text(
selection_rect.center(),
egui::Align2::CENTER_CENTER,
range_text,
font_id,
text_color,
);
let text_rect = text_rect.expand(2.0); let selection_rect = selection_rect.shrink(1.0); let min_arrow_length = 12.0;
if selection_rect.left() + min_arrow_length <= text_rect.left() {
paint_arrow_from_to(
painter,
text_rect.left_center(),
selection_rect.left_center(),
arrow_stroke,
);
}
if text_rect.right() + min_arrow_length <= selection_rect.right() {
paint_arrow_from_to(
painter,
text_rect.right_center(),
selection_rect.right_center(),
arrow_stroke,
);
}
}
fn format_duration(time_typ: TimeType, duration: TimeReal) -> String {
match time_typ {
TimeType::Time => Duration::from(duration).to_string(),
TimeType::Sequence => duration.round().as_i64().to_string(), }
}