use std::fmt::{Display, Formatter};
use itertools::Itertools as _;
use re_entity_db::InstancePath;
use re_log_types::EntityPath;
use re_ui::UiExt as _;
use crate::{Contents, Item, ItemCollection};
#[derive(Debug)]
pub enum DragAndDropPayload {
Contents { contents: Vec<Contents> },
Entities { entities: Vec<EntityPath> },
Invalid,
}
impl DragAndDropPayload {
pub fn from_items(selected_items: &ItemCollection) -> Self {
if let Some(contents) = try_item_collection_to_contents(selected_items) {
Self::Contents { contents }
} else if let Some(entities) = try_item_collection_to_entities(selected_items) {
Self::Entities { entities }
} else {
Self::Invalid
}
}
}
fn try_item_collection_to_contents(items: &ItemCollection) -> Option<Vec<Contents>> {
items.iter().map(|(item, _)| item.try_into().ok()).collect()
}
fn try_item_collection_to_entities(items: &ItemCollection) -> Option<Vec<EntityPath>> {
items
.iter()
.map(|(item, _)| match item {
Item::InstancePath(instance_path) | Item::DataResult(_, instance_path) => instance_path
.is_all()
.then(|| instance_path.entity_path.clone()),
_ => None,
})
.collect()
}
impl std::fmt::Display for DragAndDropPayload {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let mut item_counter = ItemCounter::default();
match self {
Self::Contents { contents } => {
for content in contents {
item_counter.add(&content.as_item());
}
}
Self::Entities { entities } => {
for entity in entities {
item_counter.add(&Item::InstancePath(InstancePath::from(entity.clone())));
}
}
Self::Invalid => {}
}
item_counter.fmt(f)
}
}
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
pub enum DragAndDropFeedback {
#[default]
Ignore,
Accept,
Reject,
}
pub struct DragAndDropManager {
undraggable_items: ItemCollection,
feedback: crossbeam::atomic::AtomicCell<DragAndDropFeedback>,
}
impl DragAndDropManager {
pub fn new(undraggable_items: impl Into<ItemCollection>) -> Self {
Self {
undraggable_items: undraggable_items.into(),
feedback: Default::default(),
}
}
pub fn set_feedback(&self, feedback: DragAndDropFeedback) {
self.feedback.store(feedback);
}
pub fn are_items_draggable(&self, items: &ItemCollection) -> bool {
self.undraggable_items
.iter_items()
.all(|item| !items.contains_item(item))
}
pub fn payload_cursor_ui(&self, ctx: &egui::Context) {
if let Some(payload) = egui::DragAndDrop::payload::<DragAndDropPayload>(ctx) {
if let Some(pointer_pos) = ctx.pointer_interact_pos() {
let icon = match payload.as_ref() {
DragAndDropPayload::Contents { .. } => &re_ui::icons::DND_MOVE,
DragAndDropPayload::Entities { .. } => &re_ui::icons::DND_ADD_TO_EXISTING,
DragAndDropPayload::Invalid => return,
};
let layer_id = egui::LayerId::new(
egui::Order::Tooltip,
egui::Id::new("drag_and_drop_payload_layer"),
);
let mut ui = egui::Ui::new(
ctx.clone(),
egui::Id::new("rerun_drag_and_drop_payload_ui"),
egui::UiBuilder::new().layer_id(layer_id),
);
let feedback = self.feedback.load();
match feedback {
DragAndDropFeedback::Accept => {
ctx.set_cursor_icon(egui::CursorIcon::Grabbing);
ui.set_opacity(0.8);
}
DragAndDropFeedback::Ignore => {
ctx.set_cursor_icon(egui::CursorIcon::Grabbing);
ui.set_opacity(0.5);
}
DragAndDropFeedback::Reject => {
ctx.set_cursor_icon(egui::CursorIcon::NoDrop);
ui.set_opacity(0.5);
}
}
let payload_is_currently_droppable = feedback == DragAndDropFeedback::Accept;
let response = drag_pill_frame(ui.tokens(), payload_is_currently_droppable)
.show(&mut ui, |ui| {
let text_color = ui.visuals().widgets.inactive.text_color();
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 2.0;
ui.small_icon(icon, Some(text_color));
ui.label(egui::RichText::new(payload.to_string()).color(text_color));
});
})
.response;
let delta = pointer_pos - response.rect.right_bottom();
ctx.transform_layer_shapes(layer_id, emath::TSTransform::from_translation(delta));
}
}
}
}
fn drag_pill_frame(tokens: &re_ui::DesignTokens, droppable: bool) -> egui::Frame {
egui::Frame {
fill: if droppable {
tokens.drag_pill_droppable_fill
} else {
tokens.drag_pill_nondroppable_fill
},
stroke: egui::Stroke::new(
1.0,
if droppable {
tokens.drag_pill_droppable_stroke
} else {
tokens.drag_pill_nondroppable_stroke
},
),
corner_radius: 2.into(),
inner_margin: egui::Margin {
left: 6,
right: 9,
top: 5,
bottom: 4,
},
outer_margin: egui::Margin::same(1),
..Default::default()
}
}
#[derive(Debug, Default)]
struct ItemCounter {
container_cnt: u32,
view_cnt: u32,
app_cnt: u32,
table_cnt: u32,
data_source_cnt: u32,
store_cnt: u32,
entity_cnt: u32,
instance_cnt: u32,
component_cnt: u32,
redap_server_cnt: u32,
redap_entry_cnt: u32,
}
impl ItemCounter {
fn add(&mut self, item: &Item) {
match item {
Item::Container(_) => self.container_cnt += 1,
Item::View(_) => self.view_cnt += 1,
Item::AppId(_) => self.app_cnt += 1,
Item::TableId(_) => self.table_cnt += 1,
Item::DataSource(_) => self.data_source_cnt += 1,
Item::StoreId(_) => self.store_cnt += 1,
Item::InstancePath(instance_path) | Item::DataResult(_, instance_path) => {
if instance_path.is_all() {
self.entity_cnt += 1;
} else {
self.instance_cnt += 1;
}
}
Item::ComponentPath(_) => self.component_cnt += 1,
Item::RedapServer(_) => self.redap_server_cnt += 1,
Item::RedapEntry(_) => self.redap_entry_cnt += 1,
}
}
}
impl Display for ItemCounter {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let count_and_names = [
(&self.container_cnt, "container", "containers"),
(&self.view_cnt, "view", "views"),
(&self.app_cnt, "app", "apps"),
(&self.data_source_cnt, "data source", "data sources"),
(&self.store_cnt, "store", "stores"),
(&self.entity_cnt, "entity", "entities"),
(&self.instance_cnt, "instance", "instances"),
(&self.component_cnt, "component", "components"),
];
count_and_names
.into_iter()
.filter_map(|(&count, name_singular, name_plural)| {
if count > 0 {
Some(format!(
"{} {}",
re_format::format_uint(count),
if count == 1 {
name_singular
} else {
name_plural
},
))
} else {
None
}
})
.join(", ")
.fmt(f)
}
}