use egui::IntoAtoms as _;
use re_ui::{UiExt as _, menu::menu_style};
use re_viewer_context::{Item, ItemCollection, ItemCounter, ViewerContext};
use re_viewport_blueprint::ViewportBlueprint;
use re_data_ui::item_title::QualifiedItemTitle;
use re_ui::HasDesignTokens as _;
use crate::selection_history::SelectionHistory;
const BUTTON_SIZE: f32 = 16.0;
const ICON_SIZE: f32 = 12.0;
pub fn selection_history_ui(
ctx: &ViewerContext<'_>,
viewport: &ViewportBlueprint,
ui: &mut egui::Ui,
history: &mut SelectionHistory,
) -> Option<ItemCollection> {
let prev = history_button_ui(
ctx,
viewport,
ui,
history,
&re_ui::icons::BACK_SMALL,
Direction::Back,
);
let next = history_button_ui(
ctx,
viewport,
ui,
history,
&re_ui::icons::FORWARD_SMALL,
Direction::Forward,
);
prev.or(next)
}
#[derive(Clone, Copy)]
enum Direction {
Back,
Forward,
}
fn history_button_ui(
ctx: &ViewerContext<'_>,
viewport: &ViewportBlueprint,
ui: &mut egui::Ui,
history: &mut SelectionHistory,
icon: &re_ui::Icon,
direction: Direction,
) -> Option<ItemCollection> {
let has_target = match direction {
Direction::Back => history.previous().is_some(),
Direction::Forward => history.next().is_some(),
};
let button_size = egui::vec2(BUTTON_SIZE, BUTTON_SIZE);
let (rect, response) = ui.allocate_exact_size(button_size, egui::Sense::click());
response.widget_info(|| egui::WidgetInfo::new(egui::WidgetType::Button));
if ui.is_rect_visible(rect) {
let tokens = ui.tokens();
let is_hovered = has_target && response.hovered();
let fill = if is_hovered {
tokens.history_button_fill_hovered
} else {
tokens.history_button_fill
};
let icon_tint = if has_target {
tokens.history_button_icon_active
} else {
tokens.history_button_icon_inactive
};
let corner_radius = 4.0;
ui.painter().rect_filled(rect, corner_radius, fill);
let icon_rect =
egui::Rect::from_center_size(rect.center(), egui::vec2(ICON_SIZE, ICON_SIZE));
icon.as_image().tint(icon_tint).paint_at(ui, icon_rect);
}
if has_target && response.hovered() {
ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
}
if has_target {
let (dir_label, target_sel) = match direction {
Direction::Back => ("previous", history.previous().map(|(_, s)| s)),
Direction::Forward => ("next", history.next().map(|(_, s)| s)),
};
if let Some(title) = target_sel.and_then(|sel| EntryTitle::new(ctx, viewport, sel)) {
let desc = title.text();
response
.clone()
.on_hover_text(format!("Go to {dir_label} selection:\n{desc}"));
}
} else {
let tip = match direction {
Direction::Back => "No previous selections",
Direction::Forward => "No future selections",
};
response.clone().on_disabled_hover_text(tip);
}
if has_target {
let mut navigated = false;
egui::Popup::menu(&response)
.style(menu_style())
.open_memory(if response.secondary_clicked() {
Some(egui::SetOpenCommand::Bool(true))
} else if response.clicked() {
Some(egui::SetOpenCommand::Bool(false))
} else {
None
})
.show(|ui| {
let cur = history.current;
let indices: Vec<usize> = match direction {
Direction::Back => (0..cur).rev().collect(),
Direction::Forward => ((cur + 1)..history.stack.len()).collect(),
};
for i in indices {
navigated |= history_item_ui(ctx, viewport, ui, i, history);
}
});
if navigated {
return history.current().cloned();
}
}
if has_target && response.clicked() {
return match direction {
Direction::Back => history.select_previous(),
Direction::Forward => history.select_next(),
};
}
None
}
fn history_item_ui(
ctx: &ViewerContext<'_>,
viewport: &ViewportBlueprint,
ui: &mut egui::Ui,
index: usize,
history: &mut SelectionHistory,
) -> bool {
let Some(selection) = history.stack.get(index).cloned() else {
return false;
};
let Some(title) = EntryTitle::new(ctx, viewport, &selection) else {
return false;
};
if ui.add(egui::Button::new(title.atoms)).clicked() {
history.current = index;
ui.close();
true
} else {
false
}
}
struct EntryTitle {
atoms: egui::Atoms<'static>,
}
impl EntryTitle {
fn new(
ctx: &ViewerContext<'_>,
viewport: &ViewportBlueprint,
selection: &ItemCollection,
) -> Option<Self> {
let style = ctx.egui_ctx().global_style();
let icon_tint = style.tokens().label_button_icon_color;
let items: Vec<&Item> = selection.iter_items().collect();
let first = *items.first()?;
let title = QualifiedItemTitle::from_item(ctx, viewport, &style, first);
let atoms = if items.len() == 1 {
title.into_atoms(&style, icon_tint)
} else {
items
.iter()
.copied()
.collect::<ItemCounter>()
.to_string()
.into_atoms()
};
Some(Self { atoms })
}
fn text(&self) -> String {
self.atoms.text().unwrap_or_default().to_string()
}
}