use egui::{CursorIcon, Id, NumExt as _, Rect};
use re_data_store::LogDb;
use re_log_types::{Duration, TimeInt, TimeRangeF, TimeReal, TimeType};
use re_viewer_context::{Looping, TimeControl};
use super::{is_time_safe_to_show, time_ranges_ui::TimeRangesUi};
pub fn loop_selection_ui(
log_db: &LogDb,
time_ctrl: &mut TimeControl,
time_ranges_ui: &TimeRangesUi,
ui: &mut egui::Ui,
time_area_painter: &egui::Painter,
timeline_rect: &Rect,
) {
let timeline = *time_ctrl.timeline();
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::ReUi::loop_selection_color().gamma_multiply(0.7)
} else {
re_ui::ReUi::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::ReUi::normal_rounding();
time_area_painter.rect_filled(full_rect, rounding, selection_color);
} else {
let rounding = re_ui::ReUi::normal_rounding();
time_area_painter.rect_filled(rect, rounding, selection_color);
}
if is_active
&& !selected_range.is_empty()
&& is_time_safe_to_show(log_db, &timeline, selected_range.min)
&& is_time_safe_to_show(log_db, &timeline, selected_range.max)
{
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.memory(|mem| mem.is_anything_being_dragged()) {
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.memory(|mem| mem.is_anything_being_dragged());
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(TimeRangeF::point(time));
time_ctrl.set_looping(Looping::Selection);
ui.memory_mut(|mem| mem.set_dragged_id(right_edge_id));
}
}
}
}
fn initial_time_selection(
time_ranges_ui: &TimeRangesUi,
time_type: TimeType,
) -> Option<TimeRangeF> {
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::from(Duration::from_secs(1.0));
return Some(TimeRangeF::new(range.min, range.min + one_sec));
}
}
TimeType::Sequence => {
return Some(TimeRangeF::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(TimeRangeF::new(
ranges[0].tight_time.min,
ranges[end].tight_time.max,
))
}
}
fn drag_right_loop_selection_edge(
ui: &mut egui::Ui,
time_ranges_ui: &TimeRangesUi,
selected_range: &mut TimeRangeF,
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.memory_mut(|mem| mem.set_dragged_id(right_edge_id));
}
Some(())
}
fn drag_left_loop_selection_edge(
ui: &mut egui::Ui,
time_ranges_ui: &TimeRangesUi,
selected_range: &mut TimeRangeF,
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.memory_mut(|mem| mem.set_dragged_id(left_edge_id));
}
Some(())
}
fn on_drag_loop_selection(
ui: &mut egui::Ui,
time_ranges_ui: &TimeRangesUi,
selected_range: &mut TimeRangeF,
) -> 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 = TimeRangeF::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: &mut TimeControl,
selected_range: TimeRangeF,
ui: &mut egui::Ui,
painter: &egui::Painter,
selection_rect: Rect,
) {
use egui::{Pos2, Stroke};
if selected_range.min <= TimeInt::BEGINNING {
return; }
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(), }
}