├── .gitignore ├── demo ├── .gitignore ├── Cargo.toml ├── src │ ├── main.rs │ └── app.rs └── index.html ├── .github └── workflows │ ├── ci.yml │ └── pages.yml ├── LICENSE ├── Cargo.toml ├── src ├── auto_reload.rs ├── fuzzy_matcher.rs ├── row_modification.rs ├── auto_scroll.rs ├── row_selection.rs └── lib.rs └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /target 3 | -------------------------------------------------------------------------------- /demo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "demo" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | eframe = "0.33.0" 8 | egui = "0.33.0" 9 | egui-selectable-table = { path = "..", features = ["fuzzy-matching"] } 10 | egui_extras = "0.33.0" 11 | strum = "0.27.2" 12 | strum_macros = "0.27.2" 13 | egui-theme-lerp = "0.4.0" 14 | 15 | [target.'cfg(target_arch = "wasm32")'.dependencies] 16 | wasm-bindgen-futures = "0.4" 17 | web-sys = "0.3.81" 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | ci: 7 | name: ${{ matrix.os }} 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | matrix: 11 | os: [ubuntu-latest, windows-latest, macos-latest] 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | - name: cargo fmt 18 | run: cargo fmt --all -- --check 19 | 20 | - name: cargo build 21 | run: cargo build 22 | 23 | - name: cargo test 24 | run: cargo test 25 | 26 | - name: cargo clippy 27 | run: cargo clippy -- -D warnings 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Rusty Pickle 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/pages.yml: -------------------------------------------------------------------------------- 1 | name: Github Page for demo 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'demo/**' 9 | - 'src/**' 10 | - '.github/**' 11 | 12 | permissions: 13 | contents: write # for committing to gh-pages branch. 14 | 15 | jobs: 16 | build-github-pages: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Setup toolchain for wasm 21 | run: | 22 | rustup update stable 23 | rustup default stable 24 | rustup set profile minimal 25 | rustup target add wasm32-unknown-unknown 26 | - name: Rust Cache 27 | uses: Swatinem/rust-cache@v2 28 | with: 29 | workspaces: "demo -> target" 30 | - name: Download and install Trunk binary 31 | run: wget -qO- https://github.com/thedodd/trunk/releases/latest/download/trunk-x86_64-unknown-linux-gnu.tar.gz | tar -xzf- 32 | - name: Build 33 | run: | 34 | cd demo 35 | ../trunk build --release --public-url ${{ env.public_url }} 36 | env: 37 | public_url: "https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}" 38 | - name: Deploy 39 | uses: JamesIves/github-pages-deploy-action@v4 40 | with: 41 | folder: demo/dist 42 | single-commit: true 43 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "egui-selectable-table" 3 | version = "0.5.0" 4 | edition = "2024" 5 | authors = ["TheRustyPickle "] 6 | readme = "README.md" 7 | description = """ 8 | A library for egui to create tables with draggable cell and row selection. 9 | """ 10 | keywords = ["egui", "table", "selectable-table", "drag", "row"] 11 | categories = ["gui"] 12 | homepage = "https://github.com/TheRustyPickle/egui-selectable-table" 13 | repository = "https://github.com/TheRustyPickle/egui-selectable-table" 14 | license = "MIT" 15 | exclude = ["/demo", "/.github"] 16 | 17 | [dependencies] 18 | egui = { version = "0.33", default-features = false, features = ["rayon"] } 19 | egui_extras = { version = "0.33", default-features = false } 20 | nucleo-matcher = { version = "0.3.1", optional = true } 21 | rayon = "1.11.0" 22 | 23 | [lints.rust] 24 | unsafe_code = "forbid" 25 | 26 | [lints.clippy] 27 | nursery = { level = "deny", priority = 0 } 28 | pedantic = { level = "deny", priority = 1 } 29 | enum_glob_use = { level = "deny", priority = 2 } 30 | perf = { level = "deny", priority = 3 } 31 | style = { level = "deny", priority = 4 } 32 | unwrap_used = { level = "deny", priority = 5 } 33 | 34 | # might be useful to enable in the future 35 | expect_used = { level = "allow", priority = 6 } 36 | missing_panics_doc = { level = "allow", priority = 7 } 37 | struct_excessive_bools = { level = "allow", priority = 8 } 38 | 39 | [features] 40 | default = [] 41 | fuzzy-matching = ["dep:nucleo-matcher"] 42 | 43 | [package.metadata.docs.rs] 44 | all-features = true 45 | features = ["fuzzy-matching"] 46 | rustdoc-args = ["--cfg", "docsrs"] 47 | -------------------------------------------------------------------------------- /demo/src/main.rs: -------------------------------------------------------------------------------- 1 | mod app; 2 | 3 | use app::MainWindow; 4 | 5 | #[cfg(target_arch = "wasm32")] 6 | use eframe::{WebOptions, WebRunner}; 7 | 8 | #[cfg(not(target_arch = "wasm32"))] 9 | fn main() -> eframe::Result { 10 | use egui::{ViewportBuilder, vec2}; 11 | 12 | let options = eframe::NativeOptions { 13 | centered: true, 14 | persist_window: false, 15 | viewport: ViewportBuilder { 16 | inner_size: Some(vec2(1200.0, 700.0)), 17 | ..Default::default() 18 | }, 19 | ..Default::default() 20 | }; 21 | 22 | eframe::run_native( 23 | "Selectable Table Demo", 24 | options, 25 | Box::new(|cc| Ok(Box::new(MainWindow::new(cc)))), 26 | ) 27 | } 28 | 29 | #[cfg(target_arch = "wasm32")] 30 | fn main() { 31 | use eframe::wasm_bindgen::JsCast as _; 32 | let web_options = WebOptions::default(); 33 | 34 | wasm_bindgen_futures::spawn_local(async { 35 | let document = web_sys::window() 36 | .expect("No window") 37 | .document() 38 | .expect("No document"); 39 | 40 | let canvas = document 41 | .get_element_by_id("the_canvas_id") 42 | .expect("Failed to find the_canvas_id") 43 | .dyn_into::() 44 | .expect("the_canvas_id was not a HtmlCanvasElement"); 45 | 46 | let start_result = WebRunner::new() 47 | .start( 48 | canvas, 49 | web_options, 50 | Box::new(|cc| Ok(Box::new(MainWindow::new(cc)))), 51 | ) 52 | .await; 53 | 54 | // Remove the loading text and spinner: 55 | if let Some(loading_text) = document.get_element_by_id("loading_text") { 56 | match start_result { 57 | Ok(_) => { 58 | loading_text.remove(); 59 | } 60 | Err(e) => { 61 | loading_text.set_inner_html( 62 | "

The app has crashed. See the developer console for details.

", 63 | ); 64 | panic!("Failed to start eframe: {e:?}"); 65 | } 66 | } 67 | } 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /src/auto_reload.rs: -------------------------------------------------------------------------------- 1 | use std::hash::Hash; 2 | 3 | use crate::{ColumnOperations, ColumnOrdering, SelectableTable}; 4 | 5 | #[derive(Default)] 6 | pub struct AutoReload { 7 | pub reload_after: Option, 8 | pub reload_count: u32, 9 | } 10 | impl AutoReload { 11 | /// Increase the current reload count and return bool based on if it is equal or above the count it is 12 | /// supposed to reload at 13 | pub(crate) const fn increment_count(&mut self) -> bool { 14 | self.reload_count += 1; 15 | if let Some(count) = self.reload_after { 16 | let reload = self.reload_count >= count; 17 | if reload { 18 | self.reload_count = 0; 19 | } 20 | reload 21 | } else { 22 | false 23 | } 24 | } 25 | } 26 | 27 | /// Enables or configures auto-reloading behavior in the table view. 28 | impl SelectableTable 29 | where 30 | Row: Clone + Send + Sync, 31 | F: Eq 32 | + Hash 33 | + Clone 34 | + Ord 35 | + Send 36 | + Sync 37 | + Default 38 | + ColumnOperations 39 | + ColumnOrdering, 40 | Conf: Default, 41 | { 42 | /// Enable automatic row recreation in the UI after a specified number of modifications or new rows. 43 | /// 44 | /// This function configures the table to automatically recreate its displayed rows when a certain 45 | /// number of new rows or row modifications have been made. Can be useful without having to 46 | /// manually keep track of modifications or when to reload. 47 | /// 48 | /// # Parameters: 49 | /// - `count`: The number of updates (inserts or modifications) that will trigger an automatic row recreation. 50 | /// - If `count` is high, the table will refresh less frequently, leading to potentially outdated rows being shown in the UI for longer. 51 | /// - If `count` is too low, frequent row recreation may result in performance overhead. 52 | /// 53 | /// # Considerations: 54 | /// - Tune the `count` value based on the expected rate of updates. For instance, if new rows or modifications 55 | /// occur at a rate of 1000 rows per second, a `count` between 500 and 1000 may offer the best balance between 56 | /// performance and up-to-date display. 57 | /// 58 | /// # Example: 59 | /// ```rust,ignore 60 | /// let table = SelectableTable::new(vec![col1, col2, col3]) 61 | /// .config(my_config).auto_reload(Some(500)); 62 | /// ``` 63 | #[must_use] 64 | pub const fn auto_reload(mut self, count: u32) -> Self { 65 | self.auto_reload.reload_after = Some(count); 66 | self.auto_reload.reload_count = 0; 67 | self 68 | } 69 | /// Manually set the auto-reload threshold. This lets you change the threshold dynamically. 70 | /// 71 | /// # Parameters: 72 | /// - `count`: Optionally specify how many updates (new or modified rows) should occur before rows are automatically recreated. 73 | /// - If `None` is provided, auto-reloading is disabled. 74 | /// 75 | /// # Example: 76 | /// ```rust,ignore 77 | /// table.set_auto_reload(Some(1000)); // Reload after 1000 updates. 78 | /// table.set_auto_reload(None); // Disable auto-reloading. 79 | /// ``` 80 | pub const fn set_auto_reload(&mut self, count: Option) { 81 | self.auto_reload.reload_after = count; 82 | self.auto_reload.reload_count = 0; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | Selectable Table Demo 13 | 14 | 15 | 16 | 17 | 18 | 19 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 |
114 |

Loading…

