re_dataframe_ui/
table_utils.rsuse ahash::HashSet;
use egui::{Context, Frame, Id, Margin, RichText, Stroke, Style};
use re_ui::{design_tokens, icons, Scale, UiExt as _};
pub const CELL_MARGIN: Margin = Margin::symmetric(8, 6);
pub fn apply_table_style_fixes(style: &mut Style) {
style.visuals.widgets.hovered.bg_stroke =
Stroke::new(1.0, design_tokens().color_table.gray(Scale::S300));
style.visuals.widgets.active.bg_stroke =
Stroke::new(1.0, design_tokens().color_table.gray(Scale::S350));
style.visuals.widgets.noninteractive.bg_stroke =
Stroke::new(1.0, design_tokens().color_table.gray(Scale::S200));
}
pub fn header_title(ui: &mut egui::Ui, title: impl Into<RichText>) -> egui::Response {
header_ui(ui, |ui| {
ui.monospace(title.into().strong());
})
.response
}
pub fn header_ui<R>(
ui: &mut egui::Ui,
content: impl FnOnce(&mut egui::Ui) -> R,
) -> egui::InnerResponse<R> {
let response = Frame::new()
.inner_margin(CELL_MARGIN)
.fill(design_tokens().color_table.gray(Scale::S150))
.show(ui, |ui| {
ui.set_width(ui.available_width());
content(ui)
});
let rect = response.response.rect;
ui.painter().hline(
rect.x_range(),
rect.max.y - 1.0, Stroke::new(1.0, design_tokens().color_table.gray(Scale::S300)),
);
response
}
pub fn cell_ui<R>(
ui: &mut egui::Ui,
content: impl FnOnce(&mut egui::Ui) -> R,
) -> egui::InnerResponse<R> {
let response = Frame::new().inner_margin(CELL_MARGIN).show(ui, |ui| {
ui.set_width(ui.available_width());
content(ui)
});
let rect = response.response.rect;
ui.painter().hline(
rect.x_range(),
rect.max.y - 1.0, Stroke::new(1.0, design_tokens().color_table.gray(Scale::S200)),
);
response
}
#[derive(Debug, Clone, Hash, serde::Serialize, serde::Deserialize)]
pub struct ColumnConfig {
id: Id,
name: String,
visible: bool,
}
impl ColumnConfig {
pub fn new(id: Id, name: String) -> Self {
Self {
id,
name,
visible: true,
}
}
}
#[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 get_with_columns(
ctx: &Context,
persisted_id: Id,
columns: impl Iterator<Item = ColumnConfig>,
) -> Self {
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();
for col in columns {
has_cols.insert(col.id);
if !config.columns.iter().any(|c| c.name == col.name) {
config.columns.push(col);
}
}
config.columns.retain(|col| has_cols.contains(&col.id));
config.clone()
})
}
pub fn store(self, ctx: &Context) {
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 ui(&mut self, ui: &mut egui::Ui) {
let response = egui_dnd::dnd(ui, "Columns").show(
self.columns.iter_mut(),
|ui, item, handle, _state| {
let visible = item.visible;
egui::Sides::new().show(
ui,
|ui| {
handle.ui(ui, |ui| {
ui.small_icon(&icons::DND_HANDLE, None);
});
let mut label = RichText::new(&item.name);
if visible {
label = label.strong();
} else {
label = label.weak();
}
ui.label(label);
},
|ui| {
if ui
.small_icon_button(if item.visible {
&icons::VISIBLE
} else {
&icons::INVISIBLE
})
.clicked()
{
item.visible = !item.visible;
}
},
);
},
);
if response.is_drag_finished() {
response.update_vec(self.columns.as_mut_slice());
}
}
pub fn button_ui(&mut self, ui: &mut egui::Ui) {
ui.menu_image_text_button(icons::SETTINGS.as_image(), "Columns", |ui| {
self.ui(ui);
});
}
}