Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "egui-selectable-table"
version = "0.2.0"
version = "0.2.1"
edition = "2021"
authors = ["TheRustyPickle <rusty.pickle94@gmail.com>"]
readme = "README.md"
Expand All @@ -15,8 +15,8 @@ license = "MIT"
exclude = ["/demo", "/.github"]

[dependencies]
egui = { version = "0.31.0", default-features = false, features = ["rayon"] }
egui_extras = { version = "0.31.0", default-features = false }
egui = { version = "0.31.1", default-features = false, features = ["rayon"] }
egui_extras = { version = "0.31.1", default-features = false }
rayon = "1.10.0"

[lints.rust]
Expand Down
4 changes: 2 additions & 2 deletions demo/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ version = "0.1.0"
edition = "2021"

[dependencies]
eframe = "0.31.0"
egui = "0.31.0"
eframe = "0.31.1"
egui = "0.31.1"
egui-selectable-table = { path = ".." }
egui_extras = "0.31.0"
strum = "0.27.0"
Expand Down
20 changes: 19 additions & 1 deletion demo/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,20 @@ impl App for MainWindow {
{
self.table.set_select_full_row(self.select_entire_row);
};
ui.separator();
if ui.button("Add unsorted row at the bottom").clicked() {
let new_row = TableRow {
field_1: self.row_num,
field_2: self.row_num as i64 * 10,
field_3: format!("I'm unsorted row with row num: {}", self.row_num),
field_4: format!("field 4 with row num: {}", self.row_num),
field_5: format!("field 5 with row num: {}", self.row_num),
field_6: format!("field 6 with row num: {}", self.row_num),
create_count: 0,
};
self.row_num += 1;
self.table.add_unsorted_row(new_row);
}
});
ui.separator();
ui.horizontal(|ui| {
Expand Down Expand Up @@ -257,7 +271,7 @@ impl ColumnOperations<TableRow, TableColumns, Config> for TableColumns {
// reloaded. After there is no more row creation, auto reload is turned off and won't
// reload until next manual intervention. While no more rows are being created, we are
// modifying the rows directly that are being shown in the UI which is much less
// expensive and gets shown to the UI immediately
// expensive and gets shown to the UI immediately.
// Continue to update the persistent row data to ensure once reload happens, the
// previous count data is not lost
table.add_modify_row(|table| {
Expand Down Expand Up @@ -293,6 +307,10 @@ impl ColumnOperations<TableRow, TableColumns, Config> for TableColumns {
table.copy_selected_cells(ui);
ui.close_menu();
}
if table.get_total_selected_rows() <= 1 && ui.button("Mark row as selected").clicked() {
ui.close_menu();
table.mark_row_as_selected(row_id, None);
}
});
resp
}
Expand Down
4 changes: 2 additions & 2 deletions src/auto_reload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pub struct AutoReload {
impl AutoReload {
/// Increase the current reload count and return bool based on if it is equal or above the count it is
/// supposed to reload at
pub(crate) fn increment_count(&mut self) -> bool {
pub(crate) const fn increment_count(&mut self) -> bool {
self.reload_count += 1;
if let Some(count) = self.reload_after {
let reload = self.reload_count >= count;
Expand Down Expand Up @@ -77,7 +77,7 @@ where
/// table.set_auto_reload(Some(1000)); // Reload after 1000 updates.
/// table.set_auto_reload(None); // Disable auto-reloading.
/// ```
pub fn set_auto_reload(&mut self, count: Option<u32>) {
pub const fn set_auto_reload(&mut self, count: Option<u32>) {
self.auto_reload.reload_after = count;
self.auto_reload.reload_count = 0;
}
Expand Down
4 changes: 2 additions & 2 deletions src/auto_scroll.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ where
+ ColumnOrdering<Row>,
Conf: Default,
{
pub(crate) fn update_scroll_offset(&mut self, offset: f32) {
pub(crate) const fn update_scroll_offset(&mut self, offset: f32) {
self.auto_scroll.scroll_offset = offset;
}

Expand Down Expand Up @@ -248,7 +248,7 @@ where
/// let new_scroll_settings = AutoScroll::new(true).max_speed(60.0);
/// table.update_auto_scroll(new_scroll_settings); // Update the auto-scroll settings during runtime
/// ```
pub fn update_auto_scroll(&mut self, scroll: AutoScroll) {
pub const fn update_auto_scroll(&mut self, scroll: AutoScroll) {
self.auto_scroll = scroll;
}
}
189 changes: 73 additions & 116 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
mod auto_reload;
mod auto_scroll;
mod row_modification;
mod row_selection;

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 rayon::prelude::*;
use std::cmp::Ordering;
use std::hash::Hash;

Expand Down Expand Up @@ -179,8 +179,7 @@ where
/// # Type Parameters
/// * `Row` - The type representing each row in the table.
/// * `F` - A type used to identify columns, often an enum or field type.
/// * `Conf` - Configuration type for additional table settings passed by the user. This is made available anytime
/// when creating or modifying rows
/// * `Conf` - Configuration type for additional table settings passed by the user. This is made available anytime when creating or modifying rows
pub struct SelectableTable<Row, F, Conf>
where
Row: Clone + Send + Sync,
Expand Down Expand Up @@ -234,9 +233,10 @@ where
/// Additional Parameters passed by you, available when creating new rows or header. Can
/// contain anything implementing the `Default` trait
pub config: Conf,

/// Whether to add the row serial column to the table
add_serial_column: bool,
/// The row height for the table, defaults to 25.0
row_height: f32,
}

impl<Row, F, Conf> SelectableTable<Row, F, Conf>
Expand Down Expand Up @@ -294,6 +294,7 @@ where
horizontal_scroll: false,
config: Conf::default(),
add_serial_column: false,
row_height: 25.0,
}
}

Expand Down Expand Up @@ -387,14 +388,14 @@ where
table = table.vertical_scroll_offset(offset);
ctx.request_repaint();
}
};
}

let output = table
.header(20.0, |header| {
self.build_head(header);
})
.body(|body| {
body.rows(25.0, self.formatted_rows.len(), |row| {
body.rows(self.row_height, self.formatted_rows.len(), |row| {
let index = row.index();
self.build_body(row, index);
});
Expand All @@ -416,14 +417,14 @@ where
table = table.vertical_scroll_offset(offset);
ctx.request_repaint();
}
};
}

let output = table
.header(20.0, |header| {
self.build_head(header);
})
.body(|body| {
body.rows(25.0, self.formatted_rows.len(), |row| {
body.rows(self.row_height, self.formatted_rows.len(), |row| {
let index = row.index();
self.build_body(row, index);
});
Expand Down Expand Up @@ -478,110 +479,6 @@ where
self.handle_table_body(row, &row_data);
}

/// Modify or add rows to the table. Changes are not immediately reflected in the UI.
/// You must call [`recreate_rows`](#method.recreate_rows) to apply these changes visually.
///
/// # Parameters:
/// - `table`: A closure that takes a mutable reference to the rows and optionally returns a new row.
/// If a row is returned, it will be added to the table.
///
/// # Auto Reload:
/// - Use [`auto_reload`](#method.auto_reload) to automatically refresh the UI after a specified
/// number of row modifications or additions.
///
/// # Returns
/// * `Option<i64>` - The row id that is used internally for the new row
///
/// # Example:
/// ```rust,ignore
/// let new_row_id = table.add_modify_row(|rows| {
/// let my_row = rows.get_mut(row_id).unwrap();
/// // modify your row as necessary
///
/// let new_row = MyRow {
/// // Define your row values
/// };
/// Some(new_row) // Optionally add a new row
/// });
/// ```
pub fn add_modify_row<Fn>(&mut self, table: Fn) -> Option<i64>
where
Fn: FnOnce(&mut HashMap<i64, SelectableRow<Row, F>>) -> Option<Row>,
{
let new_row = table(&mut self.rows);

let mut to_return = None;

if let Some(row) = new_row {
let selected_columns = HashSet::new();
let new_row = SelectableRow {
row_data: row,
id: self.last_id_used,
selected_columns,
};
to_return = Some(self.last_id_used);
self.rows.insert(new_row.id, new_row);
self.last_id_used += 1;
}

let reload = self.auto_reload.increment_count();

if reload {
self.recreate_rows();
}
to_return
}

/// Modify only the rows currently displayed in the UI.
///
/// # Important:
/// - This does not require calling `recreate_rows` to reflect changes.
/// - Should not be used when rows are frequently recreated, as data might be lost.
/// - Does not contribute toward `auto_reload` count.
///
/// # Parameters:
/// - `table`: A closure that takes a mutable reference to the currently formatted rows and an index map.
///
/// # Example:
/// ```rust,ignore
/// table.modify_shown_row(|formatted_rows, indexed_ids| {
/// let row_id = 0;
/// let target_index = indexed_ids.get(row_id).unwrap();
/// let row = formatted_rows.get_mut(target_index).unwrap();
/// /* modify rows */
///
/// });
/// ```
pub fn modify_shown_row<Fn>(&mut self, mut rows: Fn)
where
Fn: FnMut(&mut Vec<SelectableRow<Row, F>>, &HashMap<i64, usize>),
{
rows(&mut self.formatted_rows, &self.indexed_ids);
}

/// Sort the rows to the current sorting order and column and save them for later reuse
fn sort_rows(&mut self) {
let mut row_data: Vec<SelectableRow<Row, F>> =
self.rows.par_iter().map(|(_, v)| v.clone()).collect();

row_data.par_sort_by(|a, b| {
let ordering = self.sorted_by.order_by(&a.row_data, &b.row_data);
match self.sort_order {
SortOrder::Ascending => ordering,
SortOrder::Descending => ordering.reverse(),
}
});

let indexed_data = row_data
.par_iter()
.enumerate()
.map(|(index, row)| (row.id, index))
.collect();

self.indexed_ids = indexed_data;
self.formatted_rows = row_data;
}

/// Change the current sort order from ascending to descending and vice versa. Will unselect
/// all selected rows
fn change_sort_order(&mut self) {
Expand All @@ -602,10 +499,15 @@ where

/// Recreates the rows shown in the UI for the next frame load.
///
/// # Important:
/// - Any direct modifications made using [`modify_shown_row`](#method.modify_shown_row)
/// will be **cleared** when this is called.
/// To preserve changes, use [`add_modify_row`](#method.add_modify_row) to update row data instead.
///
/// # Performance:
/// - Should be used sparingly for large datasets as frequent calls can lead to performance issues.
/// - Consider calling after every X amount row updates, based on how frequently new rows are being
/// added or use [`auto_scroll`](#method.auto_scroll) for automatic reload.
/// - Should be used sparingly for large datasets, as frequent calls can lead to performance issues.
/// - Consider calling after every X number of row updates, depending on update frequency,
/// or use [`auto_reload`](#method.auto_reload) for automatic reload.
///
/// # Example:
/// ```rust,ignore
Expand All @@ -618,6 +520,42 @@ where
self.sort_rows();
}

/// Recreates the rows shown in the UI for the next frame load.
///
/// This function refreshes the internal row state by clearing and re-sorting the rows
/// similar to [`recreate_rows`](#method.recreate_rows), but it **preserves** the currently
/// selected rows and re-applies the active column selection to them.
///
/// Useful when the UI needs to be refreshed without resetting user interaction state.
///
/// # Important:
/// - Any direct modifications made to `formatted_rows` using [`modify_shown_row`](#method.modify_shown_row)
/// will be **cleared** when this is called.
/// To preserve changes, use [`add_modify_row`](#method.add_modify_row) to update row data instead.
///
/// # Performance:
/// - Should be used sparingly for large datasets, as frequent calls can lead to performance issues.
/// - Consider calling after every X number of row updates, depending on update frequency,
/// or use [`auto_reload`](#method.auto_reload) for automatic reload.
///
/// # Example:
/// ```rust,ignore
/// table.recreate_rows_no_unselect();
/// ```
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);
}
}

/// The first column that was passed by the user
fn first_column(&self) -> F {
self.all_columns[0].clone()
Expand Down Expand Up @@ -717,7 +655,7 @@ where
///
/// # Returns:
/// - `usize`: The number of rows that are formatted and ready for display.
pub fn total_displayed_rows(&self) -> usize {
pub const fn total_displayed_rows(&self) -> usize {
self.formatted_rows.len()
}

Expand Down Expand Up @@ -779,4 +717,23 @@ where
self.horizontal_scroll = true;
self
}

/// Sets the height rows in the table.
///
/// # Parameters:
/// - `height`: The desired height for each row in logical points.
///
/// # Returns:
/// - `Self`: The modified table with the specified row height applied.
///
/// # Example:
/// ```rust,ignore
/// let table = SelectableTable::new(vec![col1, col2, col3])
/// .row_height(24.0);
/// ```
#[must_use]
pub const fn row_height(mut self, height: f32) -> Self {
self.row_height = height;
self
}
}
Loading