use egui::{ScrollArea, Ui};
use crate::{header::HeaderSortState, CellAlign, RowSource, Table, TableEvent};
#[derive(Debug, Clone, Default)]
pub struct EguiTableState {
pub events: Vec<TableEvent>,
pub edit_mode: Option<(usize, usize)>,
pub edit_buffer: String,
pub expanded_rows: std::collections::HashSet<usize>,
pub h_scroll_offset: f32,
}
impl EguiTableState {
pub fn begin_edit(&mut self, row: usize, col: usize, current: String) {
self.edit_mode = Some((row, col));
self.edit_buffer = current;
}
pub fn commit_edit(&mut self) -> Option<(usize, usize, String)> {
let (row, col) = self.edit_mode.take()?;
let value = std::mem::take(&mut self.edit_buffer);
Some((row, col, value))
}
pub fn cancel_edit(&mut self) {
self.edit_mode = None;
self.edit_buffer.clear();
}
pub fn toggle_expand(&mut self, row: usize) {
if self.expanded_rows.contains(&row) {
self.expanded_rows.remove(&row);
} else {
self.expanded_rows.insert(row);
}
}
}
impl<S: RowSource> Table<S> {
pub fn render_egui(
&mut self,
ui: &mut Ui,
sort_state: &mut HeaderSortState,
render_state: &mut EguiTableState,
) {
render_state.events.clear();
let row_height = self.row_height();
let col_count = self.source().column_defs().len();
let order: Vec<usize> = if self.column_order.len() == col_count {
self.column_order.clone()
} else {
(0..col_count).collect()
};
let mut resize_events: Vec<TableEvent> = Vec::new();
let mut sort_events: Vec<TableEvent> = Vec::new();
let h_offset = render_state.h_scroll_offset;
ScrollArea::horizontal()
.id_salt("oxiui_table_header_h")
.scroll_offset(egui::Vec2::new(h_offset, 0.0))
.scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden)
.show(ui, |ui| {
ui.horizontal(|ui| {
for &col_idx in &order {
let effective_w = self.effective_width(col_idx);
let col_name = self
.source()
.column_defs()
.get(col_idx)
.map(|c| c.name.clone())
.unwrap_or_default();
let resizable_flag = self
.source()
.column_defs()
.get(col_idx)
.map(|c| c.resizable)
.unwrap_or(false);
let indicator = sort_state.indicator(col_idx);
let label_text = if indicator.is_empty() {
col_name
} else {
format!("{col_name} {indicator}")
};
if ui
.add_sized(
[effective_w - 8.0, row_height],
egui::Button::new(egui::RichText::new(label_text).strong()),
)
.clicked()
{
sort_state.toggle(col_idx);
let ascending = sort_state
.column
.map(|c| c == col_idx && sort_state.ascending)
.unwrap_or(false);
sort_events.push(TableEvent::SortChanged {
col: col_idx,
ascending,
});
}
if resizable_flag {
let drag_id = egui::Id::new(("col_resize", col_idx));
let drag_resp = ui.interact(
egui::Rect::from_min_size(
ui.cursor().min,
egui::vec2(6.0, row_height),
),
drag_id,
egui::Sense::drag(),
);
if drag_resp.hovered() || drag_resp.dragged() {
ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeHorizontal);
}
if drag_resp.dragged() {
let delta = drag_resp.drag_delta().x;
if let Some(new_w) = self.resize_column(col_idx, delta) {
resize_events.push(TableEvent::ColumnResized {
col: col_idx,
new_width: new_w,
});
}
}
}
}
});
});
render_state.events.extend(sort_events);
render_state.events.extend(resize_events);
let mut filter_events: Vec<TableEvent> = Vec::new();
ScrollArea::horizontal()
.id_salt("oxiui_table_filter_h")
.scroll_offset(egui::Vec2::new(h_offset, 0.0))
.scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden)
.show(ui, |ui| {
ui.horizontal(|ui| {
for &col_idx in &order {
if col_idx < self.column_filters.len() {
let effective_w = self.effective_width(col_idx);
let filter_text = &mut self.column_filters[col_idx];
let resp = ui.add_sized(
[effective_w - 8.0, row_height - 4.0],
egui::TextEdit::singleline(filter_text).hint_text("Filter…"),
);
if resp.changed() {
let new_filter = filter_text.clone();
filter_events.push(TableEvent::FilterChanged {
col: col_idx,
new_filter,
});
}
}
}
});
});
render_state.events.extend(filter_events);
ui.separator();
let filtered = self.filtered_sorted_indices();
let total_visible = filtered.len();
let viewport_w = ui.available_width().max(0.0);
let vis_col_range = if viewport_w > 0.0 {
self.visible_column_range(h_offset, viewport_w)
} else {
0..order.len()
};
let zebra = self.zebra_striping;
let pinned = self.pinned_columns;
let snap_order: Vec<usize> = order[vis_col_range.clone()].to_vec();
let snap_widths: Vec<f32> = snap_order
.iter()
.map(|&ci| self.effective_width(ci))
.collect();
let col_aligns: Vec<CellAlign> = snap_order
.iter()
.map(|&ci| self.column_align(ci, &crate::Cell::Empty))
.collect();
let row_bgs: Vec<Option<[u8; 4]>> =
(0..total_visible).map(|vis| self.row_bg(vis)).collect();
let rows_data: Vec<Vec<crate::Cell>> = filtered
.iter()
.map(|&src_i| self.source().row(src_i))
.collect();
let body_output = ScrollArea::vertical()
.scroll_offset(egui::Vec2::new(h_offset, 0.0))
.show_rows(ui, row_height, total_visible, |ui, row_range| {
for vis_i in row_range {
let bg = row_bgs
.get(vis_i)
.copied()
.flatten()
.map(|[r, g, b, a]| egui::Color32::from_rgba_premultiplied(r, g, b, a))
.or_else(|| {
if zebra && vis_i % 2 == 1 {
Some(ui.style().visuals.faint_bg_color)
} else {
None
}
});
let frame = if let Some(color) = bg {
egui::Frame::new().fill(color)
} else {
egui::Frame::new()
};
frame.show(ui, |ui| {
ui.horizontal(|ui| {
for (render_pos, &col_idx) in snap_order.iter().enumerate() {
let cell_str = rows_data
.get(vis_i)
.and_then(|row| row.get(col_idx))
.map(|c| c.to_string())
.unwrap_or_default();
let align = col_aligns
.get(render_pos)
.copied()
.unwrap_or(CellAlign::Left);
let w = snap_widths.get(render_pos).copied().unwrap_or(100.0);
let is_pinned = render_pos < pinned;
let rich = if is_pinned {
egui::RichText::new(&cell_str).strong()
} else {
egui::RichText::new(&cell_str)
};
let _ = align;
ui.add_sized([w, row_height], egui::Label::new(rich));
}
});
});
}
});
render_state.h_scroll_offset = body_output.state.offset.x;
}
}