115 |
116 |
117 | 118 | 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # egui-selectable-table 2 | 3 | Crates version 4 | Downloads 5 | [![Docs](https://img.shields.io/docsrs/egui-selectable-table/latest)](https://docs.rs/egui-selectable-table) 6 | 7 | A library for [egui](https://github.com/emilk/egui) to create tables with draggable cell and row selection. 8 | 9 | [](https://github.com/user-attachments/assets/54aadfbf-e795-4948-933b-68c08dce6242) 10 | 11 | ## Features 12 | 13 | - Individual cell or full-row selection while dragging 14 | - Auto vertical table scrolling during drag with adjustable parameters 15 | - Sort rows by clicking headers, both ascending and descending 16 | - Customizable rows and header UI 17 | - Built-in select all (Ctrl+A) and copy (Ctrl+C) functionality 18 | - Capable of handling a substantial amount of rows (1M+) with proper settings 19 | - Optional fuzzy matching for searching rows (see below) 20 | 21 | ## Optional Features 22 | 23 | This crate includes an optional `fuzzy-matching` feature that enables fuzzy row search using the [nucleo-matcher](https://crates.io/crates/nucleo-matcher) crate. 24 | 25 | To enable fuzzy matching: 26 | 27 | ```toml 28 | [dependencies] 29 | egui-selectable-table = { version = "0.4.0", features = ["fuzzy-matching"] } 30 | ``` 31 | 32 | ## Usage 33 | 34 | ```rust 35 | // See Demo folder for a complete example 36 | 37 | use egui_selectable_table::{ 38 | ColumnOperations, ColumnOrdering, SelectableRow, SelectableTable, SortOrder, 39 | }; 40 | // other use imports 41 | 42 | struct Config { 43 | // anything you want to pass 44 | } 45 | 46 | struct MyRow { 47 | field_1: String, 48 | // .. more fields 49 | } 50 | enum Column { 51 | Field1, 52 | // .. more column names 53 | } 54 | 55 | // Implement both traits for row and column 56 | impl ColumnOperations for Column { 57 | // The text of a row based on the column 58 | fn column_text(&self, row: &MyRow) -> String {} 59 | // Create your own header or no header 60 | fn create_header(&self, ui: &mut Ui, sort_order: Option, table: &mut SelectableTable) -> Option {} 61 | //Create your own table row UI 62 | fn create_table_row(&self, ui: &mut Ui, row: &SelectableRow, selected: bool, table: &mut SelectableTable,) -> Response {} 63 | } 64 | 65 | impl ColumnOrdering for Column { 66 | fn order_by(&self, row_1: &MyRow, row_2: &MyRow) -> std::cmp::Ordering { 67 | match self { 68 | Column::Field1 => row_1.field_1.cmp(&row_2.field_1), 69 | } 70 | } 71 | } 72 | 73 | pub struct MainWindow { 74 | table: SelectableTable 75 | } 76 | 77 | impl MainWindow { 78 | pub fn new() -> Self { 79 | Self { 80 | table: SelectableTable::new(vec![Column::Field1]) 81 | } 82 | } 83 | } 84 | 85 | impl eframe::App for MainWindow { 86 | fn update(&mut self, ctx: &eframe::egui::Context, _frame: &mut eframe::Frame) { 87 | egui::CentralPanel::default().show(ctx, |ui| { 88 | self.table.show_ui(ui |table| { 89 | table.striped(true) 90 | .cell_layout(Layout::left_to_right(Align::Center)) 91 | .column(Column::exact(column_size).clip(true)) 92 | }) 93 | }); 94 | } 95 | } 96 | 97 | ``` 98 | 99 | ## Run Demo 100 | 101 | The demo is accessible online via [this link](https://therustypickle.github.io/egui-selectable-table/) 102 | 103 | - Clone the repository `git clone https://github.com/TheRustyPickle/egui-selectable-table` 104 | - Move into the demo folder `cd egui-selectable-table/demo` 105 | 106 | - To run natively `cargo run --release` 107 | 108 | or 109 | 110 | - To run in wasm locally install the required target with `rustup target add wasm32-unknown-unknown` 111 | - Install Trunk with `cargo install --locked trunk` 112 | - `trunk serve` to run and visit `http://127.0.0.1:8080/` 113 | 114 | ## Contributing 115 | 116 | Contributions, issues, and feature requests are welcome! If you'd like to contribute, please open a pull request. 117 | 118 | ## License 119 | 120 | This project is licensed under the MIT License. 121 | -------------------------------------------------------------------------------- /src/fuzzy_matcher.rs: -------------------------------------------------------------------------------- 1 | use nucleo_matcher::pattern::{CaseMatching, Normalization, Pattern}; 2 | use nucleo_matcher::{Matcher, Utf32Str}; 3 | use rayon::prelude::*; 4 | use std::hash::Hash; 5 | 6 | use crate::{ColumnOperations, ColumnOrdering, SelectableRow, SelectableTable, SortOrder}; 7 | 8 | impl SelectableTable 9 | where 10 | Row: Clone + Send + Sync, 11 | F: Eq 12 | + Hash 13 | + Clone 14 | + Ord 15 | + Send 16 | + Sync 17 | + Default 18 | + ColumnOperations 19 | + ColumnOrdering, 20 | Conf: Default, 21 | { 22 | /// Performs a fuzzy search using specified columns across all rows and updates the displayed rows. 23 | /// 24 | /// This function filters the table rows based on a search `query` using `nucleo-matcher` 25 | /// crate. 26 | /// It checks the specified `column_list` for each row, concatenates their string representations, 27 | /// and scores them using the provided or generated `Pattern`. Only rows with a non-`None` score 28 | /// are retained. 29 | /// 30 | /// If a `limit` is provided, it will result at most `limit` rows. 31 | /// 32 | /// # Parameters: 33 | /// - `column_list`: A list of columns to search across. Does nothing if empty. 34 | /// - `query`: The search string. Does nothing if empty. 35 | /// - `limit`: Optional limit on the number of results returned. Does nothing if `0`. Defaults 36 | /// to no limit 37 | /// - `pattern`: Optional precomputed fuzzy `Pattern`. Default pattern is created from the query using 38 | /// case-insensitive matching and smart normalization. 39 | /// 40 | /// The search is relatively fast even with a million rows but it should not be called every 41 | /// frame and be used sparingly. 42 | /// 43 | /// To reset search results, call [`recreate_rows`](SelectableTable::recreate_rows). 44 | /// 45 | /// # Example: 46 | /// ```rust,ignore 47 | /// table.search_and_show(&vec![Column::Name, Column::Username], "john", Some(10), None); 48 | /// ``` 49 | pub fn search_and_show( 50 | &mut self, 51 | column_list: &Vec, 52 | query: &str, 53 | limit: Option, 54 | pattern: Option, 55 | ) { 56 | if query.is_empty() { 57 | return; 58 | } 59 | 60 | if column_list.is_empty() { 61 | return; 62 | } 63 | 64 | if let Some(limit) = limit 65 | && limit == 0 66 | { 67 | return; 68 | } 69 | 70 | let pattern = pattern.map_or_else( 71 | || Pattern::parse(query, CaseMatching::Ignore, Normalization::Smart), 72 | |pattern| pattern, 73 | ); 74 | 75 | let mut buf = Vec::new(); 76 | let mut row_data: Vec> = Vec::new(); 77 | 78 | for val in self.rows.values() { 79 | let mut string_val = String::new(); 80 | 81 | for column in column_list { 82 | let value = column.column_text(&val.row_data); 83 | string_val.push_str(&value); 84 | string_val.push(' '); 85 | } 86 | 87 | if pattern 88 | .score(Utf32Str::new(&string_val, &mut buf), &mut self.matcher) 89 | .is_some() 90 | { 91 | row_data.push(val.clone()); 92 | 93 | if let Some(max) = limit { 94 | if row_data.len() >= max { 95 | break; 96 | } 97 | } 98 | } 99 | } 100 | 101 | self.formatted_rows.clear(); 102 | self.active_rows.clear(); 103 | self.active_columns.clear(); 104 | 105 | row_data.par_sort_by(|a, b| { 106 | let ordering = self.sorted_by.order_by(&a.row_data, &b.row_data); 107 | match self.sort_order { 108 | SortOrder::Ascending => ordering, 109 | SortOrder::Descending => ordering.reverse(), 110 | } 111 | }); 112 | 113 | self.indexed_ids = row_data 114 | .par_iter() 115 | .enumerate() 116 | .map(|(index, row)| (row.id, index)) 117 | .collect(); 118 | 119 | self.formatted_rows = row_data; 120 | } 121 | 122 | /// Sets a custom matcher to use for fuzzy searching rows 123 | /// 124 | /// This allows the table to use a custom `Matcher` from `nucleo-matcher` crate 125 | /// for searching/filtering through rows based on the input text. Use this to change 126 | /// the search algorithm or tweak scoring behavior. 127 | /// 128 | /// # Parameters: 129 | /// - `matcher`: The matcher instance to use for row filtering. 130 | /// 131 | /// # Returns: 132 | /// - `Self`: The modified table with the specified matcher applied. 133 | /// 134 | /// # Example: 135 | /// ```rust,ignore 136 | /// let matcher = Matcher::default(); 137 | /// let table = SelectableTable::new(columns) 138 | /// .matcher(matcher); 139 | /// ``` 140 | #[must_use] 141 | pub fn matcher(mut self, matcher: Matcher) -> Self { 142 | self.matcher = matcher; 143 | self 144 | } 145 | 146 | /// Replaces the current matcher with a new one. 147 | /// 148 | /// This method allows updating the fuzzy search matcher dynamically. 149 | /// 150 | /// # Parameters: 151 | /// - `matcher`: The new matcher instance to set. 152 | /// 153 | /// # Example: 154 | /// ```rust,ignore 155 | /// table.set_matcher(new_matcher); 156 | /// ``` 157 | pub fn set_matcher(&mut self, matcher: Matcher) { 158 | self.matcher = matcher; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/row_modification.rs: -------------------------------------------------------------------------------- 1 | use egui::ahash::{HashMap, HashSet, HashSetExt}; 2 | use rayon::prelude::*; 3 | use std::hash::Hash; 4 | 5 | use crate::{ColumnOperations, ColumnOrdering, SelectableRow, SelectableTable, SortOrder}; 6 | 7 | impl SelectableTable 8 | where 9 | Row: Clone + Send + Sync, 10 | F: Eq 11 | + Hash 12 | + Clone 13 | + Ord 14 | + Send 15 | + Sync 16 | + Default 17 | + ColumnOperations 18 | + ColumnOrdering, 19 | Conf: Default, 20 | { 21 | /// Modify or add rows to the table. Changes are not immediately reflected in the UI. 22 | /// You must call [`recreate_rows`](#method.recreate_rows) or [`recreate_rows_no_unselect`](#method.recreate_rows_no_unselect) to apply these changes visually. 23 | /// 24 | /// # Parameters: 25 | /// - `table`: A closure that takes a mutable reference to the rows and optionally returns a new row. 26 | /// If a row is returned, it will be added to the table. 27 | /// 28 | /// # Auto Reload: 29 | /// - Use [`auto_reload`](#method.auto_reload) to automatically refresh the UI after a specified 30 | /// number of row modifications or additions. 31 | /// 32 | /// # Returns 33 | /// * `Option` - The row id that is used internally for the new row 34 | /// 35 | /// # Example: 36 | /// ```rust,ignore 37 | /// let new_row_id = table.add_modify_row(|rows| { 38 | /// let my_row = rows.get_mut(row_id).unwrap(); 39 | /// // modify your row as necessary 40 | /// 41 | /// let new_row = MyRow { 42 | /// // Define your row values 43 | /// }; 44 | /// Some(new_row) // Optionally add a new row 45 | /// }); 46 | /// ``` 47 | pub fn add_modify_row(&mut self, table: Fn) -> Option 48 | where 49 | Fn: FnOnce(&mut HashMap>) -> Option, 50 | { 51 | let new_row = table(&mut self.rows); 52 | 53 | let mut to_return = None; 54 | 55 | if let Some(row) = new_row { 56 | let selected_columns = HashSet::new(); 57 | let new_row = SelectableRow { 58 | row_data: row, 59 | id: self.last_id_used, 60 | selected_columns, 61 | }; 62 | to_return = Some(self.last_id_used); 63 | self.rows.insert(new_row.id, new_row); 64 | self.last_id_used += 1; 65 | } 66 | 67 | let reload = self.auto_reload.increment_count(); 68 | 69 | if reload { 70 | self.recreate_rows(); 71 | } 72 | to_return 73 | } 74 | 75 | /// Modify only the rows currently displayed in the UI. 76 | /// 77 | /// This provides direct access to the currently formatted rows for lightweight updates. 78 | /// 79 | /// # Important: 80 | /// - This does **not** require calling `recreate_rows` to reflect changes in the UI. 81 | /// - **Do not delete rows** from inside this closure — doing so will **cause a panic** and break internal assumptions. 82 | /// To safely delete a row, use [`add_modify_row`](#method.add_modify_row) and then call [`recreate_rows`](#method.recreate_rows) or [`recreate_rows_no_unselect`](#method.recreate_rows_no_unselect). 83 | /// - Can be used alongside [`add_modify_row`](#method.add_modify_row) to show updated data immediately. 84 | /// When row recreation happens, the modified data will be preserved as long as it's updated via [`add_modify_row`](#method.add_modify_row). 85 | /// - Does not contribute toward [`auto_reload`](#method.auto_reload) count. 86 | /// 87 | /// # Parameters: 88 | /// - `table`: A closure that takes a mutable reference to the currently formatted rows and an index map. 89 | /// 90 | /// # Example: 91 | /// ```rust,ignore 92 | /// table.modify_shown_row(|formatted_rows, indexed_ids| { 93 | /// let row_id = 0; 94 | /// let target_index = indexed_ids.get(&row_id).unwrap(); 95 | /// let row = formatted_rows.get_mut(*target_index).unwrap(); 96 | /// // Safely modify row contents here 97 | /// }); 98 | /// ``` 99 | pub fn modify_shown_row(&mut self, mut rows: Fn) 100 | where 101 | Fn: FnMut(&mut Vec>, &HashMap), 102 | { 103 | rows(&mut self.formatted_rows, &self.indexed_ids); 104 | } 105 | 106 | /// Adds a new row to the bottom of the table without applying any sorting logic. 107 | /// 108 | /// This method inserts the row as-is at the end of the table, assigns it a unique ID, and 109 | /// returns it as a `SelectableRow`. This does **not** 110 | /// require calling [`recreate_rows`](#method.recreate_rows) for the row to appear in the UI. 111 | /// 112 | /// # Parameters: 113 | /// - `row`: The data to insert into the table. 114 | /// 115 | /// # Returns: 116 | /// - `SelectableRow`: The newly added row wrapped in a `SelectableRow`. 117 | /// 118 | /// # Example: 119 | /// ```rust,ignore 120 | /// let row = Row::new(vec![cell1, cell2, cell3]); 121 | /// let added_row = table.add_unsorted_row(row); 122 | /// ``` 123 | pub fn add_unsorted_row(&mut self, row: Row) -> SelectableRow { 124 | let selected_columns = HashSet::new(); 125 | let new_row = SelectableRow { 126 | row_data: row, 127 | id: self.last_id_used, 128 | selected_columns, 129 | }; 130 | 131 | self.formatted_rows.push(new_row.clone()); 132 | self.indexed_ids 133 | .insert(new_row.id, self.formatted_rows.len() - 1); 134 | self.rows.insert(new_row.id, new_row.clone()); 135 | self.last_id_used += 1; 136 | new_row 137 | } 138 | 139 | /// Sort the rows to the current sorting order and column and save them for later reuse 140 | pub(crate) fn sort_rows(&mut self) { 141 | let mut row_data: Vec> = 142 | self.rows.par_iter().map(|(_, v)| v.clone()).collect(); 143 | 144 | row_data.par_sort_by(|a, b| { 145 | let ordering = self.sorted_by.order_by(&a.row_data, &b.row_data); 146 | match self.sort_order { 147 | SortOrder::Ascending => ordering, 148 | SortOrder::Descending => ordering.reverse(), 149 | } 150 | }); 151 | 152 | let indexed_data = row_data 153 | .par_iter() 154 | .enumerate() 155 | .map(|(index, row)| (row.id, index)) 156 | .collect(); 157 | 158 | self.indexed_ids = indexed_data; 159 | self.formatted_rows = row_data; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/auto_scroll.rs: -------------------------------------------------------------------------------- 1 | use egui::{Pos2, Rect}; 2 | use std::hash::Hash; 3 | 4 | use crate::{ColumnOperations, ColumnOrdering, SelectableTable}; 5 | 6 | /// Handles automatic scrolling when dragging items near the edges of the table's view. 7 | /// 8 | /// The `AutoScroll` struct allows the table to automatically scroll when the user drags items 9 | /// near the top or bottom edge of the view. It provides configurable parameters such as 10 | /// the speed of scrolling and the distances from the edges at which scrolling is triggered. 11 | pub struct AutoScroll { 12 | /// The current vertical scroll offset. 13 | pub scroll_offset: f32, 14 | /// Whether auto-scrolling is enabled or disabled. 15 | pub enabled: bool, 16 | /// The minimum distance from the top edge before auto-scrolling starts. Extra space due to the 17 | /// header being in the way. Default: 200.0 18 | pub distance_from_min: f32, 19 | /// The minimum distance from the bottom edge before auto-scrolling starts. Default: 120.0 20 | pub distance_from_max: f32, 21 | /// The maximum speed at which auto-scrolling occurs. Default: 30.0 22 | pub max_speed: f32, 23 | } 24 | 25 | impl Default for AutoScroll { 26 | fn default() -> Self { 27 | Self { 28 | scroll_offset: 0.0, 29 | enabled: false, 30 | distance_from_min: 200.0, 31 | distance_from_max: 120.0, 32 | max_speed: 30.0, 33 | } 34 | } 35 | } 36 | 37 | impl AutoScroll { 38 | /// Creates a new instance of `AutoScroll` with the option to enable or disable auto-scrolling. 39 | /// 40 | /// # Parameters: 41 | /// - `enabled`: Whether auto-scrolling should be enabled. 42 | /// 43 | /// # Example: 44 | /// ```rust,ignore 45 | /// let auto_scroll = AutoScroll::new(true); // Enables auto-scrolling 46 | /// ``` 47 | #[must_use] 48 | pub fn new(enabled: bool) -> Self { 49 | Self { 50 | enabled, 51 | ..Default::default() 52 | } 53 | } 54 | 55 | /// Sets the minimum distance from the top edge at which auto-scrolling is triggered. 56 | /// 57 | /// # Parameters: 58 | /// - `distance`: The distance from the top edge in pixels. 59 | /// 60 | /// # Returns: 61 | /// An updated instance of `AutoScroll` with the specified `distance_from_min` value. 62 | /// 63 | /// # Considerations: 64 | /// - Add some extra distance due to the header being in the way of selection 65 | /// 66 | /// # Example: 67 | /// ```rust,ignore 68 | /// let auto_scroll = AutoScroll::new(true).distance_from_min(100.0); // Auto-scrolls when 100 pixels from top 69 | /// ``` 70 | #[must_use] 71 | pub const fn distance_from_min(mut self, distance: f32) -> Self { 72 | self.distance_from_min = distance; 73 | self 74 | } 75 | /// Sets the minimum distance from the bottom edge at which auto-scrolling is triggered. 76 | /// 77 | /// # Parameters: 78 | /// - `distance`: The distance from the bottom edge in pixels. 79 | /// 80 | /// # Returns: 81 | /// An updated instance of `AutoScroll` with the specified `distance_from_max` value. 82 | /// 83 | /// # Example: 84 | /// ```rust,ignore 85 | /// let auto_scroll = AutoScroll::new(true).distance_from_max(80.0); // Auto-scrolls when 80 pixels from bottom 86 | /// ``` 87 | #[must_use] 88 | pub const fn distance_from_max(mut self, distance: f32) -> Self { 89 | self.distance_from_max = distance; 90 | self 91 | } 92 | 93 | /// Sets the maximum scroll speed when auto-scrolling is triggered. 94 | /// 95 | /// # Parameters: 96 | /// - `speed`: The maximum scroll speed 97 | /// 98 | /// # Returns: 99 | /// An updated instance of `AutoScroll` with the specified `max_speed`. 100 | /// 101 | /// # Example: 102 | /// ```rust,ignore 103 | /// let auto_scroll = AutoScroll::new(true).max_speed(50.0); // Sets the max scroll speed to 50.0 104 | /// ``` 105 | #[must_use] 106 | pub const fn max_speed(mut self, speed: f32) -> Self { 107 | self.max_speed = speed; 108 | self 109 | } 110 | 111 | /// Calculate the position based on the rectangle and return the new vertical offset 112 | pub(crate) fn start_scroll(&mut self, max_rect: Rect, pointer: Option) -> Option { 113 | if !self.enabled { 114 | return None; 115 | } 116 | 117 | if let Some(loc) = pointer { 118 | let pointer_y = loc.y; 119 | 120 | // Min gets a bit more space as the header is along the way 121 | let min_y = max_rect.min.y + self.distance_from_min; 122 | let max_y = max_rect.max.y - self.distance_from_max; 123 | 124 | // Check if the pointer is within the allowed Y range 125 | let within_y = pointer_y >= min_y && pointer_y <= max_y; 126 | 127 | // Whether the mouse is above the minimum y point 128 | let above_y = pointer_y < min_y; 129 | // Whether the mouse is below the maximum y point 130 | let below_y = pointer_y > max_y; 131 | 132 | let max_speed = self.max_speed; 133 | 134 | // Only scroll if the pointer is outside the allowed Y range 135 | if !within_y { 136 | let distance: f32; 137 | let direction: f32; // -1 for upwards, 1 for downwards 138 | 139 | if above_y { 140 | // If above, calculate distance from min_y and scroll upwards 141 | distance = (min_y - pointer_y).abs(); 142 | direction = -1.0; // Scroll up 143 | } else if below_y { 144 | // If below, calculate distance from max_y and scroll downwards 145 | distance = (pointer_y - max_y).abs(); 146 | direction = 1.0; // Scroll down 147 | } else { 148 | return None; 149 | } 150 | 151 | // Scale the speed by distance, with a cap at max_speed 152 | let speed_factor = max_speed * (distance / 100.0).clamp(0.1, 1.0); 153 | 154 | self.scroll_offset += direction * speed_factor; 155 | 156 | // Ensure scroll offset doesn't go negative 157 | if self.scroll_offset < 0.0 { 158 | self.scroll_offset = 0.0; 159 | } 160 | 161 | return Some(self.scroll_offset); 162 | } 163 | } 164 | None 165 | } 166 | } 167 | 168 | /// Enables or configures auto-scrolling behavior in the table view. 169 | impl SelectableTable 170 | where 171 | Row: Clone + Send + Sync, 172 | F: Eq 173 | + Hash 174 | + Clone 175 | + Ord 176 | + Send 177 | + Sync 178 | + Default 179 | + ColumnOperations 180 | + ColumnOrdering, 181 | Conf: Default, 182 | { 183 | pub(crate) const fn update_scroll_offset(&mut self, offset: f32) { 184 | self.auto_scroll.scroll_offset = offset; 185 | } 186 | 187 | /// Enables auto-scrolling when dragging near the edges of the view. 188 | /// 189 | /// # Returns: 190 | /// An updated instance of the table with auto-scrolling enabled. 191 | /// 192 | /// # Example: 193 | /// ```rust,ignore 194 | /// let table = SelectableTable::new(vec![col1, col2, col3]).auto_scroll() 195 | /// ``` 196 | #[must_use] 197 | pub const fn auto_scroll(mut self) -> Self { 198 | self.auto_scroll.enabled = true; 199 | self 200 | } 201 | /// Sets the maximum scroll speed for auto-scrolling. 202 | /// 203 | /// # Parameters: 204 | /// - `speed`: The maximum scroll speed (in pixels per frame) when auto-scrolling is active. 205 | /// 206 | /// # Returns: 207 | /// An updated instance of the table with the new scroll speed. 208 | /// 209 | /// # Example: 210 | /// ```rust,ignore 211 | /// let table = SelectableTable::new(vec![col1, col2, col3]) 212 | /// .auto_scroll().scroll_speed(50.0); 213 | /// ``` 214 | #[must_use] 215 | pub const fn scroll_speed(mut self, speed: f32) -> Self { 216 | self.auto_scroll.max_speed = speed; 217 | self 218 | } 219 | 220 | /// Configures the auto-scrolling behavior by providing a new `AutoScroll` instance. 221 | /// 222 | /// # Parameters: 223 | /// - `scroll`: A custom `AutoScroll` instance with defined scroll behavior. 224 | /// 225 | /// # Returns: 226 | /// An updated instance of the table with the provided `AutoScroll` configuration. 227 | /// 228 | /// # Example: 229 | /// ```rust,ignore 230 | /// let scroll_settings = AutoScroll::new(true).max_speed(50.0); 231 | /// let table = SelectableTable::new(vec![col1, col2, col3]) 232 | /// .set_auto_scroll(scroll_settings); 233 | /// ``` 234 | #[must_use] 235 | pub const fn set_auto_scroll(mut self, scroll: AutoScroll) -> Self { 236 | self.auto_scroll = scroll; 237 | self 238 | } 239 | /// Updates the table's auto-scrolling settings with a new `AutoScroll` instance. 240 | /// 241 | /// # Parameters: 242 | /// - `scroll`: The new `AutoScroll` settings to apply. 243 | /// 244 | /// This method is used when you need to change the auto-scroll behavior at runtime. 245 | /// 246 | /// # Example: 247 | /// ```rust,ignore 248 | /// let new_scroll_settings = AutoScroll::new(true).max_speed(60.0); 249 | /// table.update_auto_scroll(new_scroll_settings); // Update the auto-scroll settings during runtime 250 | /// ``` 251 | pub const fn update_auto_scroll(&mut self, scroll: AutoScroll) { 252 | self.auto_scroll = scroll; 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /demo/src/app.rs: -------------------------------------------------------------------------------- 1 | use eframe::{App, CreationContext, Frame}; 2 | use egui::ahash::{HashSet, HashSetExt}; 3 | use egui::{ 4 | Align, Button, CentralPanel, Context, Layout, Slider, TextEdit, ThemePreference, Ui, Visuals, 5 | }; 6 | use egui_extras::Column; 7 | use egui_selectable_table::{ 8 | AutoScroll, ColumnOperations, ColumnOrdering, SelectableRow, SelectableTable, SortOrder, 9 | }; 10 | use egui_theme_lerp::ThemeAnimator; 11 | use strum::IntoEnumIterator; 12 | use strum_macros::{Display, EnumIter}; 13 | 14 | #[derive(Default, Clone, Copy)] 15 | pub struct Config { 16 | counting_ongoing: bool, 17 | } 18 | 19 | pub struct MainWindow { 20 | search_text: String, 21 | select_entire_row: bool, 22 | add_rows: bool, 23 | auto_scrolling: bool, 24 | row_to_add: u64, 25 | row_num: u64, 26 | row_count: u64, 27 | scroll_speed: f32, 28 | reload_counter: u32, 29 | table: SelectableTable, 30 | conf: Config, 31 | theme_animator: ThemeAnimator, 32 | search_column_list: HashSet, 33 | } 34 | 35 | impl MainWindow { 36 | pub fn new(cc: &CreationContext) -> Self { 37 | cc.egui_ctx 38 | .options_mut(|a| a.theme_preference = ThemePreference::Light); 39 | 40 | let all_columns = TableColumns::iter().collect(); 41 | 42 | // Auto reload after each 10k table row add or modification 43 | let table = SelectableTable::new(all_columns) 44 | .auto_reload(10_000) 45 | .auto_scroll() 46 | .horizontal_scroll() 47 | .no_ctrl_a_capture(); 48 | 49 | MainWindow { 50 | search_text: String::new(), 51 | select_entire_row: false, 52 | add_rows: false, 53 | auto_scrolling: true, 54 | row_to_add: 0, 55 | row_num: 0, 56 | row_count: 0, 57 | scroll_speed: 30.0, 58 | reload_counter: 0, 59 | table, 60 | conf: Config::default(), 61 | theme_animator: ThemeAnimator::new(Visuals::light(), Visuals::dark()), 62 | search_column_list: HashSet::new(), 63 | } 64 | } 65 | } 66 | 67 | impl App for MainWindow { 68 | fn update(&mut self, ctx: &Context, _frame: &mut Frame) { 69 | let theme_emoji = if !self.theme_animator.animation_done { 70 | if self.theme_animator.theme_1_to_2 { 71 | "☀" 72 | } else { 73 | "🌙" 74 | } 75 | } else if self.theme_animator.theme_1_to_2 { 76 | "🌙" 77 | } else { 78 | "☀" 79 | }; 80 | 81 | CentralPanel::default().show(ctx, |ui| { 82 | if self.theme_animator.anim_id.is_none() { 83 | self.theme_animator.create_id(ui); 84 | } else { 85 | self.theme_animator.animate(ctx); 86 | } 87 | 88 | ui.horizontal(|ui| { 89 | if ui.button(theme_emoji).clicked() { 90 | self.theme_animator.start(); 91 | } 92 | 93 | ui.separator(); 94 | ui.label("Total Rows to add:"); 95 | ui.add(Slider::new(&mut self.row_count, 10000..=1_000_000)); 96 | 97 | let button_enabled = !self.add_rows; 98 | let button = ui.add_enabled(button_enabled, Button::new("Create Rows")); 99 | if button.clicked() { 100 | self.add_rows = true; 101 | self.row_to_add = self.row_count; 102 | 103 | // Clear previously added rows 104 | self.table.clear_all_rows(); 105 | self.table.set_auto_reload(Some(self.reload_counter)); 106 | self.conf.counting_ongoing = true; 107 | }; 108 | ui.separator(); 109 | if ui 110 | .checkbox(&mut self.select_entire_row, "Select Entire Row?") 111 | .changed() 112 | { 113 | self.table.set_select_full_row(self.select_entire_row); 114 | }; 115 | ui.separator(); 116 | if ui.button("Add unsorted row at the bottom").clicked() { 117 | let new_row = TableRow { 118 | field_1: self.row_num, 119 | field_2: self.row_num * 3, 120 | field_3: self.row_num * 8, 121 | field_4: self.row_num * 11, 122 | field_5: self.row_num * 17, 123 | field_6: self.row_num * 21, 124 | create_count: 0, 125 | }; 126 | self.row_num += 1; 127 | self.table.add_unsorted_row(new_row); 128 | } 129 | }); 130 | ui.separator(); 131 | ui.horizontal(|ui| { 132 | ui.label("Auto scrolling speed:"); 133 | if ui 134 | .add(Slider::new(&mut self.scroll_speed, 10.0..=100.0)) 135 | .changed() 136 | { 137 | let scroll = AutoScroll::new(self.auto_scrolling).max_speed(self.scroll_speed); 138 | self.table.update_auto_scroll(scroll); 139 | }; 140 | ui.separator(); 141 | if ui 142 | .checkbox(&mut self.auto_scrolling, "Enable Auto Scrolling on drag?") 143 | .changed() 144 | { 145 | let scroll = AutoScroll::new(self.auto_scrolling); 146 | self.table.update_auto_scroll(scroll); 147 | } 148 | }); 149 | ui.separator(); 150 | ui.horizontal(|ui| { 151 | ui.label("Row Recreation Counter:"); 152 | ui.add(Slider::new(&mut self.reload_counter, 10000..=1000000)); 153 | ui.label("Higher value = Less often the UI is refreshed") 154 | }); 155 | ui.separator(); 156 | 157 | if self.row_count * 10 / 100 > self.reload_counter as u64 { 158 | ui.horizontal(|ui| { 159 | ui.label( 160 | "⚠ Row count too high. Increase recreation counter to prevent ui freeze", 161 | ); 162 | }); 163 | ui.separator(); 164 | } 165 | 166 | ui.horizontal(|ui| { 167 | let text_edit = 168 | TextEdit::singleline(&mut self.search_text).hint_text("Search for rows"); 169 | 170 | ui.add(text_edit); 171 | 172 | if ui.button("Search").clicked() { 173 | let column_list: Vec = 174 | self.search_column_list.clone().into_iter().collect(); 175 | 176 | self.table 177 | .search_and_show(&column_list, &self.search_text, None, None); 178 | }; 179 | 180 | if ui.button("Clear").clicked() { 181 | self.search_text.clear(); 182 | self.table.recreate_rows(); 183 | }; 184 | 185 | ui.separator(); 186 | 187 | let all_columns: Vec = TableColumns::iter().collect(); 188 | for col in all_columns.into_iter() { 189 | if ui 190 | .selectable_label(self.search_column_list.contains(&col), col.to_string()) 191 | .clicked() 192 | { 193 | if self.search_column_list.contains(&col) { 194 | self.search_column_list.remove(&col); 195 | } else { 196 | self.search_column_list.insert(col); 197 | } 198 | }; 199 | } 200 | }); 201 | 202 | ui.separator(); 203 | 204 | self.table.show_ui(ui, |table| { 205 | let mut table = table 206 | .drag_to_scroll(false) 207 | .striped(true) 208 | .resizable(true) 209 | .cell_layout(Layout::left_to_right(Align::Center)) 210 | .drag_to_scroll(false) 211 | .auto_shrink([false; 2]) 212 | .min_scrolled_height(0.0); 213 | 214 | for _col in TableColumns::iter() { 215 | table = table.column(Column::initial(150.0)) 216 | } 217 | table 218 | }); 219 | self.table.set_config(self.conf); 220 | 221 | if self.add_rows { 222 | for _num in 0..10000 { 223 | self.table.add_modify_row(|_| { 224 | let new_row = TableRow { 225 | field_1: self.row_num, 226 | field_2: self.row_num * 3, 227 | field_3: self.row_num * 8, 228 | field_4: self.row_num * 11, 229 | field_5: self.row_num * 17, 230 | field_6: self.row_num * 21, 231 | create_count: 0, 232 | }; 233 | Some(new_row) 234 | }); 235 | self.row_num += 1; 236 | if self.row_num > self.row_to_add { 237 | self.add_rows = false; 238 | self.row_to_add = 0; 239 | self.row_num = 0; 240 | // forcefully reload the table as there are no more rows coming 241 | self.table.recreate_rows(); 242 | self.conf.counting_ongoing = false; 243 | self.table.set_auto_reload(None); 244 | 245 | break; 246 | } 247 | } 248 | // Ensure it does not wait for an event on the app to load the new rows 249 | ctx.request_repaint(); 250 | } 251 | }); 252 | } 253 | } 254 | 255 | #[derive(Clone, Default)] 256 | struct TableRow { 257 | field_1: u64, 258 | field_2: u64, 259 | field_3: u64, 260 | field_4: u64, 261 | field_5: u64, 262 | field_6: u64, 263 | create_count: u64, 264 | } 265 | 266 | #[derive(Eq, PartialEq, Debug, Ord, PartialOrd, Clone, Copy, Hash, Default, EnumIter, Display)] 267 | enum TableColumns { 268 | #[default] 269 | #[strum(to_string = "Column 1")] 270 | Field1, 271 | #[strum(to_string = "Column 2")] 272 | Field2, 273 | #[strum(to_string = "Column 3")] 274 | Field3, 275 | #[strum(to_string = "Column 4")] 276 | Field4, 277 | #[strum(to_string = "Column 5")] 278 | Field5, 279 | #[strum(to_string = "Column 6")] 280 | Field6, 281 | #[strum(to_string = "Render Counter")] 282 | Field7, 283 | } 284 | 285 | impl ColumnOperations for TableColumns { 286 | fn column_text(&self, row: &TableRow) -> String { 287 | match self { 288 | TableColumns::Field1 => row.field_1.to_string(), 289 | TableColumns::Field2 => row.field_2.to_string(), 290 | TableColumns::Field3 => row.field_3.to_string(), 291 | TableColumns::Field4 => row.field_4.to_string(), 292 | TableColumns::Field5 => row.field_5.to_string(), 293 | TableColumns::Field6 => row.field_6.to_string(), 294 | TableColumns::Field7 => row.create_count.to_string(), 295 | } 296 | } 297 | fn create_header( 298 | &self, 299 | ui: &mut Ui, 300 | sort_order: Option, 301 | _table: &mut SelectableTable, 302 | ) -> Option { 303 | let mut text = self.to_string(); 304 | 305 | if let Some(sort) = sort_order { 306 | match sort { 307 | SortOrder::Ascending => text += "🔽", 308 | SortOrder::Descending => text += "🔼", 309 | } 310 | } 311 | let selected = sort_order.is_some(); 312 | let resp = ui.add_sized(ui.available_size(), Button::selectable(selected, text)); 313 | Some(resp) 314 | } 315 | fn create_table_row( 316 | &self, 317 | ui: &mut Ui, 318 | row: &SelectableRow, 319 | cell_selected: bool, 320 | table: &mut SelectableTable, 321 | ) -> egui::Response { 322 | let row_id = row.id; 323 | let row_data = &row.row_data; 324 | let config = table.config; 325 | 326 | let text = match self { 327 | TableColumns::Field1 => row_data.field_1.to_string(), 328 | TableColumns::Field2 => row_data.field_2.to_string(), 329 | TableColumns::Field3 => row_data.field_3.to_string(), 330 | TableColumns::Field4 => row_data.field_4.to_string(), 331 | TableColumns::Field5 => row_data.field_5.to_string(), 332 | TableColumns::Field6 => row_data.field_6.to_string(), 333 | TableColumns::Field7 => row_data.create_count.to_string(), 334 | }; 335 | 336 | // Persist the creation count, while row creation is ongoing, this will get auto 337 | // reloaded. After there is no more row creation, auto reload is turned off and won't 338 | // reload until next manual intervention. While no more rows are being created, we are 339 | // modifying the rows directly that are being shown in the UI which is much less 340 | // expensive and gets shown to the UI immediately. 341 | // Continue to update the persistent row data to ensure once reload happens, the 342 | // previous count data is not lost 343 | table.add_modify_row(|table| { 344 | let target_row = table.get_mut(&row_id).unwrap(); 345 | target_row.row_data.create_count += 1; 346 | None 347 | }); 348 | if !config.counting_ongoing { 349 | table.modify_shown_row(|t, index| { 350 | let target_index = index.get(&row_id).unwrap(); 351 | let target_row = t.get_mut(*target_index).unwrap(); 352 | target_row.row_data.create_count += 1; 353 | }); 354 | } 355 | 356 | // The same approach works for both cell based selection and for entire row selection on 357 | // drag. 358 | let resp = ui.add_sized(ui.available_size(), Button::selectable(cell_selected, text)); 359 | 360 | resp.context_menu(|ui| { 361 | if ui.button("Select All Rows").clicked() { 362 | table.select_all(); 363 | ui.close(); 364 | } 365 | if ui.button("Unselect All Rows").clicked() { 366 | table.unselect_all(); 367 | ui.close(); 368 | } 369 | if ui.button("Copy Selected Cells").clicked() { 370 | table.copy_selected_cells(ui); 371 | ui.close(); 372 | } 373 | if ui.button("Mark row as selected").clicked() { 374 | ui.close(); 375 | table.mark_row_as_selected(row_id, None); 376 | } 377 | }); 378 | resp 379 | } 380 | } 381 | 382 | impl ColumnOrdering for TableColumns { 383 | fn order_by(&self, row_1: &TableRow, row_2: &TableRow) -> std::cmp::Ordering { 384 | match self { 385 | TableColumns::Field1 => row_1.field_1.cmp(&row_2.field_1), 386 | TableColumns::Field2 => row_1.field_2.cmp(&row_2.field_2), 387 | TableColumns::Field3 => row_1.field_3.cmp(&row_2.field_3), 388 | TableColumns::Field4 => row_1.field_4.cmp(&row_2.field_4), 389 | TableColumns::Field5 => row_1.field_5.cmp(&row_2.field_5), 390 | TableColumns::Field6 => row_1.field_6.cmp(&row_2.field_6), 391 | TableColumns::Field7 => row_1.create_count.cmp(&row_2.create_count), 392 | } 393 | } 394 | } 395 | -------------------------------------------------------------------------------- /src/row_selection.rs: -------------------------------------------------------------------------------- 1 | use egui::Ui; 2 | use egui::ahash::{HashMap, HashMapExt, HashSet, HashSetExt}; 3 | use std::fmt::Write as _; 4 | use std::hash::Hash; 5 | 6 | use crate::{ColumnOperations, ColumnOrdering, SelectableRow, SelectableTable}; 7 | 8 | /// Functions related to selection of rows and columns 9 | #[allow(clippy::too_many_lines)] 10 | impl SelectableTable 11 | where 12 | Row: Clone + Send + Sync, 13 | F: Eq 14 | + Hash 15 | + Clone 16 | + Ord 17 | + Send 18 | + Sync 19 | + Default 20 | + ColumnOperations 21 | + ColumnOrdering, 22 | Conf: Default, 23 | { 24 | pub(crate) fn select_single_row_cell(&mut self, id: i64, column_name: &F) { 25 | self.active_columns.insert(column_name.clone()); 26 | self.active_rows.insert(id); 27 | 28 | // Should never panic, if it does, either a library issue or it was used incorrectly 29 | let target_index = self.indexed_ids.get(&id).expect("target_index not found"); 30 | let target_row = self 31 | .formatted_rows 32 | .get_mut(*target_index) 33 | .expect("target_row not found"); 34 | 35 | if self.select_full_row { 36 | self.active_columns.extend(self.all_columns.clone()); 37 | 38 | target_row.selected_columns.extend(self.all_columns.clone()); 39 | } else { 40 | target_row.selected_columns.insert(column_name.clone()); 41 | } 42 | 43 | self.active_rows.insert(id); 44 | } 45 | 46 | /// Marks a row as selected, optionally selecting specific columns within the row. 47 | /// 48 | /// If a list of columns is provided, only those columns are marked as selected for the row. 49 | /// If no column list is provided, all columns in the row are marked as selected. 50 | /// 51 | /// # Parameters: 52 | /// - `id`: The unique identifier of the row to mark as selected. 53 | /// - `column`: An optional list of columns (`Vec`) to mark as selected within the row. If `None`, all columns are selected. 54 | /// 55 | /// # Example: 56 | /// ```rust,ignore 57 | /// table.mark_row_as_selected(42, Some(vec!["Name", "Age"])); 58 | /// table.mark_row_as_selected(43, None); // Selects all columns in row 43 59 | /// ``` 60 | pub fn mark_row_as_selected(&mut self, id: i64, column: Option>) { 61 | let Some(target_index) = self.indexed_ids.get(&id) else { 62 | return; 63 | }; 64 | 65 | let Some(target_row) = self.formatted_rows.get_mut(*target_index) else { 66 | return; 67 | }; 68 | 69 | self.active_rows.insert(id); 70 | 71 | if let Some(column_list) = column { 72 | self.active_columns.extend(column_list.clone()); 73 | 74 | target_row.selected_columns.extend(column_list); 75 | } else { 76 | self.active_columns.extend(self.all_columns.clone()); 77 | 78 | target_row.selected_columns.extend(self.all_columns.clone()); 79 | } 80 | } 81 | 82 | pub(crate) fn select_dragged_row_cell( 83 | &mut self, 84 | id: i64, 85 | column_name: &F, 86 | is_ctrl_pressed: bool, 87 | ) { 88 | // If both same then the mouse is still on the same column on the same row so nothing to process 89 | if self.last_active_row == Some(id) && self.last_active_column == Some(column_name.clone()) 90 | { 91 | return; 92 | } 93 | 94 | if self.formatted_rows.is_empty() { 95 | return; 96 | } 97 | 98 | self.active_columns.insert(column_name.clone()); 99 | self.beyond_drag_point = true; 100 | 101 | let drag_start = self.drag_started_on.clone().expect("Drag start not found"); 102 | 103 | // number of the column of drag starting point and the current cell that we are trying to select 104 | let drag_start_num = self.column_to_num(&drag_start.1); 105 | let ongoing_column_num = self.column_to_num(column_name); 106 | 107 | let mut new_column_set = HashSet::new(); 108 | 109 | let get_previous = ongoing_column_num > drag_start_num; 110 | let mut ongoing_val = Some(drag_start.1.clone()); 111 | 112 | // row1: column(drag started here) column column 113 | // row2: column column column 114 | // row3: column column column 115 | // row4: column column column (currently here) 116 | // 117 | // The goal of this is to ensure from the drag starting point to all the columns till the currently here 118 | // are considered selected and the rest are removed from active selection even if it was considered active 119 | // 120 | // During fast mouse movement active rows can contain columns that are not in the range we are targeting 121 | // We go from one point to the other point and ensure except those columns nothing else is selected 122 | // 123 | // No active row removal if ctrl is being pressed! 124 | if is_ctrl_pressed { 125 | self.active_columns.insert(column_name.clone()); 126 | } else if ongoing_column_num == drag_start_num { 127 | new_column_set.insert(drag_start.1.clone()); 128 | self.active_columns = new_column_set; 129 | } else { 130 | while let Some(col) = ongoing_val { 131 | let next_column = if get_previous { 132 | self.next_column(&col) 133 | } else { 134 | self.previous_column(&col) 135 | }; 136 | 137 | new_column_set.insert(col); 138 | 139 | if &next_column == column_name { 140 | new_column_set.insert(next_column); 141 | ongoing_val = None; 142 | } else { 143 | ongoing_val = Some(next_column); 144 | } 145 | } 146 | self.active_columns = new_column_set; 147 | } 148 | 149 | let current_row_index = self 150 | .indexed_ids 151 | .get(&id) 152 | .expect("Current row index not found"); 153 | // The row the mouse pointer is on 154 | let current_row = self 155 | .formatted_rows 156 | .get_mut(*current_row_index) 157 | .expect("Current row not found"); 158 | 159 | // If this row already selects the column that we are trying to select, it means the mouse 160 | // moved backwards from an active column to another active column. 161 | // 162 | // Row: column1 column2 (mouse is here) column3 column4 163 | // 164 | // In this case, if column 3 or 4 is also found in the active selection then 165 | // the mouse moved backwards 166 | let row_contains_column = current_row.selected_columns.contains(column_name); 167 | 168 | let mut no_checking = false; 169 | // If we have some data of the last row and column that the mouse was on, then try to unselect 170 | if row_contains_column 171 | && self.last_active_row.is_some() 172 | && self.last_active_column.is_some() 173 | { 174 | if let (Some(last_active_column), Some(last_active_row)) = 175 | (self.last_active_column.clone(), self.last_active_row) 176 | { 177 | // Remove the last column selection from the current row where the mouse is if 178 | // the previous row and the current one matches 179 | // 180 | // column column column 181 | // column column column 182 | // column column (mouse is currently here) column(mouse was here) 183 | // 184 | // We unselect the bottom right corner column 185 | if &last_active_column != column_name && last_active_row == id { 186 | current_row.selected_columns.remove(&last_active_column); 187 | self.active_columns.remove(&last_active_column); 188 | } 189 | 190 | // Get the last row where the mouse was 191 | let last_row_index = self 192 | .indexed_ids 193 | .get(&last_active_row) 194 | .expect("Last row not found"); 195 | let last_row = self 196 | .formatted_rows 197 | .get_mut(*last_row_index) 198 | .expect("Last row not found"); 199 | 200 | self.last_active_row = Some(id); 201 | 202 | // If on the same row as the last row, then unselect the column from all other select row 203 | if id == last_row.id { 204 | if &last_active_column != column_name { 205 | self.last_active_column = Some(column_name.clone()); 206 | } 207 | } else { 208 | no_checking = true; 209 | // Mouse went 1 row above or below. So just clear all selection from that previous row 210 | last_row.selected_columns.clear(); 211 | } 212 | } 213 | } else { 214 | // We are in a new row which we have not selected before 215 | self.active_rows.insert(current_row.id); 216 | self.last_active_row = Some(id); 217 | self.last_active_column = Some(column_name.clone()); 218 | current_row 219 | .selected_columns 220 | .clone_from(&self.active_columns); 221 | } 222 | 223 | let current_row_index = self 224 | .indexed_ids 225 | .get(&id) 226 | .expect("Current row index not found") 227 | .to_owned(); 228 | 229 | // Get the row number where the drag started on 230 | let drag_start_index = self 231 | .indexed_ids 232 | .get(&drag_start.0) 233 | .expect("Could not find drag start") 234 | .to_owned(); 235 | 236 | if !no_checking { 237 | // If drag started on row 1, currently on row 5, check from row 4 to 1 and select all columns 238 | // else go through all rows till a row without any selected column is found. Applied both by incrementing or decrementing index. 239 | // In case of fast mouse movement following drag started point mitigates the risk of some rows not getting selected 240 | self.check_row_selection(true, current_row_index, drag_start_index); 241 | self.check_row_selection(false, current_row_index, drag_start_index); 242 | } 243 | self.remove_row_selection(current_row_index, drag_start_index, is_ctrl_pressed); 244 | } 245 | 246 | fn check_row_selection(&mut self, check_previous: bool, index: usize, drag_start: usize) { 247 | if index == 0 && check_previous { 248 | return; 249 | } 250 | 251 | if index + 1 == self.formatted_rows.len() && !check_previous { 252 | return; 253 | } 254 | 255 | let index = if check_previous { index - 1 } else { index + 1 }; 256 | 257 | let current_row = self 258 | .formatted_rows 259 | .get(index) 260 | .expect("Current row not found"); 261 | 262 | // If for example drag started on row 5 and ended on row 10 but missed drag on row 7 263 | // Mark the rows as selected till the drag start row is hit (if recursively going that way) 264 | let unselected_row = if (check_previous && index >= drag_start) 265 | || (!check_previous && index <= drag_start) 266 | { 267 | false 268 | } else { 269 | current_row.selected_columns.is_empty() 270 | }; 271 | 272 | let target_row = self 273 | .formatted_rows 274 | .get_mut(index) 275 | .expect("Target row not found"); 276 | 277 | if !unselected_row { 278 | if self.select_full_row { 279 | target_row.selected_columns.extend(self.all_columns.clone()); 280 | } else { 281 | target_row.selected_columns.clone_from(&self.active_columns); 282 | } 283 | self.active_rows.insert(target_row.id); 284 | 285 | if check_previous { 286 | if index != 0 { 287 | self.check_row_selection(check_previous, index, drag_start); 288 | } 289 | } else if index + 1 != self.formatted_rows.len() { 290 | self.check_row_selection(check_previous, index, drag_start); 291 | } 292 | } 293 | } 294 | 295 | fn remove_row_selection( 296 | &mut self, 297 | current_index: usize, 298 | drag_start: usize, 299 | is_ctrl_pressed: bool, 300 | ) { 301 | let active_ids = self.active_rows.clone(); 302 | for id in active_ids { 303 | let ongoing_index = self 304 | .indexed_ids 305 | .get(&id) 306 | .expect("Could not get ongoing index") 307 | .to_owned(); 308 | let target_row = self 309 | .formatted_rows 310 | .get_mut(ongoing_index) 311 | .expect("target row not found"); 312 | 313 | if current_index > drag_start { 314 | if ongoing_index >= drag_start && ongoing_index <= current_index { 315 | if self.select_full_row { 316 | target_row.selected_columns.extend(self.all_columns.clone()); 317 | } else { 318 | target_row.selected_columns.clone_from(&self.active_columns); 319 | } 320 | } else if !is_ctrl_pressed { 321 | target_row.selected_columns.clear(); 322 | self.active_rows.remove(&target_row.id); 323 | } 324 | } else if ongoing_index <= drag_start && ongoing_index >= current_index { 325 | if self.select_full_row { 326 | target_row.selected_columns.extend(self.all_columns.clone()); 327 | } else { 328 | target_row.selected_columns.clone_from(&self.active_columns); 329 | } 330 | } else if !is_ctrl_pressed { 331 | target_row.selected_columns.clear(); 332 | self.active_rows.remove(&target_row.id); 333 | } 334 | } 335 | } 336 | 337 | /// Unselects all currently selected rows and columns. 338 | /// 339 | /// Clears the selection in both rows and columns, and resets internal tracking of active rows 340 | /// and columns. After this call, there will be no selected rows or columns in the table. 341 | /// 342 | /// # Panics: 343 | /// This method will panic if the indexed ID or the corresponding row cannot be found. 344 | /// 345 | /// # Example: 346 | /// ```rust,ignore 347 | /// table.unselect_all(); // Unselects everything in the table. 348 | /// ``` 349 | pub fn unselect_all(&mut self) { 350 | for id in &self.active_rows { 351 | let id_index = self.indexed_ids.get(id).expect("Could not get id index"); 352 | let target_row = self 353 | .formatted_rows 354 | .get_mut(*id_index) 355 | .expect("Could not get row"); 356 | target_row.selected_columns.clear(); 357 | } 358 | self.active_columns.clear(); 359 | self.last_active_row = None; 360 | self.last_active_column = None; 361 | self.active_rows.clear(); 362 | } 363 | 364 | /// Selects all rows and columns in the table. 365 | /// 366 | /// After calling this method, all rows will have all columns selected. 367 | /// 368 | /// # Example: 369 | /// ```rust,ignore 370 | /// table.select_all(); // Selects all rows and columns. 371 | /// ``` 372 | pub fn select_all(&mut self) { 373 | let mut all_rows = Vec::new(); 374 | 375 | for row in &mut self.formatted_rows { 376 | row.selected_columns.extend(self.all_columns.clone()); 377 | all_rows.push(row.id); 378 | } 379 | 380 | self.active_columns.extend(self.all_columns.clone()); 381 | self.active_rows.extend(all_rows); 382 | self.last_active_row = None; 383 | self.last_active_column = None; 384 | } 385 | 386 | /// Retrieves the currently selected rows. 387 | /// 388 | /// This method returns a vector of the rows that have one or more columns selected. 389 | /// 390 | /// If the `select_full_row` flag is enabled, it will ensure that all columns are selected for 391 | /// each active row. 392 | /// 393 | /// # Returns: 394 | /// A `Vec` of `SelectableRow` instances that are currently selected. 395 | /// 396 | /// # Example: 397 | /// ```rust,ignore 398 | /// let selected_rows = table.get_selected_rows(); 399 | /// ``` 400 | pub fn get_selected_rows(&mut self) -> Vec> { 401 | let mut selected_rows = Vec::new(); 402 | if self.select_full_row { 403 | self.active_columns.extend(self.all_columns.clone()); 404 | } 405 | 406 | // Cannot use active rows to iter as that does not maintain any proper format 407 | for row in &self.formatted_rows { 408 | if row.selected_columns.is_empty() { 409 | continue; 410 | } 411 | selected_rows.push(row.clone()); 412 | 413 | // We already got all the active rows if this matches 414 | if selected_rows.len() == self.active_rows.len() { 415 | break; 416 | } 417 | } 418 | selected_rows 419 | } 420 | 421 | /// Copies selected cells to the system clipboard in a tabular format. 422 | /// 423 | /// This method copies only the selected cells from each row to the clipboard, and ensures 424 | /// that the column widths align for better readability when pasted into a text editor or spreadsheet. 425 | /// 426 | /// # Parameters: 427 | /// - `ui`: The UI context used for clipboard interaction. 428 | /// 429 | /// # Example: 430 | /// ```rust,ignore 431 | /// table.copy_selected_cells(&mut ui); 432 | /// ``` 433 | pub fn copy_selected_cells(&mut self, ui: &mut Ui) { 434 | let mut selected_rows = Vec::new(); 435 | if self.select_full_row { 436 | self.active_columns.extend(self.all_columns.clone()); 437 | } 438 | 439 | let mut column_max_length = HashMap::new(); 440 | 441 | // Iter through all the rows and find the rows that have at least one column as selected. 442 | // Keep track of the biggest length of a value of a column 443 | // active rows cannot be used here because hashset does not maintain an order. 444 | // So itering will give the rows in a different order than what is shown in the ui 445 | for row in &self.formatted_rows { 446 | if row.selected_columns.is_empty() { 447 | continue; 448 | } 449 | 450 | for column in &self.active_columns { 451 | if row.selected_columns.contains(column) { 452 | let column_text = column.column_text(&row.row_data); 453 | let field_length = column_text.len(); 454 | let entry = column_max_length.entry(column).or_insert(0); 455 | if field_length > *entry { 456 | column_max_length.insert(column, field_length); 457 | } 458 | } 459 | } 460 | selected_rows.push(row); 461 | // We already got all the active rows if this matches 462 | if selected_rows.len() == self.active_rows.len() { 463 | break; 464 | } 465 | } 466 | 467 | let mut to_copy = String::new(); 468 | 469 | // Target is to ensure a fixed length after each column value of a row. 470 | // If for example highest len is 10 but the current row's 471 | // column value is 5, we will add the column value and add 5 more space after that 472 | // to ensure alignment 473 | for row in selected_rows { 474 | let mut ongoing_column = self.first_column(); 475 | let mut row_text = String::new(); 476 | loop { 477 | if self.active_columns.contains(&ongoing_column) 478 | && row.selected_columns.contains(&ongoing_column) 479 | { 480 | let column_text = ongoing_column.column_text(&row.row_data); 481 | let _ = write!( 482 | row_text, 483 | "{: Self { 522 | self.select_full_row = true; 523 | self 524 | } 525 | 526 | /// Sets whether the table should select full rows when a column is selected. 527 | /// 528 | /// # Parameters: 529 | /// - `status`: `true` to enable full row selection, `false` to disable it. 530 | /// 531 | /// # Example: 532 | /// ```rust,ignore 533 | /// table.set_select_full_row(true); // Enable full row selection. 534 | /// ``` 535 | pub const fn set_select_full_row(&mut self, status: bool) { 536 | self.select_full_row = status; 537 | } 538 | 539 | /// Returns the total number of currently selected rows. 540 | /// 541 | /// # Returns: 542 | /// - `usize`: The number of selected rows. 543 | /// 544 | /// # Example: 545 | /// ```rust,ignore 546 | /// let selected_count = table.get_total_selected_rows(); 547 | /// println!("{} rows selected", selected_count); 548 | /// ``` 549 | pub fn get_total_selected_rows(&mut self) -> usize { 550 | self.active_rows.len() 551 | } 552 | } 553 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(docsrs, feature(doc_cfg))] 2 | 3 | mod auto_reload; 4 | mod auto_scroll; 5 | mod row_modification; 6 | mod row_selection; 7 | 8 | #[cfg(feature = "fuzzy-matching")] 9 | #[cfg_attr(docsrs, doc(cfg(feature = "fuzzy-matching")))] 10 | mod fuzzy_matcher; 11 | 12 | use auto_reload::AutoReload; 13 | pub use auto_scroll::AutoScroll; 14 | use egui::ahash::{HashMap, HashMapExt, HashSet, HashSetExt}; 15 | use egui::{Event, Key, Label, Response, ScrollArea, Sense, Ui}; 16 | use egui_extras::{Column, TableBuilder, TableRow}; 17 | use std::cmp::Ordering; 18 | use std::hash::Hash; 19 | 20 | #[cfg(feature = "fuzzy-matching")] 21 | use nucleo_matcher::Matcher; 22 | 23 | /// Enum representing the possible sort orders for table columns. 24 | #[derive(Default, Clone, Copy)] 25 | pub enum SortOrder { 26 | /// Sorts in ascending order (e.g., A to Z or 1 to 10). 27 | #[default] 28 | Ascending, 29 | /// Sorts in descending order (e.g., Z to A or 10 to 1). 30 | Descending, 31 | } 32 | 33 | /// Trait for defining how to order rows based on a specific column. 34 | /// 35 | /// This trait should be implemented by users to specify how rows should be 36 | /// compared for sorting purposes. The implementation can vary depending on 37 | /// the type of column. For instance, string comparisons or numeric comparisons 38 | /// can be handled differently depending on the column. Should only be implemented for Ascending 39 | /// ordering, in case of Descending, it is handled internally. 40 | /// 41 | /// # Example 42 | /// Suppose you have a struct `MyRow` with fields like `user_id`, `name`, and `username`. 43 | /// You could implement this trait for each column to specify how rows should be compared. 44 | /// 45 | /// ```rust,ignore 46 | /// impl ColumnOrdering for ColumnName { 47 | /// fn order_by(&self, row_1: &MyRow, row_2: &MyRow) -> Ordering { 48 | /// match self { 49 | /// ColumnName::UserID => row_1.user_id.cmp(&row_2.user_id), 50 | /// ColumnName::Name => row_1.name.cmp(&row_2.name), 51 | /// ColumnName::Username => row_1.username.cmp(&row_2.username), 52 | /// } 53 | /// } 54 | /// } 55 | /// ``` 56 | pub trait ColumnOrdering 57 | where 58 | Row: Clone + Send + Sync, 59 | { 60 | /// Compare two rows and return the ordering result (`Ordering`). 61 | /// 62 | /// This function defines how to order two rows based on the specific column. 63 | /// It returns `Ordering::Less`, `Ordering::Equal`, or `Ordering::Greater` 64 | /// to indicate whether `row_1` should be placed before, after, or at the same 65 | /// position as `row_2` when sorting. Should only be implemented for ascending ordering, in 66 | /// case of Descending, it is handled internally. 67 | /// 68 | /// # Arguments 69 | /// * `row_1` - The first row for comparison. 70 | /// * `row_2` - The second row for comparison. 71 | /// 72 | /// # Returns 73 | /// * `Ordering` - Indicates the relative order between the two rows. 74 | fn order_by(&self, row_1: &Row, row_2: &Row) -> Ordering; 75 | } 76 | 77 | /// Trait for defining column-specific operations in a table UI. 78 | /// 79 | /// This trait allows users to define how each column should behave within a table. 80 | /// This includes how headers should be displayed, how each row in the table should be rendered, 81 | /// and how to extract column-specific text. 82 | /// 83 | /// # Type Parameters: 84 | /// * `Row` - The type representing each row in the table. 85 | /// * `F` - A type that identifies columns, usually an enum or a field type. 86 | /// * `Conf` - Configuration type for the table, useful for passing additional settings. 87 | /// 88 | /// # Requirements: 89 | /// You must implement this trait to specify the behavior of each column within 90 | /// the context of your table UI. 91 | pub trait ColumnOperations 92 | where 93 | Row: Clone + Send + Sync, 94 | F: Eq 95 | + Hash 96 | + Clone 97 | + Ord 98 | + Send 99 | + Sync 100 | + Default 101 | + ColumnOperations 102 | + ColumnOrdering, 103 | Conf: Default, 104 | { 105 | /// Create the header UI for this column. 106 | /// 107 | /// This function is responsible for creating the visual representation of the column header. 108 | /// The `sort_order` argument indicates whether the column is currently used for sorting and, if so, in which 109 | /// direction (ascending or descending). You can customize the header appearance based on 110 | /// this information, for example by adding icons or text. Return `None` for no header. 111 | /// 112 | /// # Arguments 113 | /// * `ui` - A mutable reference to the UI context. 114 | /// * `sort_order` - An optional `SortOrder` representing the current sort state of the column. 115 | /// * `table` - A mutable reference to the `SelectableTable`, allowing you to interact with the table state. 116 | /// 117 | /// # Returns 118 | /// * `Option` - An optional response representing interaction with the UI. 119 | fn create_header( 120 | &self, 121 | ui: &mut Ui, 122 | sort_order: Option, 123 | table: &mut SelectableTable, 124 | ) -> Option; 125 | 126 | /// Create the UI for a specific row in this column. 127 | /// 128 | /// This function is responsible for rendering the content of this column for a given row. 129 | /// It should handle user interactions like clicking or selection as necessary. Mutable table 130 | /// access is provided for modifyiing other rows as necessary. 131 | /// 132 | /// # Arguments 133 | /// * `ui` - A mutable reference to the UI context. 134 | /// * `row` - A reference to the current `SelectableRow` for this table. 135 | /// * `column_selected` - A boolean indicating whether this column is selected. 136 | /// * `table` - A mutable reference to the `SelectableTable` for modifying table data 137 | /// 138 | /// # Returns 139 | /// * `Response` - The result of the UI interaction for this row. 140 | fn create_table_row( 141 | &self, 142 | ui: &mut Ui, 143 | row: &SelectableRow, 144 | column_selected: bool, 145 | table: &mut SelectableTable, 146 | ) -> Response; 147 | 148 | /// Extract the text representation of the column for the given row. 149 | /// 150 | /// This function should return the appropriate text representation of this column 151 | /// for the given row. It can be used to display the data in a simplified form, such 152 | /// as for debugging or plain text rendering. 153 | /// 154 | /// # Arguments 155 | /// * `row` - A reference to the row from which to extract the column text. 156 | /// 157 | /// # Returns 158 | /// * `String` - The text representation of this column for the row. 159 | fn column_text(&self, row: &Row) -> String; 160 | } 161 | 162 | /// Represents a row in a table with selectable columns. 163 | /// 164 | /// This struct is used to store the data of a row along with its unique identifier (`id`) 165 | /// and the set of selected columns for this row. 166 | /// 167 | /// # Type Parameters: 168 | /// * `Row` - The type representing the data stored in each row. 169 | /// * `F` - The type used to identify each column, typically an enum or a type with unique values. 170 | /// 171 | /// # Fields: 172 | /// * `row_data` - The actual data stored in the row. 173 | /// * `id` - A unique identifier for the row. 174 | /// * `selected_columns` - A set of columns that are selected in this row. 175 | #[derive(Clone)] 176 | pub struct SelectableRow 177 | where 178 | Row: Clone + Send + Sync, 179 | F: Eq + Hash + Clone + Ord + Send + Sync + Default, 180 | { 181 | pub row_data: Row, 182 | pub id: i64, 183 | pub selected_columns: HashSet, 184 | } 185 | 186 | /// A table structure that hold data for performing selection on drag, sorting, and displaying rows and more. 187 | /// 188 | /// # Type Parameters 189 | /// * `Row` - The type representing each row in the table. 190 | /// * `F` - A type used to identify columns, often an enum or field type. 191 | /// * `Conf` - Configuration type for additional table settings passed by the user. This is made available anytime when creating or modifying rows 192 | pub struct SelectableTable 193 | where 194 | Row: Clone + Send + Sync, 195 | F: Eq 196 | + Hash 197 | + Clone 198 | + Ord 199 | + Send 200 | + Sync 201 | + Default 202 | + ColumnOperations 203 | + ColumnOrdering, 204 | Conf: Default, 205 | { 206 | /// List of all columns available in the table. 207 | all_columns: Vec, 208 | /// Maps each column to its index in the table for quick lookup. 209 | column_number: HashMap, 210 | /// Stores all rows in the table, keyed by their unique ID. 211 | rows: HashMap>, 212 | /// The current set of formatted rows for display. 213 | formatted_rows: Vec>, 214 | /// The column currently being used to sort the table. 215 | sorted_by: F, 216 | /// The current sort order (ascending or descending). 217 | sort_order: SortOrder, 218 | /// Tracks where a drag operation started in the table, if any. 219 | drag_started_on: Option<(i64, F)>, 220 | /// The columns that have at least 1 row with the column as selected 221 | active_columns: HashSet, 222 | /// The rows that have at least 1 column as selected 223 | active_rows: HashSet, 224 | /// The last row where the pointer was 225 | last_active_row: Option, 226 | /// The last column where the pointer was 227 | last_active_column: Option, 228 | /// Whether the pointer moved from the dragged point at least once 229 | beyond_drag_point: bool, 230 | /// Map of the row IDs to the indices of `formatted_rows` 231 | indexed_ids: HashMap, 232 | /// The last ID that was used for a new row in the table. 233 | last_id_used: i64, 234 | /// Handles auto scroll operation when dragging 235 | auto_scroll: AutoScroll, 236 | /// Handles auto recreating the displayed rows with the latest data 237 | auto_reload: AutoReload, 238 | /// Whether to select the entire row when dragging and selecting instead of a single cell 239 | select_full_row: bool, 240 | /// Whether to add a horizontal scrollbar 241 | horizontal_scroll: bool, 242 | /// Additional Parameters passed by you, available when creating new rows or header. Can 243 | /// contain anything implementing the `Default` trait 244 | pub config: Conf, 245 | /// Whether to add the row serial column to the table 246 | add_serial_column: bool, 247 | /// The row height for the table, defaults to 25.0 248 | row_height: f32, 249 | /// The matcher used for fuzzy searching 250 | #[cfg(feature = "fuzzy-matching")] 251 | matcher: Matcher, 252 | /// Whether to capture Ctrl+A for selecting all rows 253 | no_ctrl_a_capture: bool, 254 | } 255 | 256 | impl SelectableTable 257 | where 258 | Row: Clone + Send + Sync, 259 | F: Eq 260 | + Hash 261 | + Clone 262 | + Ord 263 | + Send 264 | + Sync 265 | + Default 266 | + ColumnOperations 267 | + ColumnOrdering, 268 | Conf: Default, 269 | { 270 | /// Creates a new `SelectableTable` with the provided columns in a specified order. 271 | /// 272 | /// # Parameters: 273 | /// - `columns`: A `Vec` representing the columns. Columns must be passed in the correct order (e.g., 1 to 10). 274 | /// 275 | /// # Returns: 276 | /// - A new instance of `SelectableTable`. 277 | /// 278 | /// # Example: 279 | /// ```rust,ignore 280 | /// let table = SelectableTable::new(vec![col1, col2, col3]); 281 | /// ``` 282 | #[must_use] 283 | pub fn new(columns: Vec) -> Self { 284 | let all_columns = columns.clone(); 285 | let mut column_number = HashMap::new(); 286 | 287 | for (index, col) in columns.into_iter().enumerate() { 288 | column_number.insert(col, index); 289 | } 290 | Self { 291 | all_columns, 292 | column_number, 293 | last_id_used: 0, 294 | rows: HashMap::new(), 295 | formatted_rows: Vec::new(), 296 | sorted_by: F::default(), 297 | sort_order: SortOrder::default(), 298 | drag_started_on: None, 299 | active_columns: HashSet::new(), 300 | active_rows: HashSet::new(), 301 | last_active_row: None, 302 | last_active_column: None, 303 | beyond_drag_point: false, 304 | indexed_ids: HashMap::new(), 305 | auto_scroll: AutoScroll::default(), 306 | auto_reload: AutoReload::default(), 307 | select_full_row: false, 308 | horizontal_scroll: false, 309 | config: Conf::default(), 310 | add_serial_column: false, 311 | row_height: 25.0, 312 | #[cfg(feature = "fuzzy-matching")] 313 | matcher: Matcher::default(), 314 | no_ctrl_a_capture: false, 315 | } 316 | } 317 | 318 | /// Updates the table's configuration with the given `conf`. 319 | /// 320 | /// # Parameters: 321 | /// - `conf`: The new configuration of type `Conf`, which is user-defined and allows 322 | /// passing data to help with row/table modification. 323 | /// 324 | /// # Example: 325 | /// ```rust,ignore 326 | /// table.set_config(my_config); 327 | /// ``` 328 | pub fn set_config(&mut self, conf: Conf) { 329 | self.config = conf; 330 | } 331 | 332 | /// Sets a configuration in a builder-style pattern. 333 | /// 334 | /// # Parameters: 335 | /// - `conf`: A configuration of type `Conf`. The user can pass any data to help with row creation or modification. 336 | /// 337 | /// # Returns: 338 | /// - The updated `SelectableTable` with the new configuration applied. 339 | /// 340 | /// # Example: 341 | /// ```rust,ignore 342 | /// let table = SelectableTable::new(vec![col1, col2, col3]).config(my_config); 343 | /// ``` 344 | #[must_use] 345 | pub fn config(mut self, conf: Conf) -> Self { 346 | self.config = conf; 347 | self 348 | } 349 | 350 | /// Clears all rows from the table, including the displayed ones 351 | /// 352 | /// # Example: 353 | /// ```rust,ignore 354 | /// table.clear_all_rows(); 355 | /// ``` 356 | pub fn clear_all_rows(&mut self) { 357 | self.rows.clear(); 358 | self.formatted_rows.clear(); 359 | self.active_rows.clear(); 360 | self.active_columns.clear(); 361 | self.last_id_used = 0; 362 | } 363 | 364 | /// Displays the UI for the table and uses the provided `TableBuilder` for creating the table UI. 365 | /// 366 | /// # Parameters: 367 | /// - `ui`: The UI context where the table will be rendered. 368 | /// - `table_builder`: A closure that receives and modifies the `TableBuilder`. 369 | /// 370 | /// # Example: 371 | /// ```rust,ignore 372 | /// table.show_ui(ui, |builder| builder.column(column1)); 373 | /// ``` 374 | pub fn show_ui(&mut self, ui: &mut Ui, table_builder: Fn) 375 | where 376 | Fn: FnOnce(TableBuilder) -> TableBuilder, 377 | { 378 | let is_ctrl_pressed = ui.ctx().input(|i| i.modifiers.ctrl); 379 | let key_a_pressed = ui.ctx().input(|i| i.key_pressed(Key::A)); 380 | let copy_initiated = ui.ctx().input(|i| i.events.contains(&Event::Copy)); 381 | let ctx = ui.ctx().clone(); 382 | 383 | if copy_initiated { 384 | self.copy_selected_cells(ui); 385 | } 386 | if is_ctrl_pressed && key_a_pressed && !self.no_ctrl_a_capture { 387 | self.select_all(); 388 | } 389 | 390 | let pointer = ui.input(|i| i.pointer.hover_pos()); 391 | let max_rect = ui.max_rect(); 392 | 393 | if self.horizontal_scroll { 394 | ScrollArea::horizontal().show(ui, |ui| { 395 | let mut table = TableBuilder::new(ui); 396 | 397 | if self.add_serial_column { 398 | table = table.column(Column::initial(25.0).clip(true)); 399 | } 400 | 401 | table = table_builder(table); 402 | 403 | if self.drag_started_on.is_some() { 404 | if let Some(offset) = self.auto_scroll.start_scroll(max_rect, pointer) { 405 | table = table.vertical_scroll_offset(offset); 406 | ctx.request_repaint(); 407 | } 408 | } 409 | 410 | let output = table 411 | .header(20.0, |header| { 412 | self.build_head(header); 413 | }) 414 | .body(|body| { 415 | body.rows(self.row_height, self.formatted_rows.len(), |row| { 416 | let index = row.index(); 417 | self.build_body(row, index); 418 | }); 419 | }); 420 | let scroll_offset = output.state.offset.y; 421 | self.update_scroll_offset(scroll_offset); 422 | }); 423 | } else { 424 | let mut table = TableBuilder::new(ui); 425 | 426 | if self.add_serial_column { 427 | table = table.column(Column::initial(25.0).clip(true)); 428 | } 429 | 430 | table = table_builder(table); 431 | 432 | if self.drag_started_on.is_some() { 433 | if let Some(offset) = self.auto_scroll.start_scroll(max_rect, pointer) { 434 | table = table.vertical_scroll_offset(offset); 435 | ctx.request_repaint(); 436 | } 437 | } 438 | 439 | let output = table 440 | .header(20.0, |header| { 441 | self.build_head(header); 442 | }) 443 | .body(|body| { 444 | body.rows(self.row_height, self.formatted_rows.len(), |row| { 445 | let index = row.index(); 446 | self.build_body(row, index); 447 | }); 448 | }); 449 | let scroll_offset = output.state.offset.y; 450 | self.update_scroll_offset(scroll_offset); 451 | } 452 | } 453 | 454 | fn build_head(&mut self, mut header: TableRow) { 455 | if self.add_serial_column { 456 | header.col(|ui| { 457 | ui.add_sized(ui.available_size(), Label::new("")); 458 | }); 459 | } 460 | for column_name in &self.all_columns.clone() { 461 | header.col(|ui| { 462 | let sort_order = if &self.sorted_by == column_name { 463 | Some(self.sort_order) 464 | } else { 465 | None 466 | }; 467 | 468 | let Some(resp) = column_name.create_header(ui, sort_order, self) else { 469 | return; 470 | }; 471 | 472 | // Response click sense is not forced. So if a header should not be used 473 | // for sorting, without click there won't be any actions. 474 | 475 | if resp.clicked() { 476 | let is_selected = &self.sorted_by == column_name; 477 | if is_selected { 478 | self.change_sort_order(); 479 | } else { 480 | self.change_sorted_by(column_name); 481 | } 482 | self.recreate_rows(); 483 | } 484 | }); 485 | } 486 | } 487 | 488 | fn build_body(&mut self, mut row: TableRow, index: usize) { 489 | let row_data = self.formatted_rows[index].clone(); 490 | 491 | if self.add_serial_column { 492 | row.col(|ui| { 493 | ui.add_sized(ui.available_size(), Label::new(format!("{}", index + 1))); 494 | }); 495 | } 496 | self.handle_table_body(row, &row_data); 497 | } 498 | 499 | /// Change the current sort order from ascending to descending and vice versa. Will unselect 500 | /// all selected rows 501 | fn change_sort_order(&mut self) { 502 | self.unselect_all(); 503 | if matches!(self.sort_order, SortOrder::Ascending) { 504 | self.sort_order = SortOrder::Descending; 505 | } else { 506 | self.sort_order = SortOrder::Ascending; 507 | } 508 | } 509 | 510 | /// Change the column that is currently being used for sorting. Will unselect all rows 511 | fn change_sorted_by(&mut self, sort_by: &F) { 512 | self.unselect_all(); 513 | self.sorted_by = sort_by.clone(); 514 | self.sort_order = SortOrder::default(); 515 | } 516 | 517 | /// Recreates the rows shown in the UI for the next frame load. 518 | /// 519 | /// # Important: 520 | /// - Any direct modifications made using [`modify_shown_row`](#method.modify_shown_row) 521 | /// will be **cleared** when this is called. 522 | /// To preserve changes, use [`add_modify_row`](#method.add_modify_row) to update row data instead. 523 | /// 524 | /// # Performance: 525 | /// - Should be used sparingly for large datasets, as frequent calls can lead to performance issues. 526 | /// - Consider calling after every X number of row updates, depending on update frequency, 527 | /// or use [`auto_reload`](#method.auto_reload) for automatic reload. 528 | /// 529 | /// # Example: 530 | /// ```rust,ignore 531 | /// table.recreate_rows(); 532 | /// ``` 533 | pub fn recreate_rows(&mut self) { 534 | self.formatted_rows.clear(); 535 | self.active_rows.clear(); 536 | self.active_columns.clear(); 537 | self.sort_rows(); 538 | } 539 | 540 | /// Recreates the rows shown in the UI for the next frame load. 541 | /// 542 | /// This function refreshes the internal row state by clearing and re-sorting the rows 543 | /// similar to [`recreate_rows`](#method.recreate_rows), but it **preserves** the currently 544 | /// selected rows and re-applies the active column selection to them. 545 | /// 546 | /// Useful when the UI needs to be refreshed without resetting user interaction state. 547 | /// 548 | /// # Important: 549 | /// - Any direct modifications made to `formatted_rows` using [`modify_shown_row`](#method.modify_shown_row) 550 | /// will be **cleared** when this is called. 551 | /// To preserve changes, use [`add_modify_row`](#method.add_modify_row) to update row data instead. 552 | /// 553 | /// # Performance: 554 | /// - Should be used sparingly for large datasets, as frequent calls can lead to performance issues. 555 | /// - Consider calling after every X number of row updates, depending on update frequency, 556 | /// or use [`auto_reload`](#method.auto_reload) for automatic reload. 557 | /// 558 | /// # Example: 559 | /// ```rust,ignore 560 | /// table.recreate_rows_no_unselect(); 561 | /// ``` 562 | pub fn recreate_rows_no_unselect(&mut self) { 563 | self.formatted_rows.clear(); 564 | self.sort_rows(); 565 | 566 | for row in &self.active_rows { 567 | let Some(target_index) = self.indexed_ids.get(row) else { 568 | continue; 569 | }; 570 | self.formatted_rows[*target_index] 571 | .selected_columns 572 | .clone_from(&self.active_columns); 573 | } 574 | } 575 | 576 | /// The first column that was passed by the user 577 | fn first_column(&self) -> F { 578 | self.all_columns[0].clone() 579 | } 580 | 581 | /// The last column that was passed by the user 582 | fn last_column(&self) -> F { 583 | self.all_columns[self.all_columns.len() - 1].clone() 584 | } 585 | 586 | /// Convert a number to a column value 587 | fn column_to_num(&self, column: &F) -> usize { 588 | *self 589 | .column_number 590 | .get(column) 591 | .expect("Not in the column list") 592 | } 593 | 594 | /// Get the next column of the provided column 595 | fn next_column(&self, column: &F) -> F { 596 | let current_column_num = self.column_to_num(column); 597 | if current_column_num == self.all_columns.len() - 1 { 598 | self.all_columns[0].clone() 599 | } else { 600 | self.all_columns[current_column_num + 1].clone() 601 | } 602 | } 603 | 604 | /// Get the previous column of the provided column 605 | fn previous_column(&self, column: &F) -> F { 606 | let current_column_num = self.column_to_num(column); 607 | if current_column_num == 0 { 608 | self.all_columns[self.all_columns.len() - 1].clone() 609 | } else { 610 | self.all_columns[current_column_num - 1].clone() 611 | } 612 | } 613 | 614 | /// Builds the table's Body section 615 | fn handle_table_body(&mut self, mut row: TableRow, row_data: &SelectableRow) { 616 | for column_name in &self.all_columns.clone() { 617 | row.col(|ui| { 618 | let selected = row_data.selected_columns.contains(column_name); 619 | let mut resp = column_name.create_table_row(ui, row_data, selected, self); 620 | 621 | // Drag sense is forced otherwise there is no point of this library. 622 | resp = resp.interact(Sense::drag()); 623 | 624 | if resp.drag_started() { 625 | // If CTRL is not pressed down and the mouse right click is not pressed, unselect all cells 626 | // Right click for context menu 627 | if !ui.ctx().input(|i| i.modifiers.ctrl) 628 | && !ui.ctx().input(|i| i.pointer.secondary_clicked()) 629 | { 630 | self.unselect_all(); 631 | } 632 | self.drag_started_on = Some((row_data.id, column_name.clone())); 633 | } 634 | 635 | let pointer_released = ui.input(|a| a.pointer.primary_released()); 636 | 637 | if pointer_released { 638 | self.last_active_row = None; 639 | self.last_active_column = None; 640 | self.drag_started_on = None; 641 | self.beyond_drag_point = false; 642 | } 643 | 644 | if resp.clicked() { 645 | // If CTRL is not pressed down and the mouse right click is not pressed, unselect all cells 646 | if !ui.ctx().input(|i| i.modifiers.ctrl) 647 | && !ui.ctx().input(|i| i.pointer.secondary_clicked()) 648 | { 649 | self.unselect_all(); 650 | } 651 | self.select_single_row_cell(row_data.id, column_name); 652 | } 653 | 654 | if ui.ui_contains_pointer() && self.drag_started_on.is_some() { 655 | if let Some(drag_start) = self.drag_started_on.as_ref() { 656 | // Only call drag either when not on the starting drag row/column or went beyond the 657 | // drag point at least once. Otherwise normal click would be considered as drag 658 | if drag_start.0 != row_data.id 659 | || &drag_start.1 != column_name 660 | || self.beyond_drag_point 661 | { 662 | let is_ctrl_pressed = ui.ctx().input(|i| i.modifiers.ctrl); 663 | self.select_dragged_row_cell(row_data.id, column_name, is_ctrl_pressed); 664 | } 665 | } 666 | } 667 | }); 668 | } 669 | } 670 | 671 | /// Returns the total number of rows currently being displayed in the UI. 672 | /// 673 | /// # Returns: 674 | /// - `usize`: The number of rows that are formatted and ready for display. 675 | pub const fn total_displayed_rows(&self) -> usize { 676 | self.formatted_rows.len() 677 | } 678 | 679 | /// Returns the total number of rows in the table (both displayed and non-displayed). 680 | /// 681 | /// # Returns: 682 | /// - `usize`: The total number of rows stored in the table, regardless of whether they are being displayed or not. 683 | pub fn total_rows(&self) -> usize { 684 | self.rows.len() 685 | } 686 | 687 | /// Provides a reference to the rows currently being displayed in the UI. 688 | /// 689 | /// # Returns: 690 | /// - `&Vec>`: A reference to the vector of formatted rows ready for display. 691 | pub const fn get_displayed_rows(&self) -> &Vec> { 692 | &self.formatted_rows 693 | } 694 | 695 | /// Provides a reference to all rows in the table, regardless of whether they are displayed. 696 | /// 697 | /// # Returns: 698 | /// - `&HashMap>`: A reference to the entire collection of rows in the table. 699 | pub const fn get_all_rows(&self) -> &HashMap> { 700 | &self.rows 701 | } 702 | 703 | /// Adds a serial column to the table. 704 | /// 705 | /// The serial column is automatically generated and displayed at the very left of the table. 706 | /// It shows the row number (starting from 1) for each row. 707 | /// 708 | /// # Returns: 709 | /// - `Self`: The modified table with the serial column enabled. 710 | /// 711 | /// # Example: 712 | /// ```rust,ignore 713 | /// let table = SelectableTable::new(vec![col1, col2, col3]) 714 | /// .config(my_config).serial_column(); 715 | /// ``` 716 | #[must_use] 717 | pub const fn serial_column(mut self) -> Self { 718 | self.add_serial_column = true; 719 | self 720 | } 721 | 722 | /// Add a horizontal scrollbar to the table 723 | /// 724 | /// # Returns: 725 | /// - `Self`: The modified table with the serial column enabled. 726 | /// 727 | /// # Example: 728 | /// ```rust,ignore 729 | /// let table = SelectableTable::new(vec![col1, col2, col3]) 730 | /// .horizontal_scroll(); 731 | /// ``` 732 | #[must_use] 733 | pub const fn horizontal_scroll(mut self) -> Self { 734 | self.horizontal_scroll = true; 735 | self 736 | } 737 | 738 | /// Sets the height rows in the table. 739 | /// 740 | /// # Parameters: 741 | /// - `height`: The desired height for each row in logical points. 742 | /// 743 | /// # Returns: 744 | /// - `Self`: The modified table with the specified row height applied. 745 | /// 746 | /// # Example: 747 | /// ```rust,ignore 748 | /// let table = SelectableTable::new(vec![col1, col2, col3]) 749 | /// .row_height(24.0); 750 | /// ``` 751 | #[must_use] 752 | pub const fn row_height(mut self, height: f32) -> Self { 753 | self.row_height = height; 754 | self 755 | } 756 | 757 | /// Disables Ctrl+A keyboard shortcut capturing for selecting all rows 758 | /// 759 | /// # Returns: 760 | /// - `Self`: The modified table with Ctrl+A capturing disabled. 761 | /// 762 | /// # Example: 763 | /// ```rust,ignore 764 | /// let table = SelectableTable::new(columns) 765 | /// .no_ctrl_a_capture(); 766 | /// ``` 767 | #[must_use] 768 | pub const fn no_ctrl_a_capture(mut self) -> Self { 769 | self.no_ctrl_a_capture = true; 770 | self 771 | } 772 | 773 | /// Enables or disables Ctrl+A keyboard shortcut capturing dynamically for selecting all rows. 774 | /// 775 | /// # Parameters: 776 | /// - `status`: `true` to disable Ctrl+A capture, `false` to enable it. 777 | /// 778 | /// # Example: 779 | /// ```rust,ignore 780 | /// table.set_no_ctrl_a_capture(true); // Disable Ctrl+A capture 781 | /// ``` 782 | pub const fn set_no_ctrl_a_capture(&mut self, status: bool) { 783 | self.no_ctrl_a_capture = status; 784 | } 785 | } 786 | --------------------------------------------------------------------------------