use ahash::HashSet;
use egui::containers::menu::{MenuButton, MenuConfig};
use egui::emath::GuiRounding as _;
use egui::{Color32, Frame, Id, PopupCloseBehavior, RichText, Stroke, Style};
use re_ui::{UiExt as _, design_tokens_of, icons};
pub const CELL_SEPARATOR_STROKE_OFFSET: f32 = 0.5;
pub fn apply_table_style_fixes(style: &mut Style) {
let theme = if style.visuals.dark_mode {
egui::Theme::Dark
} else {
egui::Theme::Light
};
let design_tokens = design_tokens_of(theme);
style.visuals.widgets.hovered.bg_stroke =
Stroke::new(1.0, design_tokens.table_interaction_hovered_bg_stroke);
style.visuals.widgets.active.bg_stroke =
Stroke::new(1.0, design_tokens.table_interaction_active_bg_stroke);
style.visuals.widgets.noninteractive.bg_stroke = Stroke::new(0.0, Color32::TRANSPARENT);
}
pub fn header_title(
ui: &mut egui::Ui,
table_style: re_ui::TableStyle,
title: impl Into<RichText>,
) -> egui::Response {
header_ui(ui, table_style, false, |ui| {
ui.monospace(title.into().strong());
})
.response
}
pub fn header_ui<R>(
ui: &mut egui::Ui,
table_style: re_ui::TableStyle,
connected_to_next_cell: bool,
content: impl FnOnce(&mut egui::Ui) -> R,
) -> egui::InnerResponse<R> {
let rect = ui
.max_rect()
.round_to_pixels(ui.pixels_per_point())
.round_ui();
ui.painter()
.rect_filled(rect, 0.0, ui.tokens().table_header_bg_fill);
let response = Frame::new()
.inner_margin(ui.tokens().header_cell_margin(table_style))
.show(ui, content);
if !connected_to_next_cell {
ui.painter().vline(
rect.max.x - CELL_SEPARATOR_STROKE_OFFSET,
rect.y_range(),
Stroke::new(1.0, ui.tokens().table_header_stroke_color),
);
}
ui.painter().hline(
rect.x_range(),
rect.max.y - CELL_SEPARATOR_STROKE_OFFSET, Stroke::new(1.0, ui.tokens().table_header_stroke_color),
);
response
}
pub fn cell_ui<R>(
ui: &mut egui::Ui,
table_style: re_ui::TableStyle,
connected_to_next_cell: bool,
content: impl FnOnce(&mut egui::Ui) -> R,
) -> egui::InnerResponse<R> {
let response = Frame::new()
.inner_margin(ui.tokens().table_cell_margin(table_style))
.show(ui, content);
let rect = ui
.max_rect()
.round_to_pixels(ui.pixels_per_point())
.round_ui();
if !connected_to_next_cell {
ui.painter().vline(
rect.max.x - CELL_SEPARATOR_STROKE_OFFSET,
rect.y_range(),
Stroke::new(1.0, ui.tokens().table_interaction_noninteractive_bg_stroke),
);
}
ui.painter().hline(
rect.x_range(),
rect.max.y - CELL_SEPARATOR_STROKE_OFFSET, Stroke::new(1.0, ui.tokens().table_interaction_noninteractive_bg_stroke),
);
response
}
#[derive(Debug, Clone, Hash, serde::Serialize, serde::Deserialize)]
pub struct ColumnConfig {
original_index: usize,
id: Id,
name: String,
visible: bool,
sort_key: i64,
}
impl ColumnConfig {
pub fn new(id: Id, name: String) -> Self {
Self {
original_index: 0,
id,
name,
visible: true,
sort_key: 0,
}
}
pub fn new_with_visible(id: Id, name: String, visible: bool) -> Self {
Self {
original_index: 0,
id,
name,
visible,
sort_key: 0,
}
}
pub fn with_sort_key(mut self, sort_key: i64) -> Self {
self.sort_key = sort_key;
self
}
pub fn id(&self) -> Id {
self.id
}
pub fn original_index(&self) -> usize {
self.original_index
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct TableConfig {
id: Id,
columns: Vec<ColumnConfig>,
}
impl TableConfig {
fn new(id: Id) -> Self {
Self {
id,
columns: Vec::new(),
}
}
pub fn clear_state(egui_ctx: &egui::Context, persisted_id: Id) {
egui_ctx.data_mut(|data| {
data.remove::<Self>(persisted_id);
});
}
pub fn get_with_columns(
egui_ctx: &egui::Context,
persisted_id: Id,
columns: impl Iterator<Item = ColumnConfig>,
) -> Self {
egui_ctx.data_mut(|data| {
let config: &mut Self =
data.get_persisted_mut_or_insert_with(persisted_id, || Self::new(persisted_id));
let mut has_cols = HashSet::default();
let mut new_cols = Vec::new();
for (index, mut new_config) in columns.enumerate() {
new_config.original_index = index;
has_cols.insert(new_config.id);
if let Some(existing_config) = config
.columns
.iter_mut()
.find(|existing| existing.name == new_config.name)
{
existing_config.id = new_config.id;
existing_config.original_index = index;
} else {
new_cols.push(new_config);
}
}
new_cols.sort_by_key(|c| c.sort_key);
config.columns.extend(new_cols);
config.columns.retain(|col| has_cols.contains(&col.id));
config.clone()
})
}
pub fn store(self, egui_ctx: &egui::Context) {
egui_ctx.data_mut(|data| {
data.insert_persisted(self.id, self);
});
}
pub fn visible_columns(&self) -> impl Iterator<Item = &ColumnConfig> {
self.columns.iter().filter(|col| col.visible)
}
pub fn visible_column_names(&self) -> impl Iterator<Item = &str> {
self.visible_columns().map(|col| col.name.as_str())
}
pub fn visible_column_ids(&self) -> impl Iterator<Item = Id> + use<'_> {
self.visible_columns().map(|col| col.id)
}
pub fn visible_column_indexes(&self) -> impl Iterator<Item = usize> + use<'_> {
self.visible_columns().map(|col| col.original_index)
}
pub fn ui(&mut self, ui: &mut egui::Ui) {
egui::ScrollArea::vertical().show(ui, |ui| {
let response = egui_dnd::dnd(ui, "Columns").show(
self.columns.iter_mut(),
|ui, column, handle, _state| {
let visible = column.visible;
egui::Sides::new().show(
ui,
|ui| {
handle.ui(ui, |ui| {
ui.small_icon(&icons::DND_HANDLE, Some(ui.visuals().text_color()));
});
let mut label = RichText::new(&column.name);
if visible {
label = label.strong();
} else {
label = label.weak();
}
ui.label(label);
},
|ui| {
let (icon, alt_text) = if column.visible {
(&icons::VISIBLE, "Hide column")
} else {
(&icons::INVISIBLE, "Show column")
};
if ui.small_icon_button(icon, alt_text).clicked() {
column.visible = !column.visible;
}
},
);
},
);
if response.is_drag_finished() {
response.update_vec(self.columns.as_mut_slice());
}
});
}
pub fn button_ui(&mut self, ui: &mut egui::Ui) {
MenuButton::from_button(icons::SETTINGS.as_button_with_label(ui.tokens(), "Columns"))
.config(MenuConfig::new().close_behavior(PopupCloseBehavior::CloseOnClickOutside))
.ui(ui, |ui| {
self.ui(ui);
});
}
}