#![cfg_attr(docsrs, feature(doc_cfg))]
mod auto_reload;
mod auto_scroll;
mod row_modification;
mod row_selection;
#[cfg(feature = "fuzzy-matching")]
#[cfg_attr(docsrs, doc(cfg(feature = "fuzzy-matching")))]
mod fuzzy_matcher;
use auto_reload::AutoReload;
pub use auto_scroll::AutoScroll;
use egui::ahash::{HashMap, HashMapExt, HashSet, HashSetExt};
use egui::{Event, Key, Label, Response, ScrollArea, Sense, Ui};
use egui_extras::{Column, TableBuilder, TableRow};
use std::cmp::Ordering;
use std::hash::Hash;
#[cfg(feature = "fuzzy-matching")]
use nucleo_matcher::Matcher;
#[derive(Default, Clone, Copy)]
pub enum SortOrder {
#[default]
Ascending,
Descending,
}
pub trait ColumnOrdering<Row>
where
Row: Clone + Send + Sync,
{
fn order_by(&self, row_1: &Row, row_2: &Row) -> Ordering;
}
pub trait ColumnOperations<Row, F, Conf>
where
Row: Clone + Send + Sync,
F: Eq
+ Hash
+ Clone
+ Ord
+ Send
+ Sync
+ Default
+ ColumnOperations<Row, F, Conf>
+ ColumnOrdering<Row>,
Conf: Default,
{
fn create_header(
&self,
ui: &mut Ui,
sort_order: Option<SortOrder>,
table: &mut SelectableTable<Row, F, Conf>,
) -> Option<Response>;
fn create_table_row(
&self,
ui: &mut Ui,
row: &SelectableRow<Row, F>,
column_selected: bool,
table: &mut SelectableTable<Row, F, Conf>,
) -> Response;
fn column_text(&self, row: &Row) -> String;
}
#[derive(Clone)]
pub struct SelectableRow<Row, F>
where
Row: Clone + Send + Sync,
F: Eq + Hash + Clone + Ord + Send + Sync + Default,
{
pub row_data: Row,
pub id: i64,
pub selected_columns: HashSet<F>,
}
pub struct SelectableTable<Row, F, Conf>
where
Row: Clone + Send + Sync,
F: Eq
+ Hash
+ Clone
+ Ord
+ Send
+ Sync
+ Default
+ ColumnOperations<Row, F, Conf>
+ ColumnOrdering<Row>,
Conf: Default,
{
all_columns: Vec<F>,
column_number: HashMap<F, usize>,
rows: HashMap<i64, SelectableRow<Row, F>>,
formatted_rows: Vec<SelectableRow<Row, F>>,
sorted_by: F,
sort_order: SortOrder,
drag_started_on: Option<(i64, F)>,
active_columns: HashSet<F>,
active_rows: HashSet<i64>,
last_active_row: Option<i64>,
last_active_column: Option<F>,
beyond_drag_point: bool,
indexed_ids: HashMap<i64, usize>,
last_id_used: i64,
auto_scroll: AutoScroll,
auto_reload: AutoReload,
select_full_row: bool,
horizontal_scroll: bool,
pub config: Conf,
add_serial_column: bool,
row_height: f32,
header_height: f32,
#[cfg(feature = "fuzzy-matching")]
matcher: Matcher,
no_ctrl_a_capture: bool,
}
impl<Row, F, Conf> SelectableTable<Row, F, Conf>
where
Row: Clone + Send + Sync,
F: Eq
+ Hash
+ Clone
+ Ord
+ Send
+ Sync
+ Default
+ ColumnOperations<Row, F, Conf>
+ ColumnOrdering<Row>,
Conf: Default,
{
#[must_use]
pub fn new(columns: Vec<F>) -> Self {
let all_columns = columns.clone();
let mut column_number = HashMap::new();
for (index, col) in columns.into_iter().enumerate() {
column_number.insert(col, index);
}
Self {
all_columns,
column_number,
last_id_used: 0,
rows: HashMap::new(),
formatted_rows: Vec::new(),
sorted_by: F::default(),
sort_order: SortOrder::default(),
drag_started_on: None,
active_columns: HashSet::new(),
active_rows: HashSet::new(),
last_active_row: None,
last_active_column: None,
beyond_drag_point: false,
indexed_ids: HashMap::new(),
auto_scroll: AutoScroll::default(),
auto_reload: AutoReload::default(),
select_full_row: false,
horizontal_scroll: false,
config: Conf::default(),
add_serial_column: false,
row_height: 25.0,
header_height: 20.0,
#[cfg(feature = "fuzzy-matching")]
matcher: Matcher::default(),
no_ctrl_a_capture: false,
}
}
pub fn set_config(&mut self, conf: Conf) {
self.config = conf;
}
#[must_use]
pub fn config(mut self, conf: Conf) -> Self {
self.config = conf;
self
}
pub fn clear_all_rows(&mut self) {
self.rows.clear();
self.formatted_rows.clear();
self.active_rows.clear();
self.active_columns.clear();
self.last_id_used = 0;
}
pub fn show_ui<Fn>(&mut self, ui: &mut Ui, table_builder: Fn)
where
Fn: FnOnce(TableBuilder) -> TableBuilder,
{
let is_ctrl_pressed = ui.ctx().input(|i| i.modifiers.ctrl);
let key_a_pressed = ui.ctx().input(|i| i.key_pressed(Key::A));
let copy_initiated = ui.ctx().input(|i| i.events.contains(&Event::Copy));
let ctx = ui.ctx().clone();
if copy_initiated {
self.copy_selected_cells(ui);
}
if is_ctrl_pressed && key_a_pressed && !self.no_ctrl_a_capture {
self.select_all();
}
let pointer = ui.input(|i| i.pointer.hover_pos());
let max_rect = ui.max_rect();
if self.horizontal_scroll {
ScrollArea::horizontal().show(ui, |ui| {
let mut table = TableBuilder::new(ui);
if self.add_serial_column {
table = table.column(Column::initial(25.0).clip(true));
}
table = table_builder(table);
if self.drag_started_on.is_some()
&& let Some(offset) = self.auto_scroll.start_scroll(max_rect, pointer)
{
table = table.vertical_scroll_offset(offset);
ctx.request_repaint();
}
let output = table
.header(self.header_height, |header| {
self.build_head(header);
})
.body(|body| {
body.rows(self.row_height, self.formatted_rows.len(), |row| {
let index = row.index();
self.build_body(row, index);
});
});
let scroll_offset = output.state.offset.y;
self.update_scroll_offset(scroll_offset);
});
} else {
let mut table = TableBuilder::new(ui);
if self.add_serial_column {
table = table.column(Column::initial(25.0).clip(true));
}
table = table_builder(table);
if self.drag_started_on.is_some()
&& let Some(offset) = self.auto_scroll.start_scroll(max_rect, pointer)
{
table = table.vertical_scroll_offset(offset);
ctx.request_repaint();
}
let output = table
.header(self.header_height, |header| {
self.build_head(header);
})
.body(|body| {
body.rows(self.row_height, self.formatted_rows.len(), |row| {
let index = row.index();
self.build_body(row, index);
});
});
let scroll_offset = output.state.offset.y;
self.update_scroll_offset(scroll_offset);
}
}
fn build_head(&mut self, mut header: TableRow) {
if self.add_serial_column {
header.col(|ui| {
ui.add_sized(ui.available_size(), Label::new(""));
});
}
for column_name in &self.all_columns.clone() {
header.col(|ui| {
let sort_order = if &self.sorted_by == column_name {
Some(self.sort_order)
} else {
None
};
let Some(resp) = column_name.create_header(ui, sort_order, self) else {
return;
};
if resp.clicked() {
let is_selected = &self.sorted_by == column_name;
if is_selected {
self.change_sort_order();
} else {
self.change_sorted_by(column_name);
}
self.recreate_rows();
}
});
}
}
fn build_body(&mut self, mut row: TableRow, index: usize) {
let row_data = self.formatted_rows[index].clone();
if self.add_serial_column {
row.col(|ui| {
ui.add_sized(ui.available_size(), Label::new(format!("{}", index + 1)));
});
}
self.handle_table_body(row, &row_data);
}
fn change_sort_order(&mut self) {
self.unselect_all();
if matches!(self.sort_order, SortOrder::Ascending) {
self.sort_order = SortOrder::Descending;
} else {
self.sort_order = SortOrder::Ascending;
}
}
fn change_sorted_by(&mut self, sort_by: &F) {
self.unselect_all();
self.sorted_by = sort_by.clone();
self.sort_order = SortOrder::default();
}
pub fn recreate_rows(&mut self) {
self.formatted_rows.clear();
self.active_rows.clear();
self.active_columns.clear();
self.sort_rows();
}
pub fn recreate_rows_no_unselect(&mut self) {
self.formatted_rows.clear();
self.sort_rows();
for row in &self.active_rows {
let Some(target_index) = self.indexed_ids.get(row) else {
continue;
};
self.formatted_rows[*target_index]
.selected_columns
.clone_from(&self.active_columns);
}
}
fn first_column(&self) -> F {
self.all_columns[0].clone()
}
fn last_column(&self) -> F {
self.all_columns[self.all_columns.len() - 1].clone()
}
fn column_to_num(&self, column: &F) -> usize {
*self
.column_number
.get(column)
.expect("Not in the column list")
}
fn next_column(&self, column: &F) -> F {
let current_column_num = self.column_to_num(column);
if current_column_num == self.all_columns.len() - 1 {
self.all_columns[0].clone()
} else {
self.all_columns[current_column_num + 1].clone()
}
}
fn previous_column(&self, column: &F) -> F {
let current_column_num = self.column_to_num(column);
if current_column_num == 0 {
self.all_columns[self.all_columns.len() - 1].clone()
} else {
self.all_columns[current_column_num - 1].clone()
}
}
fn handle_table_body(&mut self, mut row: TableRow, row_data: &SelectableRow<Row, F>) {
for column_name in &self.all_columns.clone() {
row.col(|ui| {
let selected = row_data.selected_columns.contains(column_name);
let mut resp = column_name.create_table_row(ui, row_data, selected, self);
resp = resp.interact(Sense::drag());
if resp.drag_started() {
if !ui.ctx().input(|i| i.modifiers.ctrl)
&& !ui.ctx().input(|i| i.pointer.secondary_clicked())
{
self.unselect_all();
}
self.drag_started_on = Some((row_data.id, column_name.clone()));
}
let pointer_released = ui.input(|a| a.pointer.primary_released());
if pointer_released {
self.last_active_row = None;
self.last_active_column = None;
self.drag_started_on = None;
self.beyond_drag_point = false;
}
if resp.clicked() {
if !ui.ctx().input(|i| i.modifiers.ctrl)
&& !ui.ctx().input(|i| i.pointer.secondary_clicked())
{
self.unselect_all();
}
self.select_single_row_cell(row_data.id, column_name);
}
if ui.ui_contains_pointer()
&& self.drag_started_on.is_some()
&& let Some(drag_start) = self.drag_started_on.as_ref()
{
if drag_start.0 != row_data.id
|| &drag_start.1 != column_name
|| self.beyond_drag_point
{
let is_ctrl_pressed = ui.ctx().input(|i| i.modifiers.ctrl);
self.select_dragged_row_cell(row_data.id, column_name, is_ctrl_pressed);
}
}
});
}
}
pub const fn total_displayed_rows(&self) -> usize {
self.formatted_rows.len()
}
pub fn total_rows(&self) -> usize {
self.rows.len()
}
pub const fn get_displayed_rows(&self) -> &Vec<SelectableRow<Row, F>> {
&self.formatted_rows
}
pub const fn get_all_rows(&self) -> &HashMap<i64, SelectableRow<Row, F>> {
&self.rows
}
#[must_use]
pub const fn serial_column(mut self) -> Self {
self.add_serial_column = true;
self
}
#[must_use]
pub const fn horizontal_scroll(mut self) -> Self {
self.horizontal_scroll = true;
self
}
#[must_use]
pub const fn row_height(mut self, height: f32) -> Self {
self.row_height = height;
self
}
#[must_use]
pub const fn header_height(mut self, height: f32) -> Self {
self.header_height = height;
self
}
#[must_use]
pub const fn no_ctrl_a_capture(mut self) -> Self {
self.no_ctrl_a_capture = true;
self
}
pub const fn set_no_ctrl_a_capture(&mut self, status: bool) {
self.no_ctrl_a_capture = status;
}
}