├── .vscode ├── settings.json └── tasks.json ├── .gitignore ├── .github └── workflows │ ├── cargo-publish.yml │ └── pages.yml ├── demo ├── Cargo.toml └── index.html ├── Cargo.toml ├── LICENSE ├── README.md ├── src ├── lib.rs ├── draw │ ├── tsv.rs │ └── state.rs ├── viewer.rs └── draw.rs ├── CHANGELOG.md └── examples ├── partially_editable.rs ├── internationalization.rs └── demo.rs /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /demo/dist 3 | 4 | .cargo 5 | 6 | .vscode/launch.json 7 | Cargo.lock -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Build.Demo", 6 | "type": "shell", 7 | "group": "build", 8 | "command": "cargo build --example demo", 9 | "problemMatcher": "$rustc" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /.github/workflows/cargo-publish.yml: -------------------------------------------------------------------------------- 1 | name: Cargo Publish 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | env: 7 | CARGO_TERM_COLOR: always 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: Swatinem/rust-cache@v2 15 | - run: cargo build 16 | - run: cargo test 17 | - run: cargo login ${{ secrets.CRATES_IO_TOKEN }} 18 | - run: cargo publish 19 | 20 | -------------------------------------------------------------------------------- /demo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "demo" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [[bin]] 7 | name = "demo" 8 | path = "../examples/demo.rs" 9 | 10 | [dependencies] 11 | egui = "0.33" 12 | egui_extras = { version = "0.33", default-features = false, features = [ 13 | "serde", 14 | ] } 15 | eframe = { version = "0.33", features = ["serde", "persistence"] } 16 | 17 | egui-data-table = { path = ".." } 18 | env_logger = "0.11.5" 19 | 20 | fastrand = "2" 21 | names = { version = "0.14", default-features = false } 22 | getrandom = { version = "0.2", features = ["js"] } 23 | 24 | tap = "1" 25 | log = "0.4" 26 | 27 | [target.'cfg(target_arch = "wasm32")'.dependencies] 28 | wasm-bindgen-futures = "0.4" 29 | web-sys = "0.3" 30 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "egui-data-table" 3 | version = "0.9.0" 4 | edition = "2024" 5 | repository = "https://github.com/kang-sw/egui-data-table" 6 | authors = ["kang-sw"] 7 | description = "A generic data table widget implmentation for egui" 8 | categories = ["gui"] 9 | license-file = "LICENSE" 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | [workspace] 13 | members = ["demo"] 14 | 15 | [dependencies] 16 | egui = "0.33" 17 | egui_extras = { version = "0.33", default-features = false, features = [ 18 | "serde", 19 | ] } 20 | tap = "1" 21 | itertools = "0.14" 22 | serde = { version = "1", optional = true, features = ["derive"] } 23 | thiserror = "2" 24 | 25 | [dev-dependencies] 26 | eframe = { version = "0.33", features = ["serde", "persistence"] } 27 | fastrand = "2" 28 | names = { version = "0.14", default-features = false } 29 | log = "0.4" 30 | env_logger = "0.11.5" 31 | 32 | [features] 33 | default = ["persistency"] 34 | persistency = ["dep:serde"] 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Seungwoo Kang 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: Build and Deploy Demo Web App 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - 'demo/**' 9 | - 'examples/**' 10 | - 'src/**' 11 | workflow_dispatch: 12 | 13 | env: 14 | CARGO_TERM_COLOR: always 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: Swatinem/rust-cache@v2 22 | - run: rustup target add wasm32-unknown-unknown 23 | - run: cargo install trunk@^0.20 24 | - run: trunk build --release --verbose --public-url /egui-data-table ./demo/index.html 25 | - name: Upload artifact 26 | uses: actions/upload-pages-artifact@v3 27 | with: 28 | path: "./demo/dist" 29 | 30 | deploy: 31 | needs: build 32 | 33 | permissions: 34 | pages: write 35 | id-token: write 36 | 37 | environment: 38 | name: github-pages 39 | url: ${{ steps.deployment.outputs.page_url }} 40 | 41 | runs-on: ubuntu-latest 42 | 43 | steps: 44 | - name: Deploy to Github Pages 45 | id: deployment 46 | uses: actions/deploy-pages@v4 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Latest version](https://img.shields.io/crates/v/egui-data-table.svg)](https://crates.io/crates/egui-data-table) 2 | [![Documentation](https://docs.rs/egui-data-table/badge.svg)](https://docs.rs/egui-data-table) 3 | 4 | # Data table UI implementation for egui 5 | 6 | MSRV is 1.75, with RPITIT 7 | 8 | [Demo Web Page](https://kang-sw.github.io/egui-data-table/) 9 | 10 | # Features 11 | 12 | - [x] Undo/Redo for every editions 13 | - [x] Show/Hide/Reorder columns 14 | - [x] Row duplication / removal 15 | - [x] Keyboard navigation 16 | - [x] Internal clipboard support 17 | - [x] System clipboard support 18 | - [ ] Tutorials documentation 19 | - [ ] Tidy light mode visuals 20 | 21 | # Usage 22 | 23 | In `Cargo.toml`, add `egui-data-table` to your dependencies section 24 | 25 | ```toml 26 | [dependencies] 27 | egui-data-table = "0.1" 28 | ``` 29 | 30 | Minimal example: 31 | 32 | ```rust no_run 33 | // Use same version of `egui` with this crate! 34 | use egui_data_table::egui; 35 | 36 | // Don't need to implement any trait on row data itself. 37 | struct MyRowData(i32, String, bool); 38 | 39 | // Every logic is defined in `Viewer` 40 | struct MyRowViewer; 41 | 42 | // There are several methods that MUST be implemented to make the viewer work correctly. 43 | impl egui_data_table::RowViewer for MyRowViewer { 44 | fn num_columns(&mut self) -> usize { 45 | 3 46 | } 47 | 48 | fn show_cell_view(&mut self, ui: &mut egui::Ui, row: &MyRowData, column: usize) { 49 | let _ = match column { 50 | 0 => ui.label(format!("{}", row.0)), 51 | 1 => ui.label(&row.1), 52 | 2 => ui.checkbox(&mut { row.2 }, ""), 53 | _ => unreachable!() 54 | }; 55 | } 56 | 57 | fn show_cell_editor( 58 | &mut self, 59 | ui: &mut egui::Ui, 60 | row: &mut MyRowData, 61 | column: usize, 62 | ) -> Option { 63 | match column { 64 | 0 => ui.add(egui::DragValue::new(&mut row.0).speed(1.0)), 65 | 1 => { 66 | egui::TextEdit::multiline(&mut row.1) 67 | .desired_rows(1) 68 | .code_editor() 69 | .show(ui) 70 | .response 71 | } 72 | 2 => ui.checkbox(&mut row.2, ""), 73 | _ => unreachable!() 74 | } 75 | .into() // To make focusing work correctly, valid response must be returned. 76 | } 77 | 78 | fn set_cell_value(&mut self, src: &MyRowData, dst: &mut MyRowData, column: usize) { 79 | match column { 80 | 0 => dst.0 = src.0, 81 | 1 => dst.1 = src.1.clone(), 82 | 2 => dst.2 = src.2, 83 | _ => unreachable!() 84 | } 85 | } 86 | 87 | fn new_empty_row(&mut self) -> MyRowData { 88 | // Instead of requiring `Default` trait for row data types, the viewer is 89 | // responsible of providing default creation method. 90 | MyRowData(0, Default::default(), false) 91 | } 92 | 93 | // fn clone_row(&mut self, src: &MyRowData) -> MyRowData 94 | // ^^ Overriding this method is optional. In default, it'll utilize `set_cell_value` which 95 | // would be less performant during huge duplication of lines. 96 | } 97 | 98 | fn show(ui: &mut egui::Ui, table: &mut egui_data_table::DataTable) { 99 | ui.add(egui_data_table::Renderer::new( 100 | table, 101 | &mut { MyRowViewer }, 102 | )); 103 | } 104 | ``` 105 | 106 | For more details / advanced usage, see [demo](./examples/demo.rs) 107 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | eframe template 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 |
115 |

116 | Loading… 117 |

118 |
119 |
120 | 121 | 122 | 123 | 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | 3 | pub mod draw; 4 | pub mod viewer; 5 | 6 | pub use draw::{Renderer, Style}; 7 | pub use viewer::{RowViewer, UiAction}; 8 | 9 | /// You may want to sync egui version with this crate. 10 | pub extern crate egui; 11 | 12 | /* ---------------------------------------------------------------------------------------------- */ 13 | /* CORE CLASS */ 14 | /* ---------------------------------------------------------------------------------------------- */ 15 | 16 | /// Prevents direct modification of `Vec` 17 | pub struct DataTable { 18 | /// Efficient row data storage 19 | /// 20 | /// XXX: If we use `VecDeque` here, it'd be more efficient when inserting new element 21 | /// at the beginning of the list. However, it does not support `splice` method like 22 | /// `Vec`, which results in extremely inefficient when there's multiple insertions. 23 | /// 24 | /// The efficiency order of general operations are only twice as slow when using 25 | /// `Vec`, we're just ignoring it for now. Maybe we can utilize `IndexMap` for this 26 | /// purpose, however, there are many trade-offs to consider, for now, we're just 27 | /// using `Vec` for simplicity. 28 | rows: Vec, 29 | 30 | dirty_flag: bool, 31 | 32 | /// Ui 33 | ui: Option>>, 34 | } 35 | 36 | impl std::fmt::Debug for DataTable { 37 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 38 | f.debug_struct("Spreadsheet") 39 | .field("rows", &self.rows) 40 | .finish() 41 | } 42 | } 43 | 44 | impl Default for DataTable { 45 | fn default() -> Self { 46 | Self { 47 | rows: Default::default(), 48 | ui: Default::default(), 49 | dirty_flag: false, 50 | } 51 | } 52 | } 53 | 54 | impl FromIterator for DataTable { 55 | fn from_iter>(iter: T) -> Self { 56 | Self { 57 | rows: iter.into_iter().collect(), 58 | ..Default::default() 59 | } 60 | } 61 | } 62 | 63 | impl DataTable { 64 | pub fn new() -> Self { 65 | Default::default() 66 | } 67 | 68 | pub fn take(&mut self) -> Vec { 69 | self.mark_dirty(); 70 | std::mem::take(&mut self.rows) 71 | } 72 | 73 | /// Replace the current data with the new one. 74 | pub fn replace(&mut self, new: Vec) -> Vec { 75 | self.mark_dirty(); 76 | std::mem::replace(&mut self.rows, new) 77 | } 78 | 79 | /// Insert a row at the specified index. This is thin wrapper of `Vec::retain` which provides 80 | /// additional dirty flag optimization. 81 | pub fn retain(&mut self, mut f: impl FnMut(&R) -> bool) { 82 | let mut removed_any = false; 83 | self.rows.retain(|row| { 84 | let retain = f(row); 85 | removed_any |= !retain; 86 | retain 87 | }); 88 | 89 | if removed_any { 90 | self.mark_dirty(); 91 | } 92 | } 93 | 94 | /// Check if the UI is obsolete and needs to be re-rendered due to data changes. 95 | pub fn is_dirty(&self) -> bool { 96 | self.ui.as_ref().is_some_and(|ui| ui.cc_is_dirty()) 97 | } 98 | 99 | #[deprecated( 100 | since = "0.5.1", 101 | note = "user-driven dirty flag clearance is redundant" 102 | )] 103 | pub fn clear_dirty_flag(&mut self) { 104 | // This is intentionally became a no-op 105 | } 106 | 107 | fn mark_dirty(&mut self) { 108 | let Some(state) = self.ui.as_mut() else { 109 | return; 110 | }; 111 | 112 | state.force_mark_dirty(); 113 | } 114 | 115 | /// Returns true if there were any user-driven(triggered by UI) modifications. 116 | pub fn has_user_modification(&self) -> bool { 117 | self.dirty_flag 118 | } 119 | 120 | /// Clears the user-driven(triggered by UI) modification flag. 121 | pub fn clear_user_modification_flag(&mut self) { 122 | self.dirty_flag = false; 123 | } 124 | } 125 | 126 | impl Extend for DataTable { 127 | /// Programmatic extend operation will invalidate the index table cache. 128 | fn extend>(&mut self, iter: T) { 129 | // Invalidate the cache 130 | self.ui = None; 131 | self.rows.extend(iter); 132 | } 133 | } 134 | 135 | fn default() -> T { 136 | T::default() 137 | } 138 | 139 | impl std::ops::Deref for DataTable { 140 | type Target = Vec; 141 | 142 | fn deref(&self) -> &Self::Target { 143 | &self.rows 144 | } 145 | } 146 | 147 | impl std::ops::DerefMut for DataTable { 148 | fn deref_mut(&mut self) -> &mut Self::Target { 149 | self.mark_dirty(); 150 | &mut self.rows 151 | } 152 | } 153 | 154 | impl Clone for DataTable { 155 | fn clone(&self) -> Self { 156 | Self { 157 | rows: self.rows.clone(), 158 | // UI field is treated as cache. 159 | ui: None, 160 | dirty_flag: self.dirty_flag, 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog], 6 | and this project adheres to [Semantic Versioning]. 7 | 8 | ## [Unreleased] 9 | 10 | ### Fixed 11 | 12 | - Missing call to `on_highlight_cell`. It was added to the API in 0.6.2 but was never called. 13 | 14 | ## [0.7.0] 15 | 16 | ### Added 17 | - Translator/i18n support with a new `internationalization.rs` example. 18 | - New demo features: 19 | - "Has modifications" checkbox and button to clear the state. 20 | - Column headers in the `partially_editable` example. 21 | - Module-level documentation for the `partially_editable` example. 22 | - Expanded `is_editable_cell` API to allow row-based cell editability checks, demonstrated with a "locked" column in the demo. 23 | 24 | ### Changed 25 | - Improved `on_row_updated` API to pass old and new row values for change detection. 26 | - Moved sort indicator to the right for better readability and alignment. 27 | - Improved partial editing support. 28 | - Ensured consistent sorting of values in the "processes" column. 29 | 30 | ### Fixed 31 | - Prevented drag-and-drop onto non-editable cells. 32 | - Fixed placeholder width for the sort indicator to handle multi-character indicators. 33 | - Fixed a typo in the "Row protection" checkbox in the demo. 34 | 35 | ### Exposed 36 | - Auto-shrink settings and scroll bar visibility in the API and demo. 37 | 38 | ### Prevented 39 | - New row insertion when the table is empty and row insertion is disabled. 40 | 41 | ## [0.6.2] 42 | 43 | ### Added 44 | - New viewer API `Viewer::is_editable_cell`, `Viewer::allow_row_insertions`, `Viewer::allow_row_deletions` 45 | 46 | ### Fixed 47 | - Now editing cell correctly lose its focus when clicking outside of the table. 48 | - Now default view of editing cell is correctly hidden. 49 | 50 | 51 | ## [0.6.0] 52 | 53 | ### Changed 54 | 55 | - **BREAKING** Refactor `Viewer::column_render_config` to take additional parameter. 56 | 57 | ## [0.5.1] 58 | 59 | ### Added 60 | 61 | - Implement `Clone`, `Deref`, `DerefMut` for `DataTable` widget. 62 | - Implement `Serialize`, `Deserialize` for `DataTable` widget. 63 | 64 | ### Changed 65 | 66 | - Manual dirty flag clearing now deprecated. 67 | 68 | ### Fixed 69 | 70 | 71 | ## [0.5.0] 72 | 73 | ### Added 74 | 75 | - New style flag to control editor behavior 76 | - `Style::single_click_edit_mode`: Make single click available to start edit mode. 77 | 78 | ### Removed 79 | 80 | - `viewer::TrivialConfig` was removed. 81 | - Configs are integrated inside the `Style` of renderer. 82 | 83 | ## [0.4.1] - 2024-12-14 84 | 85 | ### Added 86 | 87 | - Introduce `crate::Style` struct, which defines set of properties to control internal 88 | behavior & visuals 89 | 90 | ### Changed 91 | 92 | - Change various default visuals. 93 | 94 | ## [0.4.0] - 2024-11-21 95 | 96 | ### Changed 97 | 98 | - Bump upstream dependency `egui` version 0.29 99 | 100 | ### Fixed 101 | 102 | - Fix incorrect drag and drop area calculation logic 103 | 104 | ## [0.3.1] - 2024-08-18 105 | 106 | ### Added 107 | 108 | - System clipboard support 109 | - New trait item: `Codec` 110 | 111 | ## [0.3.0] - 2024-07-04 112 | 113 | ### Changed 114 | 115 | - Upgraded EGUI dependency version to 0.28 116 | - Remove function RPITIT in table viewer trait. 117 | 118 | ## [0.2.2] - 2024-05-11 119 | 120 | ### Added 121 | 122 | - New `Cargo.toml` feature `persistency` 123 | - New API: `Viewer::persist_ui_state` 124 | - To persist UI state over sessions, return `true` on this trait method. 125 | 126 | ## [0.1.4] - 2024-04-07 127 | 128 | ### Added 129 | 130 | - New API: `RowViewer::clone_row_as_copied_base` 131 | - Replaces call to plain `clone_row` when it's triggered by user to copy contents of given row. 132 | 133 | ### Changed 134 | 135 | - **BREAKING** 136 | - `viewer::UiAction` is now `#[non_exhaustive]` 137 | - New enum variant `UiAction::InsertEmptyRows(NonZeroUsize)`, an action for inserting number of empty rows. 138 | - Dependencies 139 | - egui 0.26 -> 0.27 140 | 141 | ## [0.1.3] - 2024-03-25 142 | 143 | ### Fixed 144 | 145 | - Panic on row removal due to invalid index access 146 | 147 | ## [0.1.2] - 2024-03-09 148 | 149 | Add more controls for viewer. 150 | 151 | ### Added 152 | 153 | - New `RowViewer` APIs for detailed control of user interaction. 154 | - `RowViewer::confirm_cell_write` 155 | - New enum `viewer::CellWriteContext` 156 | - `RowViewer::confirm_row_deletion` 157 | - `RowViewer::clone_row_for_insertion` 158 | - `RowViewer::on_highlight_cell` 159 | - `RowViewer::new_empty_row_for` 160 | - New enum `viewer::EmptyRowCreateContext` 161 | 162 | ### Changed 163 | 164 | - Insert `cargo-semver-checks` on Cargo Publish task. 165 | 166 | ## [0.1.1] - 2024-03-07 167 | 168 | ### Added 169 | 170 | - Initial implementation with features 171 | - [x] Undo/Redo for every editions 172 | - [x] Show/Hide/Reorder columns 173 | - [x] Row duplication / removal 174 | - [x] Keyboard navigation 175 | - [x] Internal clipboard support 176 | 177 | ## [Wishlist] 178 | 179 | - [ ] System clipboard support 180 | - [ ] Tutorials documentation 181 | - [ ] Tidy light mode visuals 182 | 183 | 184 | [keep a changelog]: https://keepachangelog.com/en/1.0.0/ 185 | [semantic versioning]: https://semver.org/spec/v2.0.0.html 186 | -------------------------------------------------------------------------------- /src/draw/tsv.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | //! A short implementation for reading and writing TSV data. 3 | 4 | use std::ops::Range; 5 | 6 | pub fn write_tab(buf: &mut String) { 7 | buf.push('\t'); 8 | } 9 | 10 | pub fn write_newline(buf: &mut String) { 11 | buf.push('\n'); 12 | } 13 | 14 | pub fn write_content(buf: &mut String, mut item: &str) { 15 | if item.is_empty() { 16 | item = " "; 17 | } 18 | 19 | buf.reserve(item.len()); 20 | 21 | for char in item.chars() { 22 | match char { 23 | '\t' => buf.push_str(r"\t"), 24 | '\n' => buf.push_str(r"\n"), 25 | '\r' => buf.push_str(r"\r"), 26 | '\\' => buf.push_str(r"\\"), 27 | _ => buf.push(char), 28 | } 29 | } 30 | } 31 | 32 | /* ============================================================================================== */ 33 | /* READER */ 34 | /* ============================================================================================== */ 35 | 36 | pub struct ParsedTsv { 37 | /// We need owned buffer to store escaped TSV data. 38 | data: String, 39 | 40 | /// Byte span info for each cell in the TSV data. As long as the cell is explicitly allocated 41 | /// using tab character, the cell will be stored in this vector even if it is empty. 42 | cell_spans: Vec>, 43 | 44 | /// Index offsets for start of each row in the `cell_spans` vector. 45 | row_offsets: Vec, 46 | } 47 | 48 | impl ParsedTsv { 49 | pub fn parse(data: &str) -> Self { 50 | #[derive(Clone, Copy)] 51 | enum ParseState { 52 | Empty, 53 | Escaping, 54 | } 55 | 56 | let mut s = Self { 57 | data: Default::default(), 58 | cell_spans: Default::default(), 59 | row_offsets: Default::default(), 60 | }; 61 | 62 | let mut state = ParseState::Empty; 63 | let mut cell_start_char = 0; 64 | 65 | // Add initial row offset. 66 | s.row_offsets.push(0); 67 | 68 | for char in data.chars() { 69 | match state { 70 | ParseState::Empty => match char { 71 | '\n' | '\t' => { 72 | if char == '\t' || cell_start_char != s.data.len() as u32 { 73 | // For tab character, we don't care if it's empty cell. Otherwise, 74 | // we add the last cell only when it's not empty. 75 | s.cell_spans.push(cell_start_char..s.data.len() as u32); 76 | cell_start_char = s.data.len() as _; 77 | } 78 | 79 | if char == '\n' { 80 | // Add row offset and move to new row. 81 | s.row_offsets.push(s.cell_spans.len() as _); 82 | } 83 | } 84 | '\r' => { 85 | // Ignoring. 86 | } 87 | '\\' => state = ParseState::Escaping, 88 | ch => s.data.push(ch), 89 | }, 90 | ParseState::Escaping => { 91 | match char { 92 | 't' => s.data.push('\t'), 93 | 'n' => s.data.push('\n'), 94 | 'r' => s.data.push('\r'), 95 | '\\' => s.data.push('\\'), 96 | ch => { 97 | // Just add the character as it is. 98 | s.data.push('\\'); 99 | s.data.push(ch); 100 | } 101 | } 102 | 103 | state = ParseState::Empty; 104 | } 105 | } 106 | } 107 | 108 | // Need to check if we have any remaining cell to add. 109 | { 110 | if cell_start_char != s.data.len() as u32 { 111 | s.cell_spans.push(cell_start_char..s.data.len() as u32); 112 | } 113 | 114 | if *s.row_offsets.last().unwrap() != s.cell_spans.len() as u32 { 115 | s.row_offsets.push(s.cell_spans.len() as _); 116 | } 117 | } 118 | 119 | // Optimize buffer usage. 120 | s.data.shrink_to_fit(); 121 | s.cell_spans.shrink_to_fit(); 122 | s.row_offsets.shrink_to_fit(); 123 | 124 | s 125 | } 126 | 127 | /// Calculate the width of the table. This is the longest row in the table. 128 | pub fn calc_table_width(&self) -> usize { 129 | self.row_offsets 130 | .windows(2) 131 | .map(|range| range[1] - range[0]) 132 | .max() 133 | .unwrap_or(0) as usize 134 | } 135 | 136 | pub fn num_columns_at(&self, row: usize) -> usize { 137 | if row >= self.row_offsets.len() - 1 { 138 | return 0; 139 | } 140 | 141 | let start = self.row_offsets[row] as usize; 142 | let end = self.row_offsets[row + 1] as usize; 143 | 144 | end - start 145 | } 146 | 147 | pub fn num_rows(&self) -> usize { 148 | self.row_offsets.len() - 1 149 | } 150 | 151 | pub fn get_cell(&self, row: usize, column: usize) -> Option<&str> { 152 | let row_offset = *self.row_offsets.get(row)? as usize; 153 | let cell_span = self.cell_spans.get(row_offset + column)?; 154 | 155 | Some(&self.data[cell_span.start as usize..cell_span.end as usize]) 156 | } 157 | 158 | // TODO: Iterator function which returns (row, column, cell data) tuple. 159 | pub fn iter_rows(&self) -> impl Iterator)> { 160 | self.row_offsets 161 | .windows(2) 162 | .enumerate() 163 | .map(move |(row, range)| { 164 | let (start, end) = (range[0] as usize, range[1] as usize); 165 | let row_iter = (start..end).map(move |cell_offset| { 166 | let cell_span = self.cell_spans.get(cell_offset).unwrap(); 167 | ( 168 | cell_offset - start, 169 | &self.data[cell_span.start as usize..cell_span.end as usize], 170 | ) 171 | }); 172 | 173 | (row, row_iter) 174 | }) 175 | } 176 | 177 | #[cfg(test)] 178 | fn iter_index_data(&self) -> impl Iterator { 179 | self.iter_rows() 180 | .flat_map(|(row, row_iter)| row_iter.map(move |(col, data)| (row, col, data))) 181 | } 182 | } 183 | 184 | #[test] 185 | fn tsv_parsing() { 186 | const TSV_DATA: &str = "Hello\tWorld\nThis\tIs\tA\tTest"; 187 | 188 | let parsed = ParsedTsv::parse(TSV_DATA); 189 | assert_eq!(parsed.num_columns_at(0), 2); 190 | assert_eq!(parsed.num_columns_at(1), 4); 191 | assert_eq!(parsed.num_columns_at(2), 0); 192 | 193 | assert_eq!(parsed.num_rows(), 2); 194 | 195 | assert_eq!(parsed.get_cell(0, 0), Some("Hello")); 196 | assert_eq!(parsed.get_cell(0, 1), Some("World")); 197 | assert_eq!(parsed.get_cell(1, 0), Some("This")); 198 | assert_eq!(parsed.get_cell(1, 1), Some("Is")); 199 | assert_eq!(parsed.get_cell(1, 2), Some("A")); 200 | assert_eq!(parsed.get_cell(1, 3), Some("Test")); 201 | assert!(parsed.get_cell(1, 4).is_none()); 202 | 203 | assert_eq!( 204 | parsed.iter_index_data().collect::>(), 205 | vec![ 206 | (0, 0, "Hello"), 207 | (0, 1, "World"), 208 | (1, 0, "This"), 209 | (1, 1, "Is"), 210 | (1, 2, "A"), 211 | (1, 3, "Test"), 212 | ] 213 | ); 214 | } 215 | -------------------------------------------------------------------------------- /examples/partially_editable.rs: -------------------------------------------------------------------------------- 1 | //! Demonstrate the partially editable features. 2 | //! 3 | //! Sometimes, some of the data you need to work with is not always editable, this example uses API features 4 | //! to prevent new rows being added/deleted and to prevent some cells from being edited/cleared or pasted into. 5 | //! 6 | //! See [`Viewer::is_editable_cell`], [`Viewer::allow_row_insertions`] and [`Viewer::allow_row_deletions`] 7 | 8 | use std::borrow::Cow; 9 | use egui::{Response, Ui}; 10 | use egui_data_table::RowViewer; 11 | use std::collections::HashMap; 12 | use tap::Tap; 13 | 14 | #[derive( 15 | Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize, 16 | )] 17 | pub struct Part { 18 | pub manufacturer: String, 19 | pub mpn: String, 20 | } 21 | 22 | impl Part { 23 | pub fn new(manufacturer: String, mpn: String) -> Self { 24 | Self { manufacturer, mpn } 25 | } 26 | } 27 | 28 | #[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug, Clone)] 29 | pub struct PartWithState { 30 | pub part: Part, 31 | pub processes: Vec, 32 | } 33 | 34 | #[derive(Debug)] 35 | struct PartStatesRow { 36 | part: Part, 37 | enabled_processes: HashMap, 38 | } 39 | 40 | struct DemoApp { 41 | table: egui_data_table::DataTable, 42 | viewer: Viewer, 43 | } 44 | 45 | impl Default for DemoApp { 46 | fn default() -> Self { 47 | let parts_states = vec![ 48 | PartWithState { 49 | part: Part::new( 50 | "Manufacturer 1".to_string(), 51 | "MFR1MPN1".to_ascii_lowercase(), 52 | ), 53 | processes: vec!["pnp".to_string()], 54 | }, 55 | PartWithState { 56 | part: Part::new( 57 | "Manufacturer 2".to_string(), 58 | "MFR2MPN1".to_ascii_lowercase(), 59 | ), 60 | processes: vec!["pnp".to_string()], 61 | }, 62 | PartWithState { 63 | part: Part::new( 64 | "Manufacturer 2".to_string(), 65 | "MFR2MPN2".to_ascii_lowercase(), 66 | ), 67 | processes: vec!["manual".to_string()], 68 | }, 69 | ]; 70 | 71 | let processes: Vec = vec!["manual".to_string(), "pnp".to_string()]; 72 | 73 | let table = parts_states 74 | .iter() 75 | .map(|part_state| { 76 | let enabled_processes = processes 77 | .iter() 78 | .map(|process| (process.clone(), part_state.processes.contains(process))) 79 | .collect::>(); 80 | 81 | PartStatesRow { 82 | part: part_state.part.clone(), 83 | enabled_processes, 84 | } 85 | }) 86 | .collect(); 87 | 88 | Self { 89 | table, 90 | viewer: Viewer::default(), 91 | } 92 | } 93 | } 94 | 95 | #[derive(Default)] 96 | struct Viewer { 97 | pub enable_row_insertion: bool, 98 | pub enable_row_deletion: bool, 99 | } 100 | 101 | impl RowViewer for Viewer { 102 | fn num_columns(&mut self) -> usize { 103 | 3 104 | } 105 | 106 | fn column_name(&mut self, column: usize) -> Cow<'static, str> { 107 | match column { 108 | 0 => "Manufacturer".into(), 109 | 1 => "MPN".into(), 110 | 2 => "Processes".into(), 111 | _ => unreachable!(), 112 | } 113 | } 114 | 115 | fn is_editable_cell(&mut self, column: usize, _row: usize, _row_value: &PartStatesRow) -> bool { 116 | match column { 117 | 0 => false, 118 | 1 => false, 119 | 2 => true, 120 | _ => unreachable!(), 121 | } 122 | } 123 | 124 | fn allow_row_insertions(&mut self) -> bool { 125 | self.enable_row_insertion 126 | } 127 | 128 | fn allow_row_deletions(&mut self) -> bool { 129 | self.enable_row_deletion 130 | } 131 | 132 | fn show_cell_view(&mut self, ui: &mut Ui, row: &PartStatesRow, column: usize) { 133 | match column { 134 | 0 => { 135 | ui.label(&row.part.manufacturer); 136 | } 137 | 1 => { 138 | ui.label(&row.part.mpn); 139 | } 140 | 2 => { 141 | let processes = row 142 | .enabled_processes 143 | .iter() 144 | .filter_map(|(process, enabled)| { 145 | if *enabled { 146 | Some(process.clone()) 147 | } else { 148 | None 149 | } 150 | }) 151 | .collect::>() 152 | .tap_mut(|processes|processes.sort()); 153 | let label = processes.join(", "); 154 | ui.label(label); 155 | } 156 | _ => unreachable!(), 157 | } 158 | } 159 | 160 | fn show_cell_editor( 161 | &mut self, 162 | ui: &mut Ui, 163 | row: &mut PartStatesRow, 164 | column: usize, 165 | ) -> Option { 166 | match column { 167 | 2 => { 168 | let ui = ui.add(|ui: &mut Ui| { 169 | ui.horizontal_wrapped(|ui| { 170 | for (name, enabled) in row.enabled_processes.iter_mut() { 171 | ui.checkbox(enabled, name.clone()); 172 | } 173 | }) 174 | .response 175 | }); 176 | Some(ui) 177 | } 178 | _ => None, 179 | } 180 | } 181 | 182 | fn set_cell_value(&mut self, src: &PartStatesRow, dst: &mut PartStatesRow, column: usize) { 183 | match column { 184 | 0 => dst.part.manufacturer = src.part.manufacturer.clone(), 185 | 1 => dst.part.mpn = src.part.mpn.clone(), 186 | 2 => dst.enabled_processes = src.enabled_processes.clone(), 187 | _ => unreachable!(), 188 | } 189 | } 190 | 191 | fn new_empty_row(&mut self) -> PartStatesRow { 192 | PartStatesRow { 193 | part: Part { 194 | manufacturer: "".to_string(), 195 | mpn: "".to_string(), 196 | }, 197 | enabled_processes: Default::default(), 198 | } 199 | } 200 | } 201 | 202 | impl eframe::App for DemoApp { 203 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 204 | egui::TopBottomPanel::top("menubar").show(ctx, |ui| { 205 | egui::MenuBar::new().ui(ui, |ui| { 206 | ui.checkbox( 207 | &mut self.viewer.enable_row_insertion, 208 | "Enable Row Insertion", 209 | ); 210 | ui.checkbox(&mut self.viewer.enable_row_deletion, "Enable Row Deletion"); 211 | }); 212 | }); 213 | 214 | egui::TopBottomPanel::bottom("bottom_panel").show(ctx, |ui| { 215 | egui::Sides::new().show(ui, |_ui| { 216 | }, |ui|{ 217 | let mut has_modifications = self.table.has_user_modification(); 218 | ui.add_enabled(false, egui::Checkbox::new(&mut has_modifications, "Has modifications")); 219 | 220 | ui.add_enabled_ui(has_modifications, |ui| { 221 | if ui.button("Clear").clicked() { 222 | self.table.clear_user_modification_flag(); 223 | } 224 | }); 225 | }); 226 | }); 227 | 228 | egui::CentralPanel::default().show(ctx, |ui| { 229 | ui.add(egui_data_table::Renderer::new( 230 | &mut self.table, 231 | &mut self.viewer, 232 | )); 233 | }); 234 | } 235 | } 236 | 237 | #[cfg(not(target_arch = "wasm32"))] 238 | fn main() { 239 | use eframe::App; 240 | env_logger::init(); 241 | 242 | eframe::run_simple_native( 243 | "Partially editable demo", 244 | eframe::NativeOptions { 245 | centered: true, 246 | ..Default::default() 247 | }, 248 | { 249 | let mut app = DemoApp::default(); 250 | move |ctx, frame| { 251 | app.update(ctx, frame); 252 | } 253 | }, 254 | ) 255 | .unwrap(); 256 | } 257 | -------------------------------------------------------------------------------- /examples/internationalization.rs: -------------------------------------------------------------------------------- 1 | //! Demonstrate usage of the [`Translator`] trait. 2 | //! 3 | //! Note that it's possible to use more advanced translation systems like Fluent and egui_i18n. 4 | //! by providing something that implements the [`Translator`] trait, this is beyond the scope of this example. 5 | 6 | use std::borrow::Cow; 7 | use std::collections::HashMap; 8 | use egui::{ComboBox, Response, Ui}; 9 | use egui_data_table::RowViewer; 10 | use std::iter::repeat_with; 11 | use std::sync::Arc; 12 | use egui_data_table::draw::{EnglishTranslator, Translator}; 13 | 14 | #[derive(Default)] 15 | struct CustomSpanishTranslator {} 16 | 17 | impl Translator for CustomSpanishTranslator { 18 | fn translate(&self, key: &str) -> String { 19 | match key { 20 | // custom translations 21 | "language" => "Idioma", 22 | 23 | // languages 24 | "en_US" => "Inglés (Estados Unidos)", 25 | "es_ES" => "Español (España)", 26 | 27 | // tables 28 | "table-column-header-name" => "Nombre", 29 | "table-column-header-number" => "Número", 30 | "table-column-header-flag" => "Indicador", 31 | 32 | // cell context menu 33 | "context-menu-selection-copy" => "Selección: Copiar", 34 | "context-menu-selection-cut" => "Selección: Cortar", 35 | "context-menu-selection-clear" => "Selección: Limpiar", 36 | "context-menu-selection-fill" => "Selección: Rellenar", 37 | "context-menu-clipboard-paste" => "Portapapeles: Pegar", 38 | "context-menu-clipboard-insert" => "Portapapeles: Insertar", 39 | "context-menu-row-duplicate" => "Fila: Duplicar", 40 | "context-menu-row-delete" => "Fila: Eliminar", 41 | "context-menu-undo" => "Deshacer", 42 | "context-menu-redo" => "Rehacer", 43 | 44 | // column header context menu 45 | "context-menu-hide" => "Ocultar columna", 46 | "context-menu-hidden" => "Columnas ocultas", 47 | "context-menu-clear-sort" => "Borrar ordenación", 48 | 49 | _ => key, 50 | }.to_string() 51 | } 52 | } 53 | 54 | /// Allows additional translation keys, and can fall back to the EnglishTranslator supplied by this crate. 55 | #[derive(Default)] 56 | struct CustomEnglishTranslator { 57 | fallback_translator: EnglishTranslator 58 | } 59 | 60 | impl Translator for CustomEnglishTranslator { 61 | fn translate(&self, key: &str) -> String { 62 | match key { 63 | // custom translations 64 | "language" => "Language".to_string(), 65 | 66 | // languages 67 | "en_US" => "English (United States)".to_string(), 68 | "es_ES" => "Spanish (Spain)".to_string(), 69 | 70 | // tables 71 | "table-column-header-name" => "Name".to_string(), 72 | "table-column-header-number" => "Number".to_string(), 73 | "table-column-header-flag" => "Flag".to_string(), 74 | 75 | // using the fallback translator for other keys 76 | _ => self.fallback_translator.translate(key), 77 | } 78 | } 79 | } 80 | 81 | struct DemoApp { 82 | table: egui_data_table::DataTable, 83 | viewer: Viewer, 84 | 85 | selected_language_key: String, 86 | translators: HashMap<&'static str, Arc>, 87 | } 88 | 89 | impl Default for DemoApp { 90 | fn default() -> Self { 91 | 92 | let translators: HashMap<&'static str, Arc> = vec![ 93 | ("en_US", Arc::new(CustomEnglishTranslator::default()) as Arc), 94 | ("es_ES", Arc::new(CustomSpanishTranslator::default()) as Arc), 95 | ].into_iter().collect(); 96 | 97 | let selected_language = "en_US".to_string(); 98 | 99 | let translator = translators[selected_language.as_str()].clone(); 100 | 101 | let table = { 102 | let mut rng = fastrand::Rng::new(); 103 | let mut name_gen = names::Generator::with_naming(names::Name::Numbered); 104 | 105 | repeat_with(move || { 106 | Row( 107 | name_gen.next().unwrap(), 108 | rng.i32(4..31), 109 | rng.bool(), 110 | ) 111 | }) 112 | } 113 | .take(10) 114 | .collect(); 115 | 116 | Self { 117 | table, 118 | viewer: Viewer { translator: translator.clone() }, 119 | selected_language_key: selected_language.to_string(), 120 | translators, 121 | } 122 | } 123 | } 124 | 125 | #[derive(Debug, Clone)] 126 | struct Row(String, i32, bool); 127 | 128 | struct Viewer { 129 | translator: Arc, 130 | } 131 | 132 | impl Viewer { 133 | fn change_translator(&mut self, translator: Arc) { 134 | self.translator = translator; 135 | } 136 | } 137 | 138 | impl RowViewer for Viewer { 139 | fn num_columns(&mut self) -> usize { 140 | 3 141 | } 142 | 143 | fn column_name(&mut self, column: usize) -> Cow<'static, str> { 144 | match column { 145 | 0 => self.translator.translate("table-column-header-name").into(), 146 | 1 => self.translator.translate("table-column-header-number").into(), 147 | 2 => self.translator.translate("table-column-header-flag").into(), 148 | _ => unreachable!(), 149 | } 150 | } 151 | 152 | fn is_editable_cell(&mut self, column: usize, _row: usize, _row_value: &Row) -> bool { 153 | match column { 154 | 0 => true, 155 | 1 => true, 156 | 2 => true, 157 | _ => unreachable!(), 158 | } 159 | } 160 | 161 | fn show_cell_view(&mut self, ui: &mut Ui, row: &Row, column: usize) { 162 | match column { 163 | 0 => ui.label(&row.0), 164 | 1 => ui.label(row.1.to_string()), 165 | 2 => ui.checkbox(&mut { row.2 }, ""), 166 | _ => unreachable!(), 167 | }; 168 | } 169 | 170 | fn show_cell_editor( 171 | &mut self, 172 | ui: &mut Ui, 173 | row: &mut Row, 174 | column: usize, 175 | ) -> Option { 176 | match column { 177 | 0 => { 178 | egui::TextEdit::singleline(&mut row.0) 179 | .show(ui) 180 | .response 181 | } 182 | 1 => ui.add(egui::DragValue::new(&mut row.1).speed(1.0)), 183 | 2 => ui.checkbox(&mut row.2, ""), 184 | _ => unreachable!(), 185 | } 186 | .into() 187 | } 188 | 189 | fn set_cell_value(&mut self, src: &Row, dst: &mut Row, column: usize) { 190 | match column { 191 | 0 => dst.0.clone_from(&src.0), 192 | 1 => dst.1 = src.1, 193 | 2 => dst.2 = src.2, 194 | _ => unreachable!(), 195 | } 196 | } 197 | 198 | fn new_empty_row(&mut self) -> Row { 199 | Row("".to_string(), 0, false) 200 | } 201 | } 202 | 203 | impl eframe::App for DemoApp { 204 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 205 | 206 | let mut language_keys: Vec<&str> = self.translators.keys().copied().collect(); 207 | language_keys.sort(); 208 | 209 | let translator = self.translators[&self.selected_language_key.as_str()].clone(); 210 | 211 | egui::TopBottomPanel::top("menubar").show(ctx, |ui| { 212 | ComboBox::from_label(translator.translate("language")) 213 | .selected_text(translator.translate(&self.selected_language_key)) 214 | .show_ui(ui, |ui| { 215 | for &language_key in &language_keys { 216 | let language = translator.translate(language_key); 217 | if ui.selectable_label(self.selected_language_key == language_key, language).clicked() { 218 | self.selected_language_key = language_key.to_string(); 219 | self.viewer.change_translator(self.translators[&self.selected_language_key.as_str()].clone()); 220 | } 221 | } 222 | }); 223 | }); 224 | egui::CentralPanel::default().show(ctx, |ui| { 225 | let renderer = egui_data_table::Renderer::new( 226 | &mut self.table, 227 | &mut self.viewer, 228 | ) 229 | .with_translator(translator); 230 | 231 | ui.add(renderer); 232 | }); 233 | } 234 | } 235 | 236 | #[cfg(not(target_arch = "wasm32"))] 237 | fn main() { 238 | use eframe::App; 239 | env_logger::init(); 240 | 241 | eframe::run_simple_native( 242 | "Translator demo", 243 | eframe::NativeOptions { 244 | centered: true, 245 | ..Default::default() 246 | }, 247 | { 248 | let mut app = DemoApp::default(); 249 | move |ctx, frame| { 250 | app.update(ctx, frame); 251 | } 252 | }, 253 | ) 254 | .unwrap(); 255 | } 256 | -------------------------------------------------------------------------------- /src/viewer.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use egui::{Key, KeyboardShortcut, Modifiers}; 4 | pub use egui_extras::Column as TableColumnConfig; 5 | use tap::prelude::Pipe; 6 | 7 | #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] 8 | pub enum DecodeErrorBehavior { 9 | /// Skip the cell and continue decoding. 10 | SkipCell, 11 | 12 | /// Skip the whole row 13 | SkipRow, 14 | 15 | /// Stop decoding and return error. 16 | #[default] 17 | Abort, 18 | } 19 | 20 | /// A trait for encoding/decoding row data. Any valid UTF-8 string can be used for encoding, 21 | /// however, as csv is used for clipboard operations, it is recommended to serialize data in simple 22 | /// string format as possible. 23 | pub trait RowCodec { 24 | type DeserializeError; 25 | 26 | /// Creates a new empty row for decoding 27 | fn create_empty_decoded_row(&mut self) -> R; 28 | 29 | /// Tries encode column data of given row into a string. As the cell for CSV row is already 30 | /// occupied, if any error or unsupported data is found for that column, just empty out the 31 | /// destination string buffer. 32 | fn encode_column(&mut self, src_row: &R, column: usize, dst: &mut String); 33 | 34 | /// Tries decode column data from a string into a row. 35 | fn decode_column( 36 | &mut self, 37 | src_data: &str, 38 | column: usize, 39 | dst_row: &mut R, 40 | ) -> Result<(), DecodeErrorBehavior>; 41 | } 42 | 43 | /// A placeholder codec for row viewers that not require serialization. 44 | impl RowCodec for () { 45 | type DeserializeError = (); 46 | 47 | fn create_empty_decoded_row(&mut self) -> R { 48 | unimplemented!() 49 | } 50 | 51 | fn encode_column(&mut self, src_row: &R, column: usize, dst: &mut String) { 52 | let _ = (src_row, column, dst); 53 | unimplemented!() 54 | } 55 | 56 | fn decode_column( 57 | &mut self, 58 | src_data: &str, 59 | column: usize, 60 | dst_row: &mut R, 61 | ) -> Result<(), DecodeErrorBehavior> { 62 | let _ = (src_data, column, dst_row); 63 | unimplemented!() 64 | } 65 | } 66 | 67 | /// The primary trait for the spreadsheet viewer. 68 | // TODO: When lifetime for `'static` is stabilized; remove the `static` bound. 69 | pub trait RowViewer: 'static { 70 | /// Number of columns. Changing this will completely invalidate the table rendering status, 71 | /// including undo histories. Therefore, frequently changing this value is discouraged. 72 | fn num_columns(&mut self) -> usize; 73 | 74 | /// Name of the column. This can be dynamically changed. 75 | fn column_name(&mut self, column: usize) -> Cow<'static, str> { 76 | Cow::Borrowed( 77 | &" 0 1 2 3 4 5 6 7 8 91011121314151617181920212223242526272829303132" 78 | [(column % 10 * 2).pipe(|x| x..x + 2)], 79 | ) 80 | } 81 | 82 | /// Tries to create a codec for the row (de)serialization. If this returns `Some`, it'll use 83 | /// the system clipboard for copy/paste operations. 84 | /// 85 | /// `is_encoding` parameter is provided to determine if we're creating the codec as encoding 86 | /// mode or decoding mode. 87 | /// 88 | /// It is just okay to choose not to implement both encoding and decoding; returning `None` 89 | /// conditionally based on `is_encoding` parameter is also valid. It is guaranteed that created 90 | /// codec will be used only for the same mode during its lifetime. 91 | fn try_create_codec(&mut self, is_encoding: bool) -> Option> { 92 | let _ = is_encoding; 93 | None::<()> 94 | } 95 | 96 | /// Returns the rendering configuration for the column. 97 | fn column_render_config( 98 | &mut self, 99 | column: usize, 100 | is_last_visible_column: bool, 101 | ) -> TableColumnConfig { 102 | let _ = column; 103 | if is_last_visible_column { 104 | TableColumnConfig::remainder().at_least(24.0) 105 | } else { 106 | TableColumnConfig::auto().resizable(true) 107 | } 108 | } 109 | 110 | /// Returns if given column is 'sortable' 111 | fn is_sortable_column(&mut self, column: usize) -> bool { 112 | let _ = column; 113 | false 114 | } 115 | 116 | /// Returns if a given cell is 'editable'. 117 | /// 118 | /// i.e. 119 | /// * true to allow editing of a cell 120 | /// * false to disable editing of a cell 121 | fn is_editable_cell(&mut self, column: usize, row: usize, row_value: &R) -> bool { 122 | let _ = column; 123 | let _ = row; 124 | let _ = row_value; 125 | true 126 | } 127 | 128 | /// Returns if row insertions are allowed. 129 | fn allow_row_insertions(&mut self) -> bool { 130 | true 131 | } 132 | 133 | /// Returns if row deletions are allowed. 134 | fn allow_row_deletions(&mut self) -> bool { 135 | true 136 | } 137 | 138 | /// Compare two column contents for sort. 139 | fn compare_cell(&self, row_a: &R, row_b: &R, column: usize) -> std::cmp::Ordering { 140 | let _ = (row_a, row_b, column); 141 | std::cmp::Ordering::Equal 142 | } 143 | 144 | /// Get hash value of a filter. This is used to determine if the filter has changed. 145 | fn row_filter_hash(&mut self) -> &impl std::hash::Hash { 146 | &() 147 | } 148 | 149 | /// Filter single row. If this returns false, the row will be hidden. 150 | fn filter_row(&mut self, row: &R) -> bool { 151 | let _ = row; 152 | true 153 | } 154 | 155 | /// Display values of the cell. Any input will be consumed before table renderer; 156 | /// therefore any widget rendered inside here is read-only. 157 | /// 158 | /// To deal with input, use `cell_edit` method. If you need to deal with drag/drop, 159 | /// see [`RowViewer::on_cell_view_response`] which delivers resulting response of 160 | /// containing cell. 161 | fn show_cell_view(&mut self, ui: &mut egui::Ui, row: &R, column: usize); 162 | 163 | /// Use this to check if given cell is going to take any dropped payload / use as drag 164 | /// source. 165 | fn on_cell_view_response( 166 | &mut self, 167 | row: &R, 168 | column: usize, 169 | resp: &egui::Response, 170 | ) -> Option> { 171 | let _ = (row, column, resp); 172 | None 173 | } 174 | 175 | /// Edit values of the cell. 176 | fn show_cell_editor( 177 | &mut self, 178 | ui: &mut egui::Ui, 179 | row: &mut R, 180 | column: usize, 181 | ) -> Option; 182 | 183 | /// Set the value of a column in a row. 184 | fn set_cell_value(&mut self, src: &R, dst: &mut R, column: usize); 185 | 186 | /// In the write context that happens outside of `show_cell_editor`, this method is 187 | /// called on every cell value editions. 188 | fn confirm_cell_write_by_ui( 189 | &mut self, 190 | current: &R, 191 | next: &R, 192 | column: usize, 193 | context: CellWriteContext, 194 | ) -> bool { 195 | let _ = (current, next, column, context); 196 | true 197 | } 198 | 199 | /// Before removing each row, this method is called to confirm the deletion from the 200 | /// viewer. This won't be called during the undo/redo operation! 201 | fn confirm_row_deletion_by_ui(&mut self, row: &R) -> bool { 202 | let _ = row; 203 | true 204 | } 205 | 206 | /// Create a new empty row. 207 | fn new_empty_row(&mut self) -> R; 208 | 209 | /// Create a new empty row under the given context. 210 | fn new_empty_row_for(&mut self, context: EmptyRowCreateContext) -> R { 211 | let _ = context; 212 | self.new_empty_row() 213 | } 214 | 215 | /// Create duplication of existing row. 216 | /// 217 | /// You may want to override this method for more efficient duplication. 218 | fn clone_row(&mut self, row: &R) -> R { 219 | let mut dst = self.new_empty_row(); 220 | for i in 0..self.num_columns() { 221 | self.set_cell_value(row, &mut dst, i); 222 | } 223 | dst 224 | } 225 | 226 | /// Create duplication of existing row for insertion. 227 | fn clone_row_for_insertion(&mut self, row: &R) -> R { 228 | self.clone_row(row) 229 | } 230 | 231 | /// Create duplication of existing row for clipboard. Useful when you need to specify 232 | /// different behavior for clipboard duplication. (e.g. unset transient flag) 233 | fn clone_row_as_copied_base(&mut self, row: &R) -> R { 234 | self.clone_row(row) 235 | } 236 | 237 | /// Called when a cell is selected/highlighted. 238 | fn on_highlight_cell(&mut self, row: &R, column: usize) { 239 | let _ = (row, column); 240 | } 241 | 242 | /// Called when a row selected/highlighted status changes. 243 | fn on_highlight_change(&mut self, highlighted: &[&R], unhighlighted: &[&R]) { 244 | let (_, _) = (highlighted, unhighlighted); 245 | } 246 | 247 | /// Called when a row is updated, including when undoing/redoing 248 | fn on_row_updated(&mut self, row_index: usize, new_row: &R, old_row: &R) { 249 | let (_, _, _) = (row_index, new_row, old_row); 250 | } 251 | 252 | /// Called when a row has been inserted, including when undoing/redoing 253 | fn on_row_inserted(&mut self, row_index: usize, row: &R) { 254 | let (_, _) = (row_index, row); 255 | } 256 | 257 | /// Called when a row has been removed, including when undoing/redoing 258 | fn on_row_removed(&mut self, row_index: usize, row: &R) { 259 | let (_, _) = (row_index, row); 260 | } 261 | 262 | /// Return hotkeys for the current context. 263 | fn hotkeys(&mut self, context: &UiActionContext) -> Vec<(egui::KeyboardShortcut, UiAction)> { 264 | self::default_hotkeys(context) 265 | } 266 | 267 | /// If you want to keep UI state on storage(i.e. persist over sessions), return true from this 268 | /// function. 269 | #[cfg(feature = "persistency")] 270 | fn persist_ui_state(&self) -> bool { 271 | false 272 | } 273 | } 274 | 275 | /* ------------------------------------------- Context ------------------------------------------ */ 276 | 277 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 278 | #[non_exhaustive] 279 | pub enum CellWriteContext { 280 | /// Value is being pasted/duplicated from different row. 281 | Paste, 282 | 283 | /// Value is being cleared by cut/delete operation. 284 | Clear, 285 | } 286 | 287 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 288 | #[non_exhaustive] 289 | pub enum EmptyRowCreateContext { 290 | /// Row is created to be used as simple default template. 291 | Default, 292 | 293 | /// Row is created to be used as explicit `empty` value when deletion 294 | DeletionDefault, 295 | 296 | /// Row is created to be inserted as a new row. 297 | InsertNewLine, 298 | } 299 | 300 | /* ------------------------------------------- Hotkeys ------------------------------------------ */ 301 | 302 | /// Base context for determining current input state. 303 | #[derive(Debug, Clone)] 304 | #[non_exhaustive] 305 | pub struct UiActionContext { 306 | pub cursor: UiCursorState, 307 | } 308 | 309 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 310 | pub enum UiCursorState { 311 | Idle, 312 | Editing, 313 | SelectOne, 314 | SelectMany, 315 | } 316 | 317 | impl UiCursorState { 318 | pub fn is_idle(&self) -> bool { 319 | matches!(self, Self::Idle) 320 | } 321 | 322 | pub fn is_editing(&self) -> bool { 323 | matches!(self, Self::Editing) 324 | } 325 | 326 | pub fn is_selecting(&self) -> bool { 327 | matches!(self, Self::SelectOne | Self::SelectMany) 328 | } 329 | } 330 | 331 | /* ----------------------------------------- Ui Actions ----------------------------------------- */ 332 | 333 | /// Represents a user interaction, calculated from the UI input state. 334 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 335 | #[non_exhaustive] 336 | pub enum UiAction { 337 | SelectionStartEditing, 338 | 339 | CancelEdition, 340 | CommitEdition, 341 | 342 | CommitEditionAndMove(MoveDirection), 343 | 344 | Undo, 345 | Redo, 346 | 347 | MoveSelection(MoveDirection), 348 | CopySelection, 349 | CutSelection, 350 | 351 | PasteInPlace, 352 | PasteInsert, 353 | 354 | DuplicateRow, 355 | DeleteSelection, 356 | DeleteRow, 357 | 358 | NavPageDown, 359 | NavPageUp, 360 | NavTop, 361 | NavBottom, 362 | 363 | SelectionDuplicateValues, 364 | SelectAll, 365 | } 366 | 367 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 368 | pub enum MoveDirection { 369 | Up, 370 | Down, 371 | Left, 372 | Right, 373 | } 374 | 375 | pub fn default_hotkeys(context: &UiActionContext) -> Vec<(KeyboardShortcut, UiAction)> { 376 | let c = context.cursor; 377 | 378 | fn shortcut(actions: &[(Modifiers, Key, UiAction)]) -> Vec<(egui::KeyboardShortcut, UiAction)> { 379 | actions 380 | .iter() 381 | .map(|(m, k, a)| (egui::KeyboardShortcut::new(*m, *k), *a)) 382 | .collect() 383 | } 384 | 385 | let none = Modifiers::NONE; 386 | let ctrl = Modifiers::CTRL; 387 | let alt = Modifiers::ALT; 388 | let shift = Modifiers::SHIFT; 389 | 390 | use UiAction::CommitEditionAndMove; 391 | type MD = MoveDirection; 392 | 393 | if c.is_editing() { 394 | shortcut(&[ 395 | (none, Key::Escape, UiAction::CommitEdition), 396 | (ctrl, Key::Escape, UiAction::CancelEdition), 397 | (shift, Key::Enter, CommitEditionAndMove(MD::Up)), 398 | (ctrl, Key::Enter, CommitEditionAndMove(MD::Down)), 399 | (shift, Key::Tab, CommitEditionAndMove(MD::Left)), 400 | (none, Key::Tab, CommitEditionAndMove(MD::Right)), 401 | ]) 402 | } else { 403 | shortcut(&[ 404 | (ctrl, Key::X, UiAction::CutSelection), 405 | (ctrl, Key::C, UiAction::CopySelection), 406 | (ctrl | shift, Key::V, UiAction::PasteInsert), 407 | (ctrl, Key::V, UiAction::PasteInPlace), 408 | (ctrl, Key::Y, UiAction::Redo), 409 | (ctrl, Key::Z, UiAction::Undo), 410 | (none, Key::Enter, UiAction::SelectionStartEditing), 411 | (none, Key::ArrowUp, UiAction::MoveSelection(MD::Up)), 412 | (none, Key::ArrowDown, UiAction::MoveSelection(MD::Down)), 413 | (none, Key::ArrowLeft, UiAction::MoveSelection(MD::Left)), 414 | (none, Key::ArrowRight, UiAction::MoveSelection(MD::Right)), 415 | (shift, Key::V, UiAction::PasteInsert), 416 | (alt, Key::V, UiAction::PasteInsert), 417 | (ctrl | shift, Key::D, UiAction::DuplicateRow), 418 | (ctrl, Key::D, UiAction::SelectionDuplicateValues), 419 | (ctrl, Key::A, UiAction::SelectAll), 420 | (ctrl, Key::Delete, UiAction::DeleteRow), 421 | (none, Key::Delete, UiAction::DeleteSelection), 422 | (none, Key::Backspace, UiAction::DeleteSelection), 423 | (none, Key::PageUp, UiAction::NavPageUp), 424 | (none, Key::PageDown, UiAction::NavPageDown), 425 | (none, Key::Home, UiAction::NavTop), 426 | (none, Key::End, UiAction::NavBottom), 427 | ]) 428 | } 429 | } 430 | -------------------------------------------------------------------------------- /examples/demo.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, iter::repeat_with}; 2 | use std::fmt::{Display, Formatter}; 3 | use std::str::FromStr; 4 | use egui::{Response, Sense, Widget}; 5 | use egui::scroll_area::ScrollBarVisibility; 6 | use egui_data_table::{ 7 | viewer::{default_hotkeys, CellWriteContext, DecodeErrorBehavior, RowCodec, UiActionContext}, 8 | RowViewer, 9 | }; 10 | 11 | /* ----------------------------------------- Columns -------------------------------------------- */ 12 | 13 | mod columns { 14 | 15 | // column indices 16 | // columns can easily be reordered simply by changing the values of these indices. 17 | pub const NAME: usize = 0; 18 | pub const AGE: usize = 1; 19 | pub const GENDER: usize = 2; 20 | pub const IS_STUDENT: usize = 3; 21 | pub const GRADE: usize = 4; 22 | pub const ROW_LOCKED: usize = 5; 23 | 24 | /// count of columns 25 | pub const COLUMN_COUNT: usize = 6; 26 | 27 | pub const COLUMN_NAMES: [&str; COLUMN_COUNT] = [ 28 | "Name (Click to sort)", 29 | "Age", 30 | "Gender", 31 | "Is Student (Not sortable)", 32 | "Grade", 33 | "Row locked", 34 | ]; 35 | } 36 | use columns::*; 37 | 38 | /* ----------------------------------------- Data Scheme ---------------------------------------- */ 39 | 40 | struct Viewer { 41 | name_filter: String, 42 | row_protection: bool, 43 | hotkeys: Vec<(egui::KeyboardShortcut, egui_data_table::UiAction)>, 44 | } 45 | 46 | #[derive(Debug, Clone)] 47 | struct Row { 48 | name: String, 49 | age: i32, 50 | gender: Option, 51 | is_student: bool, 52 | grade: Grade, 53 | row_locked: bool 54 | } 55 | 56 | impl Default for Row { 57 | fn default() -> Self { 58 | Row { 59 | name: "".to_string(), 60 | age: 0, 61 | gender: None, 62 | is_student: false, 63 | grade: Grade::F, 64 | row_locked: false 65 | } 66 | } 67 | } 68 | 69 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] 70 | enum Grade { 71 | A, 72 | B, 73 | C, 74 | D, 75 | E, 76 | F, 77 | } 78 | 79 | impl Display for Grade { 80 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 81 | match self { 82 | Grade::A => write!(f, "A"), 83 | Grade::B => write!(f, "B"), 84 | Grade::C => write!(f, "C"), 85 | Grade::D => write!(f, "D"), 86 | Grade::E => write!(f, "E"), 87 | Grade::F => write!(f, "F"), 88 | } 89 | } 90 | } 91 | 92 | impl TryFrom for Grade { 93 | type Error = (); 94 | 95 | fn try_from(input: i32) -> Result { 96 | let value = match input { 97 | 0 => Grade::A, 98 | 1 => Grade::B, 99 | 2 => Grade::C, 100 | 3 => Grade::D, 101 | 4 => Grade::E, 102 | 5 => Grade::F, 103 | _ => return Err(()) 104 | }; 105 | Ok(value) 106 | } 107 | } 108 | 109 | impl FromStr for Grade { 110 | type Err = (); 111 | 112 | fn from_str(input: &str) -> Result { 113 | let value = match input { 114 | "A" => Grade::A, 115 | "B" => Grade::B, 116 | "C" => Grade::C, 117 | "D" => Grade::D, 118 | "E" => Grade::E, 119 | "F" => Grade::F, 120 | _ => return Err(()), 121 | }; 122 | 123 | Ok(value) 124 | } 125 | } 126 | 127 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] 128 | enum Gender { 129 | Male, 130 | Female, 131 | } 132 | 133 | impl Display for Gender { 134 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 135 | match self { 136 | Gender::Male => write!(f, "Male"), 137 | Gender::Female => write!(f, "Female"), 138 | } 139 | } 140 | } 141 | 142 | 143 | /* -------------------------------------------- Codec ------------------------------------------- */ 144 | 145 | struct Codec; 146 | 147 | impl RowCodec for Codec { 148 | type DeserializeError = &'static str; 149 | 150 | fn encode_column(&mut self, src_row: &Row, column: usize, dst: &mut String) { 151 | match column { 152 | NAME => dst.push_str(&src_row.name), 153 | AGE => dst.push_str(&src_row.age.to_string()), 154 | IS_STUDENT => dst.push_str(&src_row.is_student.to_string()), 155 | GRADE => dst.push_str(src_row.grade.to_string().as_str()), 156 | ROW_LOCKED => dst.push_str(&src_row.row_locked.to_string()), 157 | _ => unreachable!(), 158 | } 159 | } 160 | 161 | fn decode_column( 162 | &mut self, 163 | src_data: &str, 164 | column: usize, 165 | dst_row: &mut Row, 166 | ) -> Result<(), DecodeErrorBehavior> { 167 | match column { 168 | NAME => dst_row.name.replace_range(.., src_data), 169 | AGE => dst_row.age = src_data.parse().map_err(|_| DecodeErrorBehavior::SkipRow)?, 170 | IS_STUDENT => dst_row.is_student = src_data.parse().map_err(|_| DecodeErrorBehavior::SkipRow)?, 171 | GRADE => { 172 | dst_row.grade = src_data.parse().map_err(|_| DecodeErrorBehavior::SkipRow)?; 173 | } 174 | ROW_LOCKED => dst_row.row_locked = src_data.parse().map_err(|_| DecodeErrorBehavior::SkipRow)?, 175 | _ => unreachable!(), 176 | } 177 | 178 | Ok(()) 179 | } 180 | 181 | fn create_empty_decoded_row(&mut self) -> Row { 182 | Row::default() 183 | } 184 | } 185 | 186 | /* ------------------------------------ Viewer Implementation ----------------------------------- */ 187 | 188 | impl RowViewer for Viewer { 189 | 190 | fn on_highlight_cell(&mut self, row: &Row, column: usize) { 191 | println!("cell highlighted: row: {:?}, column: {}", row, column); 192 | } 193 | 194 | fn try_create_codec(&mut self, _: bool) -> Option> { 195 | Some(Codec) 196 | } 197 | 198 | fn num_columns(&mut self) -> usize { 199 | COLUMN_COUNT 200 | } 201 | 202 | fn column_name(&mut self, column: usize) -> Cow<'static, str> { 203 | COLUMN_NAMES[column] 204 | .into() 205 | } 206 | 207 | fn is_sortable_column(&mut self, column: usize) -> bool { 208 | [true, true, true, false, true, true][column] 209 | } 210 | 211 | fn is_editable_cell(&mut self, column: usize, _row: usize, row_value: &Row) -> bool { 212 | let row_locked = row_value.row_locked; 213 | // allow editing of the locked flag, but prevent editing other columns when locked. 214 | match column { 215 | ROW_LOCKED => true, 216 | _ => !row_locked, 217 | } 218 | } 219 | 220 | fn compare_cell(&self, row_l: &Row, row_r: &Row, column: usize) -> std::cmp::Ordering { 221 | match column { 222 | NAME => row_l.name.cmp(&row_r.name), 223 | AGE => row_l.age.cmp(&row_r.age), 224 | GENDER => row_l.gender.cmp(&row_r.gender), 225 | IS_STUDENT => unreachable!(), 226 | GRADE => row_l.grade.cmp(&row_r.grade), 227 | ROW_LOCKED => row_l.row_locked.cmp(&row_r.row_locked), 228 | _ => unreachable!(), 229 | } 230 | } 231 | 232 | fn new_empty_row(&mut self) -> Row { 233 | Row::default() 234 | } 235 | 236 | fn set_cell_value(&mut self, src: &Row, dst: &mut Row, column: usize) { 237 | match column { 238 | NAME => dst.name.clone_from(&src.name), 239 | AGE => dst.age = src.age, 240 | GENDER => dst.gender = src.gender, 241 | IS_STUDENT => dst.is_student = src.is_student, 242 | GRADE => dst.grade = src.grade, 243 | ROW_LOCKED => dst.row_locked = src.row_locked, 244 | _ => unreachable!(), 245 | } 246 | } 247 | 248 | fn confirm_cell_write_by_ui( 249 | &mut self, 250 | current: &Row, 251 | _next: &Row, 252 | _column: usize, 253 | _context: CellWriteContext, 254 | ) -> bool { 255 | if !self.row_protection { 256 | return true; 257 | } 258 | 259 | !current.is_student 260 | } 261 | 262 | fn confirm_row_deletion_by_ui(&mut self, row: &Row) -> bool { 263 | if !self.row_protection { 264 | return true; 265 | } 266 | 267 | !row.is_student 268 | } 269 | 270 | fn show_cell_view(&mut self, ui: &mut egui::Ui, row: &Row, column: usize) { 271 | let _ = match column { 272 | NAME => ui.label(&row.name), 273 | AGE => ui.label(row.age.to_string()), 274 | GENDER => ui.label(row.gender.map(|gender|gender.to_string()).unwrap_or("Unspecified".to_string())), 275 | IS_STUDENT => ui.checkbox(&mut { row.is_student }, ""), 276 | GRADE => ui.label(row.grade.to_string()), 277 | ROW_LOCKED => ui.checkbox(&mut { row.row_locked }, ""), 278 | 279 | _ => unreachable!(), 280 | }; 281 | } 282 | 283 | fn on_cell_view_response( 284 | &mut self, 285 | _row: &Row, 286 | _column: usize, 287 | resp: &egui::Response, 288 | ) -> Option> { 289 | resp.dnd_release_payload::() 290 | .map(|x| { 291 | Box::new(Row{ 292 | name: (*x).clone(), 293 | age: 9999, 294 | gender: Some(Gender::Female), 295 | is_student: false, 296 | grade: Grade::A, 297 | row_locked: false 298 | }) 299 | }) 300 | } 301 | 302 | fn show_cell_editor( 303 | &mut self, 304 | ui: &mut egui::Ui, 305 | row: &mut Row, 306 | column: usize, 307 | ) -> Option { 308 | match column { 309 | NAME => { 310 | egui::TextEdit::multiline(&mut row.name) 311 | .desired_rows(1) 312 | .code_editor() 313 | .show(ui) 314 | .response 315 | } 316 | AGE => ui.add(egui::DragValue::new(&mut row.age).speed(1.0)), 317 | GENDER => { 318 | let gender = &mut row.gender; 319 | 320 | egui::ComboBox::new(ui.id().with("gender"), "".to_string()) 321 | .selected_text(gender.map(|gender|gender.to_string()).unwrap_or("Unspecified".to_string())) 322 | .show_ui(ui, |ui|{ 323 | if ui 324 | .add(egui::Button::selectable( 325 | matches!(gender, Some(gender) if *gender == Gender::Male), 326 | "Male" 327 | )) 328 | .clicked() 329 | { 330 | *gender = Some(Gender::Male); 331 | } 332 | if ui 333 | .add(egui::Button::selectable( 334 | matches!(gender, Some(gender) if *gender == Gender::Female), 335 | "Female" 336 | )) 337 | .clicked() 338 | { 339 | *gender = Some(Gender::Female); 340 | } 341 | 342 | }).response 343 | } 344 | IS_STUDENT => ui.checkbox(&mut row.is_student, ""), 345 | GRADE => { 346 | let grade = &mut row.grade; 347 | ui.horizontal_wrapped(|ui| { 348 | ui.radio_value(grade, Grade::A, "A") 349 | | ui.radio_value(grade, Grade::B, "B") 350 | | ui.radio_value(grade, Grade::C, "C") 351 | | ui.radio_value(grade, Grade::D, "D") 352 | | ui.radio_value(grade, Grade::E, "E") 353 | | ui.radio_value(grade, Grade::F, "F") 354 | }) 355 | .inner 356 | } 357 | ROW_LOCKED => ui.checkbox(&mut row.row_locked, ""), 358 | _ => unreachable!(), 359 | } 360 | .into() 361 | } 362 | 363 | fn row_filter_hash(&mut self) -> &impl std::hash::Hash { 364 | &self.name_filter 365 | } 366 | 367 | fn filter_row(&mut self, row: &Row) -> bool { 368 | row.name.contains(&self.name_filter) 369 | } 370 | 371 | fn hotkeys( 372 | &mut self, 373 | context: &UiActionContext, 374 | ) -> Vec<(egui::KeyboardShortcut, egui_data_table::UiAction)> { 375 | let hotkeys = default_hotkeys(context); 376 | self.hotkeys.clone_from(&hotkeys); 377 | hotkeys 378 | } 379 | 380 | fn persist_ui_state(&self) -> bool { 381 | true 382 | } 383 | 384 | fn on_highlight_change(&mut self, highlighted: &[&Row], unhighlighted: &[&Row]) { 385 | println!("highlight {:?}", highlighted); 386 | println!("unhighlight {:?}", unhighlighted); 387 | } 388 | 389 | fn on_row_updated(&mut self, row_index: usize, new_row: &Row, old_row: &Row) { 390 | println!("row updated. row_id: {}, new_row: {:?}, old_row: {:?}", row_index, new_row, old_row); 391 | } 392 | 393 | fn on_row_inserted(&mut self, row_index: usize, row: &Row) { 394 | println!("row inserted. row_id: {}, values: {:?}", row_index, row); 395 | } 396 | 397 | fn on_row_removed(&mut self, row_index: usize, row: &Row) { 398 | println!("row removed. row_id: {}, values: {:?}", row_index, row); 399 | } 400 | } 401 | 402 | /* ------------------------------------------ View Loop ----------------------------------------- */ 403 | 404 | struct DemoApp { 405 | table: egui_data_table::DataTable, 406 | viewer: Viewer, 407 | style_override: egui_data_table::Style, 408 | scroll_bar_always_visible: bool, 409 | } 410 | 411 | impl Default for DemoApp { 412 | fn default() -> Self { 413 | Self { 414 | table: { 415 | let mut rng = fastrand::Rng::new(); 416 | let mut name_gen = names::Generator::with_naming(names::Name::Numbered); 417 | 418 | repeat_with(move || { 419 | Row { 420 | name: name_gen.next().unwrap(), 421 | age: rng.i32(4..31), 422 | gender: match rng.i32(0..=2) { 423 | 0 => None, 424 | 1 => Some(Gender::Male), 425 | 2 => Some(Gender::Female), 426 | _ => unreachable!(), 427 | }, 428 | is_student: rng.bool(), 429 | grade: rng.i32(0..=5).try_into().unwrap_or(Grade::F), 430 | row_locked: false, 431 | } 432 | }) 433 | } 434 | .take(100000) 435 | .collect(), 436 | viewer: Viewer { 437 | name_filter: String::new(), 438 | hotkeys: Vec::new(), 439 | row_protection: false, 440 | }, 441 | style_override: Default::default(), 442 | scroll_bar_always_visible: false, 443 | } 444 | } 445 | } 446 | 447 | impl eframe::App for DemoApp { 448 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 449 | fn is_send(_: &T) {} 450 | fn is_sync(_: &T) {} 451 | 452 | is_send(&self.table); 453 | is_sync(&self.table); 454 | 455 | egui::TopBottomPanel::top("MenuBar").show(ctx, |ui| { 456 | egui::MenuBar::new().ui(ui, |ui| { 457 | ui.hyperlink_to( 458 | " kang-sw/egui-data-table", 459 | "https://github.com/kang-sw/egui-data-table", 460 | ); 461 | 462 | ui.hyperlink_to( 463 | "(source)", 464 | "https://github.com/kang-sw/egui-data-table/blob/master/examples/demo.rs", 465 | ); 466 | 467 | ui.separator(); 468 | 469 | egui::widgets::global_theme_preference_buttons(ui); 470 | 471 | ui.separator(); 472 | 473 | ui.label("Name Filter"); 474 | ui.text_edit_singleline(&mut self.viewer.name_filter); 475 | 476 | ui.add(egui::Button::new("Drag me and drop on any cell").sense(Sense::drag())) 477 | .on_hover_text( 478 | "Dropping this will replace the cell \ 479 | content with some predefined value.", 480 | ) 481 | .dnd_set_drag_payload(String::from("Hallo~")); 482 | 483 | ui.menu_button("🎌 Flags", |ui| { 484 | ui.checkbox(&mut self.viewer.row_protection, "Row Protection") 485 | .on_hover_text( 486 | "If checked, any rows `Is Student` marked \ 487 | won't be deleted or overwritten by UI actions.", 488 | ); 489 | 490 | ui.checkbox( 491 | &mut self.style_override.single_click_edit_mode, 492 | "Single Click Edit", 493 | ) 494 | .on_hover_text("If checked, cells will be edited with a single click."); 495 | 496 | ui.checkbox( 497 | &mut self.style_override.auto_shrink.x, 498 | "Auto-shrink X", 499 | ); 500 | ui.checkbox( 501 | &mut self.style_override.auto_shrink.y, 502 | "Auto-shrink Y", 503 | ); 504 | 505 | ui.checkbox( 506 | &mut self.scroll_bar_always_visible, 507 | "Scrollbar always visible", 508 | ); 509 | 510 | if ui.button("Shuffle Rows").clicked() { 511 | fastrand::shuffle(&mut self.table); 512 | } 513 | }) 514 | }) 515 | }); 516 | 517 | egui::TopBottomPanel::bottom("bottom_panel").show(ctx, |ui| { 518 | egui::Sides::new().show(ui, |_ui| { 519 | }, |ui|{ 520 | let mut has_modifications = self.table.has_user_modification(); 521 | ui.add_enabled(false, egui::Checkbox::new(&mut has_modifications, "Has modifications")); 522 | 523 | ui.add_enabled_ui(has_modifications, |ui| { 524 | if ui.button("Clear").clicked() { 525 | self.table.clear_user_modification_flag(); 526 | } 527 | }); 528 | }); 529 | }); 530 | 531 | egui::SidePanel::left("Hotkeys") 532 | .default_width(500.) 533 | .show(ctx, |ui| { 534 | ui.vertical_centered_justified(|ui| { 535 | ui.heading("Hotkeys"); 536 | ui.separator(); 537 | ui.add_space(0.); 538 | 539 | for (k, a) in &self.viewer.hotkeys { 540 | egui::Button::new(format!("{a:?}")) 541 | .shortcut_text(ctx.format_shortcut(k)) 542 | .wrap_mode(egui::TextWrapMode::Wrap) 543 | .sense(Sense::hover()) 544 | .ui(ui); 545 | } 546 | }); 547 | }); 548 | 549 | egui::CentralPanel::default().show(ctx, |ui| { 550 | match self.scroll_bar_always_visible { 551 | true => { 552 | ui.style_mut().spacing.scroll = egui::style::ScrollStyle::solid(); 553 | self.style_override.scroll_bar_visibility = ScrollBarVisibility::AlwaysVisible; 554 | }, 555 | false => { 556 | ui.style_mut().spacing.scroll = egui::style::ScrollStyle::floating(); 557 | self.style_override.scroll_bar_visibility = ScrollBarVisibility::VisibleWhenNeeded; 558 | } 559 | }; 560 | 561 | ui.add( 562 | egui_data_table::Renderer::new(&mut self.table, &mut self.viewer) 563 | .with_style(self.style_override), 564 | ) 565 | }); 566 | } 567 | } 568 | 569 | /* --------------------------------------- App Entrypoint --------------------------------------- */ 570 | 571 | #[cfg(not(target_arch = "wasm32"))] 572 | fn main() { 573 | use eframe::App; 574 | env_logger::init(); 575 | 576 | eframe::run_simple_native( 577 | "Spreadsheet Demo", 578 | eframe::NativeOptions { 579 | centered: true, 580 | ..Default::default() 581 | }, 582 | { 583 | let mut app = DemoApp::default(); 584 | move |ctx, frame| { 585 | app.update(ctx, frame); 586 | } 587 | }, 588 | ) 589 | .unwrap(); 590 | } 591 | 592 | // When compiling to web using trunk: 593 | #[cfg(target_arch = "wasm32")] 594 | fn main() { 595 | use eframe::wasm_bindgen::JsCast as _; 596 | 597 | // Redirect `log` message to `console.log` and friends: 598 | eframe::WebLogger::init(log::LevelFilter::Debug).ok(); 599 | 600 | let web_options = eframe::WebOptions::default(); 601 | 602 | wasm_bindgen_futures::spawn_local(async { 603 | let document = web_sys::window() 604 | .expect("No window") 605 | .document() 606 | .expect("No document"); 607 | 608 | let canvas = document 609 | .get_element_by_id("the_canvas_id") 610 | .expect("Failed to find the_canvas_id") 611 | .dyn_into::() 612 | .expect("the_canvas_id was not a HtmlCanvasElement"); 613 | 614 | let start_result = eframe::WebRunner::new() 615 | .start( 616 | canvas, 617 | web_options, 618 | Box::new(|_cc| Ok(Box::new(DemoApp::default()))), 619 | ) 620 | .await; 621 | 622 | // Remove the loading text and spinner: 623 | if let Some(loading_text) = document.get_element_by_id("loading_text") { 624 | match start_result { 625 | Ok(_) => { 626 | loading_text.remove(); 627 | } 628 | Err(e) => { 629 | loading_text.set_inner_html( 630 | "

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

", 631 | ); 632 | panic!("Failed to start eframe: {e:?}"); 633 | } 634 | } 635 | } 636 | }); 637 | } 638 | -------------------------------------------------------------------------------- /src/draw.rs: -------------------------------------------------------------------------------- 1 | use std::mem::{replace, take}; 2 | 3 | use egui::{Align, Color32, CornerRadius, Event, Label, Layout, PointerButton, PopupAnchor, Rect, Response, RichText, Sense, Stroke, StrokeKind, Tooltip, Vec2b}; 4 | use egui_extras::Column; 5 | use tap::prelude::{Pipe, Tap}; 6 | 7 | use crate::{ 8 | viewer::{EmptyRowCreateContext, RowViewer}, 9 | DataTable, UiAction, 10 | }; 11 | 12 | use self::state::*; 13 | 14 | use format as f; 15 | use std::sync::Arc; 16 | use egui::scroll_area::ScrollBarVisibility; 17 | 18 | pub(crate) mod state; 19 | mod tsv; 20 | 21 | /* -------------------------------------------- Style ------------------------------------------- */ 22 | 23 | /// Style configuration for the table. 24 | // TODO: Implement more style configurations. 25 | #[derive(Default, Debug, Clone, Copy)] 26 | #[non_exhaustive] 27 | pub struct Style { 28 | /// Background color override for selection. Default uses `visuals.selection.bg_fill`. 29 | pub bg_selected_cell: Option, 30 | 31 | /// Background color override for selected cell. Default uses `visuals.selection.bg_fill`. 32 | pub bg_selected_highlight_cell: Option, 33 | 34 | /// Foreground color override for selected cell. Default uses `visuals.strong_text_colors`. 35 | pub fg_selected_highlight_cell: Option, 36 | 37 | /// Foreground color for cells that are going to be selected when mouse is dropped. 38 | pub fg_drag_selection: Option, 39 | 40 | /* ·························································································· */ 41 | /// Maximum number of undo history. This is applied when actual action is performed. 42 | /// 43 | /// Setting value '0' results in kinda appropriate default value. 44 | pub max_undo_history: usize, 45 | 46 | /// If specify this as [`None`], the heterogeneous row height will be used. 47 | pub table_row_height: Option, 48 | 49 | /// When enabled, single click on a cell will start editing mode. Default is `false` where 50 | /// double action(click 1: select, click 2: edit) is required. 51 | pub single_click_edit_mode: bool, 52 | 53 | /// How to align cell contents. Default is left-aligned. 54 | pub cell_align: egui::Align, 55 | 56 | /// Color to use for the stroke above/below focused row. 57 | /// If `None`, defaults to a darkened `warn_fg_color`. 58 | pub focused_row_stroke: Option, 59 | 60 | /// See [`ScrollArea::auto_shrink`] for details. 61 | pub auto_shrink: Vec2b, 62 | 63 | /// See ['ScrollArea::ScrollBarVisibility`] for details. 64 | pub scroll_bar_visibility: ScrollBarVisibility, 65 | } 66 | 67 | /* ------------------------------------------ Rendering ----------------------------------------- */ 68 | 69 | pub struct Renderer<'a, R, V: RowViewer> { 70 | table: &'a mut DataTable, 71 | viewer: &'a mut V, 72 | state: Option>>, 73 | style: Style, 74 | translator: Arc 75 | } 76 | 77 | impl> egui::Widget for Renderer<'_, R, V> { 78 | fn ui(self, ui: &mut egui::Ui) -> Response { 79 | self.show(ui) 80 | } 81 | } 82 | 83 | impl<'a, R, V: RowViewer> Renderer<'a, R, V> { 84 | pub fn new(table: &'a mut DataTable, viewer: &'a mut V) -> Self { 85 | if table.rows.is_empty() && viewer.allow_row_insertions() { 86 | table.push(viewer.new_empty_row_for(EmptyRowCreateContext::InsertNewLine)); 87 | } 88 | 89 | Self { 90 | state: Some(table.ui.take().unwrap_or_default().tap_mut(|state| { 91 | state.validate_identity(viewer); 92 | })), 93 | table, 94 | viewer, 95 | style: Default::default(), 96 | translator: Arc::new(EnglishTranslator::default()), 97 | } 98 | } 99 | 100 | pub fn with_style(mut self, style: Style) -> Self { 101 | self.style = style; 102 | self 103 | } 104 | 105 | pub fn with_style_modify(mut self, f: impl FnOnce(&mut Style)) -> Self { 106 | f(&mut self.style); 107 | self 108 | } 109 | 110 | pub fn with_table_row_height(mut self, height: f32) -> Self { 111 | self.style.table_row_height = Some(height); 112 | self 113 | } 114 | 115 | pub fn with_max_undo_history(mut self, max_undo_history: usize) -> Self { 116 | self.style.max_undo_history = max_undo_history; 117 | self 118 | } 119 | 120 | /// Sets a custom translator for the instance. 121 | /// # Example 122 | /// 123 | /// ``` 124 | /// // Define a simple translator 125 | /// struct EsEsTranslator; 126 | /// impl Translator for EsEsTranslator { 127 | /// fn translate(&self, key: &str) -> String { 128 | /// match key { 129 | /// "hello" => "Hola".to_string(), 130 | /// "world" => "Mundo".to_string(), 131 | /// _ => key.to_string(), 132 | /// } 133 | /// } 134 | /// } 135 | /// 136 | /// let renderer = Renderer::new(&mut table, &mut viewer) 137 | /// .with_translator(Arc::new(EsEsTranslator)); 138 | /// ``` 139 | #[cfg(not(doctest))] 140 | pub fn with_translator(mut self, translator: Arc) -> Self { 141 | self.translator = translator; 142 | self 143 | } 144 | 145 | pub fn show(self, ui: &mut egui::Ui) -> Response { 146 | egui::ScrollArea::horizontal() 147 | .show(ui, |ui| self.impl_show(ui)) 148 | .inner 149 | } 150 | 151 | fn impl_show(mut self, ui: &mut egui::Ui) -> Response { 152 | let ctx = &ui.ctx().clone(); 153 | let ui_id = ui.id(); 154 | let style = ui.style().clone(); 155 | let painter = ui.painter().clone(); 156 | let visual = &style.visuals; 157 | let viewer = &mut *self.viewer; 158 | let s = self.state.as_mut().unwrap(); 159 | let mut resp_total = None::; 160 | let mut resp_ret = None::; 161 | let mut commands = Vec::>::new(); 162 | let ui_layer_id = ui.layer_id(); 163 | 164 | // NOTE: unlike RED and YELLOW which can be acquirable through 'error_bg_color' and 165 | // 'warn_bg_color', there's no 'green' color which can be acquired from inherent theme. 166 | // Following logic simply gets 'green' color from current background's brightness. 167 | let green = if visual.window_fill.g() > 128 { 168 | Color32::DARK_GREEN 169 | } else { 170 | Color32::GREEN 171 | }; 172 | 173 | let mut builder = egui_extras::TableBuilder::new(ui).column(Column::auto()); 174 | 175 | let iter_vis_cols_with_flag = s 176 | .vis_cols() 177 | .iter() 178 | .enumerate() 179 | .map(|(index, column)| (column, index + 1 == s.vis_cols().len())); 180 | 181 | for (column, flag) in iter_vis_cols_with_flag { 182 | builder = builder.column(viewer.column_render_config(column.0, flag)); 183 | } 184 | 185 | if replace(&mut s.cci_want_move_scroll, false) { 186 | let interact_row = s.interactive_cell().0; 187 | builder = builder.scroll_to_row(interact_row.0, None); 188 | } 189 | 190 | builder 191 | .columns(Column::auto(), s.num_columns() - s.vis_cols().len()) 192 | .drag_to_scroll(false) // Drag is used for selection; 193 | .striped(true) 194 | .cell_layout(egui::Layout::default().with_cross_align(self.style.cell_align)) 195 | .max_scroll_height(f32::MAX) 196 | .auto_shrink(self.style.auto_shrink) 197 | .scroll_bar_visibility(self.style.scroll_bar_visibility) 198 | .sense(Sense::click_and_drag().tap_mut(|s| s.set(Sense::FOCUSABLE, true))) 199 | .header(20., |mut h| { 200 | h.col(|_ui| { 201 | // TODO: Add `Configure Sorting` button 202 | }); 203 | 204 | let has_any_hidden_col = s.vis_cols().len() != s.num_columns(); 205 | 206 | for (vis_col, &col) in s.vis_cols().iter().enumerate() { 207 | let vis_col = VisColumnPos(vis_col); 208 | let mut painter = None; 209 | let (col_rect, resp) = h.col(|ui| { 210 | egui::Sides::new().show(ui, |ui| { 211 | ui.add(Label::new(viewer.column_name(col.0)) 212 | .selectable(false) 213 | ); 214 | }, |ui|{ 215 | if let Some(pos) = s.sort().iter().position(|(c, ..)| c == &col) { 216 | let is_asc = s.sort()[pos].1 .0 as usize; 217 | 218 | ui.colored_label( 219 | [green, Color32::RED][is_asc], 220 | RichText::new(format!("{}{}", ["↘", "↗"][is_asc], pos + 1,)) 221 | .monospace(), 222 | ); 223 | } else { 224 | // calculate the maximum width for the sort indicator 225 | let max_sort_indicator_width = (s.num_columns() + 1).to_string().len() + 1; 226 | // when the sort indicator is present, create a label the same size as the sort indicator 227 | // so that the columns don't resize when sorted. 228 | ui.add(Label::new(RichText::new(" ".repeat(max_sort_indicator_width)).monospace()).selectable(false)); 229 | } 230 | }); 231 | 232 | painter = Some(ui.painter().clone()); 233 | }); 234 | 235 | // Set drag payload for column reordering. 236 | resp.dnd_set_drag_payload(vis_col); 237 | 238 | if resp.dragged() { 239 | Tooltip::always_open(ctx.clone(), ui_layer_id, "_EGUI_DATATABLE__COLUMN_MOVE__".into(), PopupAnchor::Pointer) 240 | .gap(12.0) 241 | .show(|ui|{ 242 | let colum_name = viewer.column_name(col.0); 243 | ui.label(colum_name); 244 | }); 245 | } 246 | 247 | if resp.hovered() && viewer.is_sortable_column(col.0) { 248 | if let Some(p) = &painter { 249 | p.rect_filled( 250 | col_rect, 251 | egui::CornerRadius::ZERO, 252 | visual.selection.bg_fill.gamma_multiply(0.2), 253 | ); 254 | } 255 | } 256 | 257 | if viewer.is_sortable_column(col.0) && resp.clicked_by(PointerButton::Primary) { 258 | let mut sort = s.sort().to_owned(); 259 | match sort.iter_mut().find(|(c, ..)| c == &col) { 260 | Some((_, asc)) => match asc.0 { 261 | true => asc.0 = false, 262 | false => sort.retain(|(c, ..)| c != &col), 263 | }, 264 | None => { 265 | sort.push((col, IsAscending(true))); 266 | } 267 | } 268 | 269 | commands.push(Command::SetColumnSort(sort)); 270 | } 271 | 272 | if resp.dnd_hover_payload::().is_some() { 273 | if let Some(p) = &painter { 274 | p.rect_filled( 275 | col_rect, 276 | egui::CornerRadius::ZERO, 277 | visual.selection.bg_fill.gamma_multiply(0.5), 278 | ); 279 | } 280 | } 281 | 282 | if let Some(payload) = resp.dnd_release_payload::() { 283 | commands.push(Command::CcReorderColumn { 284 | from: *payload, 285 | to: vis_col 286 | .0 287 | .pipe(|v| v + (payload.0 < v) as usize) 288 | .pipe(VisColumnPos), 289 | }) 290 | } 291 | 292 | resp.context_menu(|ui| { 293 | if ui.button(self.translator.translate("context-menu-hide")).clicked() { 294 | commands.push(Command::CcHideColumn(col)); 295 | } 296 | 297 | if !s.sort().is_empty() && ui.button(self.translator.translate("context-menu-clear-sort")).clicked() { 298 | commands.push(Command::SetColumnSort(Vec::new())); 299 | } 300 | 301 | if has_any_hidden_col { 302 | ui.separator(); 303 | ui.label(self.translator.translate("context-menu-hidden")); 304 | 305 | for col in (0..s.num_columns()).map(ColumnIdx) { 306 | if !s.vis_cols().contains(&col) 307 | && ui.button(viewer.column_name(col.0)).clicked() 308 | { 309 | commands.push(Command::CcShowColumn { 310 | what: col, 311 | at: vis_col, 312 | }); 313 | } 314 | } 315 | } 316 | }); 317 | } 318 | 319 | // Account for header response to calculate total response. 320 | resp_total = Some(h.response()); 321 | }) 322 | .tap_mut(|table| { 323 | table.ui_mut().separator(); 324 | }) 325 | .body(|body: egui_extras::TableBody<'_>| { 326 | resp_ret = Some( 327 | self.impl_show_body(body, painter, commands, ctx, &style, ui_id, resp_total), 328 | ); 329 | }); 330 | 331 | resp_ret.unwrap_or_else(|| ui.label("??")) 332 | } 333 | 334 | #[allow(clippy::too_many_arguments)] 335 | fn impl_show_body( 336 | &mut self, 337 | body: egui_extras::TableBody<'_>, 338 | mut _painter: egui::Painter, 339 | mut commands: Vec>, 340 | ctx: &egui::Context, 341 | style: &egui::Style, 342 | ui_id: egui::Id, 343 | mut resp_total: Option, 344 | ) -> Response { 345 | let viewer = &mut *self.viewer; 346 | let s = self.state.as_mut().unwrap(); 347 | let table = &mut *self.table; 348 | let visual = &style.visuals; 349 | let visible_cols = s.vis_cols().clone(); 350 | let no_rounding = egui::CornerRadius::ZERO; 351 | 352 | let mut actions = Vec::::new(); 353 | let mut edit_started = false; 354 | let hotkeys = viewer.hotkeys(&s.ui_action_context()); 355 | 356 | // Preemptively consume all hotkeys. 357 | 'detect_hotkey: { 358 | // Detect hotkey inputs only when the table has focus. While editing, let the 359 | // editor consume input. 360 | if !s.cci_has_focus { 361 | break 'detect_hotkey; 362 | } 363 | 364 | if !s.is_editing() { 365 | ctx.input_mut(|i| { 366 | i.events.retain(|x| { 367 | match x { 368 | Event::Copy => actions.push(UiAction::CopySelection), 369 | Event::Cut => actions.push(UiAction::CutSelection), 370 | 371 | // Try to parse clipboard contents and detect if it's compatible 372 | // with cells being pasted. 373 | Event::Paste(clipboard) => { 374 | if !clipboard.is_empty() { 375 | // If system clipboard is not empty, try to update the internal 376 | // clipboard with system clipboard content before applying 377 | // paste operation. 378 | s.try_update_clipboard_from_string(viewer, clipboard); 379 | } 380 | 381 | if i.modifiers.shift { 382 | if viewer.allow_row_insertions() { 383 | actions.push(UiAction::PasteInsert) 384 | } 385 | } else { 386 | actions.push(UiAction::PasteInPlace) 387 | } 388 | } 389 | 390 | _ => return true, 391 | } 392 | false 393 | }) 394 | }); 395 | } 396 | 397 | for (hotkey, action) in &hotkeys { 398 | ctx.input_mut(|inp| { 399 | if inp.consume_shortcut(hotkey) { 400 | actions.push(*action); 401 | } 402 | }) 403 | } 404 | } 405 | 406 | // Validate persistency state. 407 | #[cfg(feature = "persistency")] 408 | if viewer.persist_ui_state() { 409 | s.validate_persistency(ctx, ui_id, viewer); 410 | } 411 | 412 | // Validate ui state. Defer this as late as possible; since it may not be 413 | // called if the table area is out of the visible space. 414 | s.validate_cc(&mut table.rows, viewer); 415 | 416 | // Checkout `cc_rows` to satisfy borrow checker. We need to access to 417 | // state mutably within row rendering; therefore, we can't simply borrow 418 | // `cc_rows` during the whole logic! 419 | let cc_row_heights = take(&mut s.cc_row_heights); 420 | 421 | let mut row_height_updates = Vec::new(); 422 | let vis_row_digits = s.cc_rows.len().max(1).ilog10(); 423 | let row_id_digits = table.len().max(1).ilog10(); 424 | 425 | let body_max_rect = body.max_rect(); 426 | let has_any_sort = !s.sort().is_empty(); 427 | 428 | let pointer_interact_pos = ctx.input(|i| i.pointer.latest_pos().unwrap_or_default()); 429 | let pointer_primary_down = ctx.input(|i| i.pointer.button_down(PointerButton::Primary)); 430 | 431 | s.cci_page_row_count = 0; 432 | 433 | /* ----------------------------- Primary Rendering Function ----------------------------- */ 434 | // - Extracted as a closure to differentiate behavior based on row height 435 | // configuration. (heterogeneous or homogeneous row heights) 436 | 437 | let render_fn = |mut row: egui_extras::TableRow| { 438 | s.cci_page_row_count += 1; 439 | 440 | let vis_row = VisRowPos(row.index()); 441 | let row_id = s.cc_rows[vis_row.0]; 442 | let prev_row_height = cc_row_heights[vis_row.0]; 443 | 444 | let mut row_elem_start = Default::default(); 445 | 446 | // Check if current row is edition target 447 | let edit_state = s.row_editing_cell(row_id); 448 | let mut editing_cell_rect = Rect::NOTHING; 449 | let interactive_row = s.is_interactive_row(vis_row); 450 | 451 | let check_mouse_dragging_selection = { 452 | let s_cci_has_focus = s.cci_has_focus; 453 | let s_cci_has_selection = s.has_cci_selection(); 454 | 455 | move |rect: &Rect, resp: &egui::Response| { 456 | let cci_hovered: bool = s_cci_has_focus 457 | && s_cci_has_selection 458 | && rect 459 | .with_max_x(resp.rect.right()) 460 | .contains(pointer_interact_pos); 461 | let sel_drag = cci_hovered && pointer_primary_down; 462 | let sel_click = !s_cci_has_selection && resp.hovered() && pointer_primary_down; 463 | 464 | sel_drag || sel_click 465 | } 466 | }; 467 | 468 | /* -------------------------------- Header Rendering -------------------------------- */ 469 | 470 | // Mark row background filled if being edited. 471 | row.set_selected(edit_state.is_some()); 472 | 473 | // Render row header button 474 | let (head_rect, head_resp) = row.col(|ui| { 475 | // Calculate the position where values start. 476 | row_elem_start = ui.max_rect().right_top(); 477 | 478 | ui.with_layout(Layout::right_to_left(Align::Center), |ui| { 479 | ui.separator(); 480 | 481 | if has_any_sort { 482 | ui.monospace( 483 | RichText::from(f!( 484 | "{:·>width$}", 485 | row_id.0, 486 | width = row_id_digits as usize 487 | )) 488 | .strong(), 489 | ); 490 | } else { 491 | ui.monospace( 492 | RichText::from(f!("{:>width$}", "", width = row_id_digits as usize)) 493 | .strong(), 494 | ); 495 | } 496 | 497 | ui.monospace( 498 | RichText::from(f!( 499 | "{:·>width$}", 500 | vis_row.0 + 1, 501 | width = vis_row_digits as usize 502 | )) 503 | .weak(), 504 | ); 505 | }); 506 | }); 507 | 508 | if check_mouse_dragging_selection(&head_rect, &head_resp) { 509 | s.cci_sel_update_row(vis_row); 510 | } 511 | 512 | /* -------------------------------- Columns Rendering ------------------------------- */ 513 | 514 | // Overridable maximum height 515 | let mut new_maximum_height = 0.; 516 | 517 | // Render cell contents regardless of the edition state. 518 | for (vis_col, col) in visible_cols.iter().enumerate() { 519 | let vis_col = VisColumnPos(vis_col); 520 | let linear_index = vis_row.linear_index(visible_cols.len(), vis_col); 521 | let selected = s.is_selected(vis_row, vis_col); 522 | let cci_selected = s.is_selected_cci(vis_row, vis_col); 523 | let is_editing = edit_state.is_some(); 524 | let is_interactive_cell = interactive_row.is_some_and(|x| x == vis_col); 525 | let mut response_consumed = s.is_editing(); 526 | 527 | let (rect, resp) = row.col(|ui| { 528 | let ui_max_rect = ui.max_rect(); 529 | 530 | if cci_selected { 531 | ui.painter().rect_stroke( 532 | ui_max_rect, 533 | no_rounding, 534 | Stroke { 535 | width: 2., 536 | color: self 537 | .style 538 | .fg_drag_selection 539 | .unwrap_or(visual.selection.bg_fill), 540 | }, 541 | StrokeKind::Inside, 542 | ); 543 | } 544 | 545 | if is_interactive_cell { 546 | ui.painter().rect_filled( 547 | ui_max_rect.expand(2.), 548 | no_rounding, 549 | self.style 550 | .bg_selected_highlight_cell 551 | .unwrap_or(visual.selection.bg_fill), 552 | ); 553 | } else if selected { 554 | ui.painter().rect_filled( 555 | ui_max_rect.expand(1.), 556 | no_rounding, 557 | self.style 558 | .bg_selected_cell 559 | .unwrap_or(visual.selection.bg_fill.gamma_multiply(0.5)), 560 | ); 561 | } 562 | 563 | // Actual widget rendering happens within this line. 564 | 565 | // ui.set_enabled(false); 566 | ui.style_mut() 567 | .visuals 568 | .widgets 569 | .noninteractive 570 | .fg_stroke 571 | .color = if is_interactive_cell { 572 | self.style 573 | .fg_selected_highlight_cell 574 | .unwrap_or(visual.strong_text_color()) 575 | } else { 576 | visual.strong_text_color() 577 | }; 578 | 579 | // FIXME: After egui 0.27, now the widgets spawned inside this closure 580 | // intercepts interactions, which is basically natural behavior(Upper layer 581 | // widgets). However, this change breaks current implementation which relies on 582 | // the previous table behavior. 583 | ui.add_enabled_ui(false, |ui| { 584 | if !(is_editing && is_interactive_cell) { 585 | viewer.show_cell_view(ui, &table.rows[row_id.0], col.0); 586 | } 587 | }); 588 | 589 | #[cfg(any())] 590 | if selected { 591 | ui.painter().rect_stroke( 592 | ui_max_rect, 593 | no_rounding, 594 | Stroke { 595 | width: 1., 596 | color: visual.weak_text_color(), 597 | }, 598 | ); 599 | } 600 | 601 | if interactive_row.is_some() && !is_editing { 602 | let st = Stroke { 603 | width: 1., 604 | color: self 605 | .style 606 | .focused_row_stroke 607 | .unwrap_or(visual.warn_fg_color.gamma_multiply(0.5)), 608 | }; 609 | 610 | let xr = ui_max_rect.x_range(); 611 | let yr = ui_max_rect.y_range(); 612 | ui.painter().hline(xr, yr.min, st); 613 | ui.painter().hline(xr, yr.max, st); 614 | } 615 | 616 | if edit_state.is_some_and(|(_, vis)| vis == vis_col) { 617 | editing_cell_rect = ui_max_rect; 618 | } 619 | }); 620 | 621 | new_maximum_height = rect.height().max(new_maximum_height); 622 | 623 | // -- Mouse Actions -- 624 | if check_mouse_dragging_selection(&rect, &resp) { 625 | // Expand cci selection 626 | response_consumed = true; 627 | s.cci_sel_update(linear_index); 628 | } 629 | 630 | let editable = viewer.is_editable_cell(vis_col.0, vis_row.0, &table.rows[row_id.0]); 631 | 632 | if editable 633 | && (resp.clicked_by(PointerButton::Primary) 634 | && (self.style.single_click_edit_mode || is_interactive_cell)) 635 | { 636 | response_consumed = true; 637 | commands.push(Command::CcEditStart( 638 | row_id, 639 | vis_col, 640 | viewer.clone_row(&table.rows[row_id.0]).into(), 641 | )); 642 | edit_started = true; 643 | } 644 | 645 | /* --------------------------- Context Menu Rendering --------------------------- */ 646 | 647 | (resp.clone() | head_resp.clone()).context_menu(|ui| { 648 | response_consumed = true; 649 | ui.set_min_size(egui::vec2(250., 10.)); 650 | 651 | if !selected { 652 | commands.push(Command::CcSetSelection(vec![VisSelection( 653 | linear_index, 654 | linear_index, 655 | )])); 656 | } else if !is_interactive_cell { 657 | s.set_interactive_cell(vis_row, vis_col); 658 | } 659 | 660 | let sel_multi_row = s.cursor_as_selection().is_some_and(|sel| { 661 | let mut min = usize::MAX; 662 | let mut max = usize::MIN; 663 | 664 | for sel in sel { 665 | min = min.min(sel.0 .0); 666 | max = max.max(sel.1 .0); 667 | } 668 | 669 | let (r_min, _) = VisLinearIdx(min).row_col(s.vis_cols().len()); 670 | let (r_max, _) = VisLinearIdx(max).row_col(s.vis_cols().len()); 671 | 672 | r_min != r_max 673 | }); 674 | 675 | let cursor_x = ui.cursor().min.x; 676 | let clip = s.has_clipboard_contents(); 677 | let b_undo = s.has_undo(); 678 | let b_redo = s.has_redo(); 679 | let mut n_sep_menu = 0; 680 | let mut draw_sep = false; 681 | 682 | let context_menu_items = [ 683 | Some((selected, "🖻", "context-menu-selection-copy", UiAction::CopySelection)), 684 | Some((selected, "🖻", "context-menu-selection-cut", UiAction::CutSelection)), 685 | Some((selected, "🗙", "context-menu-selection-clear", UiAction::DeleteSelection)), 686 | Some(( 687 | sel_multi_row, 688 | "🗐", 689 | "context-menu-selection-fill", 690 | UiAction::SelectionDuplicateValues, 691 | )), 692 | None, 693 | Some((clip, "➿", "context-menu-clipboard-paste", UiAction::PasteInPlace)), 694 | Some(( 695 | clip && viewer.allow_row_insertions(), 696 | "🛠", 697 | "context-menu-clipboard-insert", 698 | UiAction::PasteInsert, 699 | )), 700 | None, 701 | Some(( 702 | viewer.allow_row_insertions(), 703 | "🗐", 704 | "context-menu-row-duplicate", 705 | UiAction::DuplicateRow, 706 | )), 707 | Some(( 708 | viewer.allow_row_deletions(), 709 | "🗙", 710 | "context-menu-row-delete", 711 | UiAction::DeleteRow, 712 | )), 713 | None, 714 | Some((b_undo, "⎗", "context-menu-undo", UiAction::Undo)), 715 | Some((b_redo, "⎘", "context-menu-redo", UiAction::Redo)), 716 | ]; 717 | 718 | context_menu_items.map(|opt| { 719 | if let Some((icon, key, action)) = 720 | opt.filter(|x| x.0).map(|x| (x.1, x.2, x.3)) 721 | { 722 | if draw_sep { 723 | draw_sep = false; 724 | ui.separator(); 725 | } 726 | 727 | let hotkey = hotkeys 728 | .iter() 729 | .find_map(|(k, a)| (a == &action).then(|| ctx.format_shortcut(k))); 730 | 731 | ui.horizontal(|ui| { 732 | ui.monospace(icon); 733 | ui.add_space(cursor_x + 20. - ui.cursor().min.x); 734 | 735 | let label = self.translator.translate(key); 736 | let btn = egui::Button::new(label) 737 | .shortcut_text(hotkey.unwrap_or_else(|| "🗙".into())); 738 | let r = ui.centered_and_justified(|ui| ui.add(btn)).inner; 739 | 740 | if r.clicked() { 741 | actions.push(action); 742 | } 743 | }); 744 | 745 | n_sep_menu += 1; 746 | } else if n_sep_menu > 0 { 747 | n_sep_menu = 0; 748 | draw_sep = true; 749 | } 750 | }); 751 | }); 752 | 753 | // Forward DnD event if not any event was consumed by the response. 754 | 755 | // FIXME: Upgrading egui 0.29 make interaction rectangle of response object 756 | // larger(in y axis) than actually visible column cell size. To deal with this, 757 | // I've used returned content area rectangle instead, expanding its width to 758 | // response size. 759 | 760 | let drop_area_rect = rect.with_max_x(resp.rect.max.x); 761 | let contains_pointer = ctx 762 | .pointer_hover_pos() 763 | .is_some_and(|pos| drop_area_rect.contains(pos)); 764 | 765 | if !response_consumed && contains_pointer { 766 | if let Some(new_value) = 767 | viewer.on_cell_view_response(&table.rows[row_id.0], col.0, &resp) 768 | { 769 | let mut values = vec![(row_id, *col, RowSlabIndex(0))]; 770 | 771 | values.retain(|(row, col, _slab_id)| { 772 | viewer.is_editable_cell(col.0, row.0, &table.rows[row.0]) 773 | }); 774 | 775 | commands.push(Command::SetCells { 776 | slab: vec![*new_value].into_boxed_slice(), 777 | values: values.into_boxed_slice(), 778 | }); 779 | } 780 | } 781 | } 782 | 783 | /* -------------------------------- Editor Rendering -------------------------------- */ 784 | if let Some((should_focus, vis_column)) = edit_state { 785 | let column = s.vis_cols()[vis_column.0]; 786 | 787 | egui::Window::new("") 788 | .id(ui_id.with(row_id).with(column)) 789 | .constrain_to(body_max_rect) 790 | .fixed_pos(editing_cell_rect.min) 791 | .auto_sized() 792 | .min_size(editing_cell_rect.size()) 793 | .max_width(editing_cell_rect.width()) 794 | .title_bar(false) 795 | .frame(egui::Frame::NONE.corner_radius(egui::CornerRadius::same(3))) 796 | .show(ctx, |ui| { 797 | ui.with_layout(Layout::top_down_justified(Align::LEFT), |ui| { 798 | if let Some(resp) = 799 | viewer.show_cell_editor(ui, s.unwrap_editing_row_data(), column.0) 800 | { 801 | if should_focus { 802 | resp.request_focus() 803 | } 804 | 805 | new_maximum_height = resp.rect.height().max(new_maximum_height); 806 | } else { 807 | commands.push(Command::CcCommitEdit); 808 | } 809 | }); 810 | }); 811 | } 812 | 813 | // Accumulate response 814 | if let Some(resp) = &mut resp_total { 815 | *resp = resp.union(row.response()); 816 | } else { 817 | resp_total = Some(row.response()); 818 | } 819 | 820 | // Update row height cache if necessary. 821 | if self.style.table_row_height.is_none() && prev_row_height != new_maximum_height { 822 | row_height_updates.push((vis_row, new_maximum_height)); 823 | } 824 | }; // ~ render_fn 825 | 826 | // Actual rendering 827 | if let Some(height) = self.style.table_row_height { 828 | body.rows(height, cc_row_heights.len(), render_fn); 829 | } else { 830 | body.heterogeneous_rows(cc_row_heights.iter().cloned(), render_fn); 831 | } 832 | 833 | /* ----------------------------------- Event Handling ----------------------------------- */ 834 | 835 | if ctx.input(|i| i.pointer.button_released(PointerButton::Primary)) { 836 | let mods = ctx.input(|i| i.modifiers); 837 | if let Some(sel) = s.cci_take_selection(mods).filter(|_| !edit_started) { 838 | commands.push(Command::CcSetSelection(sel)); 839 | } 840 | } 841 | 842 | // Control overall focus status. 843 | if let Some(resp) = resp_total.clone() { 844 | 845 | let clicked_elsewhere = resp.clicked_elsewhere(); 846 | // IMPORTANT: cannot use `resp.contains_pointer()` here 847 | let response_rect_contains_pointer = resp.rect.contains(pointer_interact_pos); 848 | 849 | if resp.clicked() | resp.dragged() { 850 | s.cci_has_focus = true; 851 | } else if clicked_elsewhere && !response_rect_contains_pointer { 852 | s.cci_has_focus = false; 853 | if s.is_editing() { 854 | commands.push(Command::CcCommitEdit) 855 | } 856 | } 857 | } 858 | 859 | // Check in borrowed `cc_rows` back to state. 860 | s.cc_row_heights = cc_row_heights.tap_mut(|values| { 861 | if !row_height_updates.is_empty() { 862 | ctx.request_repaint(); 863 | } 864 | 865 | for (row_index, row_height) in row_height_updates { 866 | values[row_index.0] = row_height; 867 | } 868 | }); 869 | 870 | // Handle queued actions 871 | commands.extend( 872 | actions 873 | .into_iter() 874 | .flat_map(|action| s.try_apply_ui_action(table, viewer, action)), 875 | ); 876 | 877 | // Handle queued commands 878 | for cmd in commands { 879 | match cmd { 880 | Command::CcUpdateSystemClipboard(new_content) => { 881 | ctx.copy_text(new_content); 882 | } 883 | cmd => { 884 | if matches!(cmd, Command::CcCommitEdit) { 885 | // If any commit action is detected, release any remaining focus. 886 | ctx.memory_mut(|x| { 887 | if let Some(fc) = x.focused() { 888 | x.surrender_focus(fc) 889 | } 890 | }); 891 | } 892 | 893 | s.push_new_command( 894 | table, 895 | viewer, 896 | cmd, 897 | if self.style.max_undo_history == 0 { 898 | 100 899 | } else { 900 | self.style.max_undo_history 901 | }, 902 | ); 903 | } 904 | } 905 | } 906 | 907 | // Total response 908 | resp_total.unwrap() 909 | } 910 | } 911 | 912 | impl> Drop for Renderer<'_, R, V> { 913 | fn drop(&mut self) { 914 | self.table.ui = self.state.take(); 915 | } 916 | } 917 | 918 | /* ------------------------------------------- Translations ------------------------------------- */ 919 | 920 | pub trait Translator { 921 | 922 | /// Translates a given key into its corresponding string representation. 923 | /// 924 | /// If the translation key is unknown, return the key as a [`String`] 925 | fn translate(&self, key: &str) -> String; 926 | } 927 | 928 | #[derive(Default)] 929 | pub struct EnglishTranslator {} 930 | 931 | impl Translator for EnglishTranslator { 932 | fn translate(&self, key: &str) -> String { 933 | match key { 934 | // cell context menu 935 | "context-menu-selection-copy" => "Selection: Copy", 936 | "context-menu-selection-cut" => "Selection: Cut", 937 | "context-menu-selection-clear" => "Selection: Clear", 938 | "context-menu-selection-fill" => "Selection: Fill", 939 | "context-menu-clipboard-paste" => "Clipboard: Paste", 940 | "context-menu-clipboard-insert" => "Clipboard: Insert", 941 | "context-menu-row-duplicate" => "Row: Duplicate", 942 | "context-menu-row-delete" => "Row: Delete", 943 | "context-menu-undo" => "Undo", 944 | "context-menu-redo" => "Redo", 945 | 946 | // column header context menu 947 | "context-menu-hide" => "Hide", 948 | "context-menu-hidden" => "Hidden", 949 | "context-menu-clear-sort" => "Clear sort", 950 | _ => key, 951 | }.to_string() 952 | } 953 | } 954 | -------------------------------------------------------------------------------- /src/draw/state.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::{BTreeMap, BTreeSet, VecDeque}, 3 | hash::{Hash, Hasher}, 4 | mem::{replace, take}, 5 | }; 6 | use std::collections::HashSet; 7 | use egui::{ 8 | ahash::{AHasher, HashMap, HashMapExt}, 9 | Modifiers, 10 | }; 11 | use itertools::Itertools; 12 | use tap::prelude::{Pipe, Tap}; 13 | 14 | use crate::{ 15 | default, 16 | draw::tsv, 17 | viewer::{ 18 | CellWriteContext, DecodeErrorBehavior, EmptyRowCreateContext, MoveDirection, RowCodec, 19 | UiActionContext, UiCursorState, 20 | }, 21 | DataTable, RowViewer, UiAction, 22 | }; 23 | 24 | macro_rules! int_ty { 25 | ( 26 | $(#[$meta:meta])* 27 | struct $name:ident ($($ty:ty),+); $($rest:tt)*) => { 28 | #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, PartialOrd, Ord)] 29 | $(#[$meta])* 30 | pub(crate) struct $name($(pub(in crate::draw) $ty),+); 31 | 32 | int_ty!($($rest)*); 33 | }; 34 | () => {} 35 | } 36 | 37 | int_ty!( 38 | struct VisLinearIdx(usize); 39 | struct VisSelection(VisLinearIdx, VisLinearIdx); 40 | struct RowSlabIndex(usize); 41 | 42 | struct RowIdx(usize); 43 | struct VisRowPos(usize); 44 | struct VisRowOffset(usize); 45 | struct VisColumnPos(usize); 46 | 47 | #[cfg_attr(feature = "persistency", derive(serde::Serialize, serde::Deserialize))] 48 | struct IsAscending(bool); 49 | #[cfg_attr(feature = "persistency", derive(serde::Serialize, serde::Deserialize))] 50 | struct ColumnIdx(usize); 51 | ); 52 | 53 | impl VisSelection { 54 | pub fn contains(&self, ncol: usize, row: VisRowPos, col: VisColumnPos) -> bool { 55 | let (top, left) = self.0.row_col(ncol); 56 | let (bottom, right) = self.1.row_col(ncol); 57 | 58 | row.0 >= top.0 && row.0 <= bottom.0 && col.0 >= left.0 && col.0 <= right.0 59 | } 60 | 61 | pub fn contains_rect(&self, ncol: usize, other: Self) -> bool { 62 | let (top, left) = self.0.row_col(ncol); 63 | let (bottom, right) = self.1.row_col(ncol); 64 | 65 | let (other_top, other_left) = other.0.row_col(ncol); 66 | let (other_bottom, other_right) = other.1.row_col(ncol); 67 | 68 | other_top.0 >= top.0 69 | && other_bottom.0 <= bottom.0 70 | && other_left.0 >= left.0 71 | && other_right.0 <= right.0 72 | } 73 | 74 | pub fn from_points(ncol: usize, a: VisLinearIdx, b: VisLinearIdx) -> Self { 75 | let (a_r, a_c) = a.row_col(ncol); 76 | let (b_r, b_c) = b.row_col(ncol); 77 | 78 | let top = a_r.0.min(b_r.0); 79 | let bottom = a_r.0.max(b_r.0); 80 | let left = a_c.0.min(b_c.0); 81 | let right = a_c.0.max(b_c.0); 82 | 83 | Self( 84 | VisLinearIdx(top * ncol + left), 85 | VisLinearIdx(bottom * ncol + right), 86 | ) 87 | } 88 | 89 | pub fn is_point(&self) -> bool { 90 | self.0 == self.1 91 | } 92 | 93 | pub fn union(&self, ncol: usize, other: Self) -> Self { 94 | let (top, left) = self.0.row_col(ncol); 95 | let (bottom, right) = self.1.row_col(ncol); 96 | 97 | let (other_top, other_left) = other.0.row_col(ncol); 98 | let (other_bottom, other_right) = other.1.row_col(ncol); 99 | 100 | let top = top.0.min(other_top.0); 101 | let left = left.0.min(other_left.0); 102 | let bottom = bottom.0.max(other_bottom.0); 103 | let right = right.0.max(other_right.0); 104 | 105 | Self( 106 | VisLinearIdx(top * ncol + left), 107 | VisLinearIdx(bottom * ncol + right), 108 | ) 109 | } 110 | 111 | pub fn _from_row_col(ncol: usize, r: VisRowPos, c: VisColumnPos) -> Self { 112 | r.linear_index(ncol, c).pipe(|idx| Self(idx, idx)) 113 | } 114 | } 115 | 116 | impl From for VisSelection { 117 | fn from(value: VisLinearIdx) -> Self { 118 | Self(value, value) 119 | } 120 | } 121 | 122 | impl VisLinearIdx { 123 | pub fn row_col(&self, ncol: usize) -> (VisRowPos, VisColumnPos) { 124 | let (row, col) = (self.0 / ncol, self.0 % ncol); 125 | (VisRowPos(row), VisColumnPos(col)) 126 | } 127 | } 128 | 129 | impl VisRowPos { 130 | pub fn linear_index(&self, ncol: usize, col: VisColumnPos) -> VisLinearIdx { 131 | VisLinearIdx(self.0 * ncol + col.0) 132 | } 133 | } 134 | 135 | /// TODO: Serialization? 136 | pub(crate) struct UiState { 137 | /// Type id of the viewer. 138 | viewer_type: std::any::TypeId, 139 | 140 | /// Unique hash of the viewer. This is to prevent cache invalidation when the viewer 141 | /// state is changed. 142 | viewer_filter_hash: u64, 143 | 144 | /// Undo queue. 145 | /// 146 | /// - Push tasks front of the queue. 147 | /// - Drain all elements from `0..undo_cursor` 148 | /// - Pop overflow elements from the back. 149 | undo_queue: VecDeque>, 150 | 151 | /// Undo cursor => increment by 1 on every undo, decrement by 1 on redo. 152 | undo_cursor: usize, 153 | 154 | /// Clipboard contents. 155 | /// 156 | /// XXX: Should we move this into global storage? 157 | clipboard: Option>, 158 | 159 | /// Persistent data 160 | p: PersistData, 161 | 162 | #[cfg(feature = "persistency")] 163 | is_p_loaded: bool, 164 | 165 | /* 166 | 167 | SECTION: Cache - Rendering 168 | 169 | */ 170 | /// Cached rows. Vector index is `VisRowPos`. Tuple is (row_id, 171 | /// cached_row_display_height) 172 | pub cc_rows: Vec, 173 | 174 | /// Cached row heights. Vector index is `VisRowPos`. 175 | /// 176 | /// WARNING: DO NOT ACCESS THIS DURING RENDERING; as it's taken out for heterogenous 177 | /// row height support, therefore invalid during table rendering. 178 | pub cc_row_heights: Vec, 179 | 180 | /// Cached row id to visual row position table for quick lookup. 181 | cc_row_id_to_vis: HashMap, 182 | 183 | /// Spreadsheet is modified during the last validation. 184 | cc_dirty: bool, 185 | 186 | /// Row selections. First element's top-left corner is always 'highlight' row if 187 | /// editing row isn't present. 188 | cc_cursor: CursorState, 189 | 190 | /// Number of frames from the last edit. Used to validate sorting. 191 | cc_num_frame_from_last_edit: usize, 192 | 193 | /// Cached previous number of columns. 194 | cc_prev_n_columns: usize, 195 | 196 | /// Latest interactive cell; Used for keyboard navigation. 197 | cc_interactive_cell: VisLinearIdx, 198 | 199 | /// Desired selection of next validation 200 | cc_desired_selection: Option)>>, 201 | 202 | /* 203 | 204 | SECTION: Cache - Input Status 205 | 206 | */ 207 | /// (Pivot, Current) selection. 208 | cci_selection: Option<(VisLinearIdx, VisLinearIdx)>, 209 | 210 | /// We have latest click. 211 | pub cci_has_focus: bool, 212 | 213 | /// Interface wants to scroll to the row. 214 | pub cci_want_move_scroll: bool, 215 | 216 | /// How many rows are rendered at once recently? 217 | pub cci_page_row_count: usize, 218 | } 219 | 220 | #[cfg_attr(feature = "persistency", derive(serde::Serialize, serde::Deserialize))] 221 | #[derive(Clone, Default)] 222 | struct PersistData { 223 | /// Cached number of columns. 224 | num_columns: usize, 225 | 226 | /// Visible columns selected by user. 227 | vis_cols: Vec, 228 | 229 | /// Column sorting state. 230 | sort: Vec<(ColumnIdx, IsAscending)>, 231 | } 232 | 233 | struct Clipboard { 234 | slab: Box<[R]>, 235 | 236 | /// The first tuple element `VisRowPos` is offset from the top-left corner of the 237 | /// selection. 238 | pastes: Box<[(VisRowOffset, ColumnIdx, RowSlabIndex)]>, 239 | } 240 | 241 | impl Clipboard { 242 | pub fn sort(&mut self) { 243 | self.pastes 244 | .sort_by(|(a_row, a_col, ..), (b_row, b_col, ..)| { 245 | a_row.0.cmp(&b_row.0).then(a_col.0.cmp(&b_col.0)) 246 | }) 247 | } 248 | } 249 | 250 | struct UndoArg { 251 | apply: Command, 252 | restore: Vec>, 253 | } 254 | 255 | impl Default for UiState { 256 | fn default() -> Self { 257 | Self { 258 | viewer_filter_hash: 0, 259 | clipboard: None, 260 | viewer_type: std::any::TypeId::of::<()>(), 261 | cc_cursor: CursorState::Select(default()), 262 | undo_queue: VecDeque::new(), 263 | cc_rows: Vec::new(), 264 | cc_row_heights: Vec::new(), 265 | cc_dirty: false, 266 | undo_cursor: 0, 267 | cci_selection: None, 268 | cci_has_focus: false, 269 | cc_interactive_cell: VisLinearIdx(0), 270 | cc_row_id_to_vis: default(), 271 | cc_num_frame_from_last_edit: 0, 272 | cc_prev_n_columns: 0, 273 | cc_desired_selection: None, 274 | cci_want_move_scroll: false, 275 | cci_page_row_count: 0, 276 | p: default(), 277 | #[cfg(feature = "persistency")] 278 | is_p_loaded: false, 279 | } 280 | } 281 | } 282 | 283 | enum CursorState { 284 | Select(Vec), 285 | Edit { 286 | next_focus: bool, 287 | last_focus: VisColumnPos, 288 | row: RowIdx, 289 | edition: R, 290 | }, 291 | } 292 | 293 | impl UiState { 294 | pub fn cc_is_dirty(&self) -> bool { 295 | self.cc_dirty 296 | } 297 | 298 | pub fn validate_identity>(&mut self, vwr: &mut V) { 299 | let num_columns = vwr.num_columns(); 300 | let vwr_type_id = std::any::TypeId::of::(); 301 | let vwr_hash = AHasher::default().pipe(|mut hsh| { 302 | vwr.row_filter_hash().hash(&mut hsh); 303 | hsh.finish() 304 | }); 305 | 306 | // Check for nontrivial changes. 307 | if self.p.num_columns == num_columns && self.viewer_type == vwr_type_id { 308 | // Check for trivial changes which does not require total reconstruction of 309 | // UiState. 310 | 311 | // If viewer's filter is changed. It always invalidates current cache. 312 | if self.viewer_filter_hash != vwr_hash { 313 | self.viewer_filter_hash = vwr_hash; 314 | self.cc_dirty = true; 315 | } 316 | 317 | // Defer validation of cache if it's still editing. This is prevent annoying re-sort 318 | // during editing multiple cells in-a-row without escape from insertion mode. 319 | { 320 | if !self.is_editing() { 321 | self.cc_num_frame_from_last_edit += 1; 322 | } 323 | 324 | if self.cc_num_frame_from_last_edit == 2 { 325 | self.cc_dirty |= !self.p.sort.is_empty(); 326 | } 327 | } 328 | 329 | // Check if any sort config is invalidated. 330 | self.cc_dirty |= { 331 | let mut any_sort_invalidated = false; 332 | 333 | self.p.sort.retain(|(c, _)| { 334 | vwr.is_sortable_column(c.0) 335 | .tap(|x| any_sort_invalidated |= !x) 336 | }); 337 | 338 | any_sort_invalidated 339 | }; 340 | 341 | return; 342 | } 343 | 344 | // Clear the cache 345 | *self = Default::default(); 346 | self.viewer_type = vwr_type_id; 347 | self.viewer_filter_hash = vwr_hash; 348 | self.p.num_columns = num_columns; 349 | 350 | self.p.vis_cols.extend((0..num_columns).map(ColumnIdx)); 351 | self.cc_dirty = true; 352 | } 353 | 354 | #[cfg(feature = "persistency")] 355 | pub fn validate_persistency>( 356 | &mut self, 357 | ctx: &egui::Context, 358 | ui_id: egui::Id, 359 | vwr: &mut V, 360 | ) { 361 | if !self.is_p_loaded { 362 | // Load initial storage status 363 | self.is_p_loaded = true; 364 | self.cc_dirty = true; 365 | let p: PersistData = 366 | ctx.memory_mut(|m| m.data.get_persisted(ui_id).unwrap_or_default()); 367 | 368 | if p.num_columns == self.p.num_columns { 369 | // Data should only be copied when column count matches. Otherwise, we regard 370 | // stored column differs from the current. 371 | self.p = p; 372 | 373 | // Only retain valid sorting configuration. 374 | self.p.sort.retain(|(col, _)| vwr.is_sortable_column(col.0)); 375 | } 376 | } else if self.cc_dirty { 377 | // Copy current ui status into persistency storage. 378 | ctx.memory_mut(|m| m.data.insert_persisted(ui_id, self.p.clone())); 379 | } 380 | } 381 | 382 | pub fn validate_cc>(&mut self, rows: &mut [R], vwr: &mut V) { 383 | if !replace(&mut self.cc_dirty, false) { 384 | self.handle_desired_selection(); 385 | return; 386 | } 387 | 388 | // XXX: Boost performance with `rayon`? 389 | // - Returning `comparator` which is marked as `Sync` 390 | // - For this, `R` also need to be sent to multiple threads safely. 391 | // - Maybe we need specialization for `R: Send`? 392 | 393 | // We should validate the entire cache. 394 | self.cc_rows.clear(); 395 | self.cc_rows.extend( 396 | rows.iter() 397 | .enumerate() 398 | .filter_map(|(i, x)| vwr.filter_row(x).then_some(i)) 399 | .map(RowIdx), 400 | ); 401 | 402 | for (sort_col, asc) in self.p.sort.iter().rev() { 403 | self.cc_rows.sort_by(|a, b| { 404 | vwr.compare_cell(&rows[a.0], &rows[b.0], sort_col.0) 405 | .tap_mut(|x| { 406 | if !asc.0 { 407 | *x = x.reverse() 408 | } 409 | }) 410 | }); 411 | } 412 | 413 | // Just refill with neat default height. 414 | self.cc_row_heights.resize(self.cc_rows.len(), 20.0); 415 | 416 | self.cc_row_id_to_vis.clear(); 417 | self.cc_row_id_to_vis.extend( 418 | self.cc_rows 419 | .iter() 420 | .enumerate() 421 | .map(|(i, id)| (*id, VisRowPos(i))), 422 | ); 423 | 424 | if self.handle_desired_selection() { 425 | // no-op. 426 | } else if let CursorState::Select(cursor) = &mut self.cc_cursor { 427 | // Validate cursor range if it's still in range. 428 | 429 | let old_cols = self.cc_prev_n_columns; 430 | let new_rows = self.cc_rows.len(); 431 | let new_cols = self.p.num_columns; 432 | self.cc_prev_n_columns = self.p.num_columns; 433 | 434 | cursor.retain_mut(|sel| { 435 | let (old_min_r, old_min_c) = sel.0.row_col(old_cols); 436 | if old_min_r.0 >= new_rows || old_min_c.0 >= new_cols { 437 | return false; 438 | } 439 | 440 | let (mut old_max_r, mut old_max_c) = sel.1.row_col(old_cols); 441 | old_max_r.0 = old_max_r.0.min(new_rows.saturating_sub(1)); 442 | old_max_c.0 = old_max_c.0.min(new_cols.saturating_sub(1)); 443 | 444 | let min = old_min_r.linear_index(new_cols, old_min_c); 445 | let max = old_max_r.linear_index(new_cols, old_max_c); 446 | *sel = VisSelection(min, max); 447 | 448 | true 449 | }); 450 | } else { 451 | self.cc_cursor = CursorState::Select(Vec::default()); 452 | } 453 | 454 | // Prevent overflow. 455 | self.validate_interactive_cell(self.p.vis_cols.len()); 456 | } 457 | 458 | pub fn try_update_clipboard_from_string>( 459 | &mut self, 460 | vwr: &mut V, 461 | contents: &str, 462 | ) -> bool { 463 | /* 464 | NOTE: System clipboard implementation 465 | 466 | We can't just determine if the internal clipboard should be preferred over the system 467 | clipboard, as the source of clipboard contents can be vary. Therefore, on every copy 468 | operation, we should clear out the system clipboard unconditionally, then we tries to 469 | encode the copied content into clipboard. 470 | 471 | TODO: Need to find way to handle if both of system clipboard and internal clipboard 472 | content exist. We NEED to determine if which content should be applied for this. 473 | 474 | # Dumping 475 | 476 | - For rectangular(including single cell) selection of data, we'll just create 477 | appropriate sized small TSV data which suits within given range. 478 | - Note that this'll differentiate the clipboard behavior from internal-only 479 | version. 480 | - For non-rectangular selections, full-scale rectangular table is dumped which 481 | can cover all selection range including empty selections; where any data that 482 | is not being dumped is just emptied out. 483 | - From this, any data cell that is just 'empty' but selected, should be dumped 484 | as explicit empty data; in this case, empty data wrapped with double 485 | quotes(""). 486 | 487 | # Decoding 488 | 489 | - Every format is regarded as TSV. (only \t, \n matters) 490 | - For TSV data with same column count with this table 491 | - Parse as full-scale table, then put into clipboard as-is. 492 | - Column count is less than current table 493 | - In this case, current selection matters. 494 | - Offset the copied content table as the selection column offset. 495 | - Then create clipboard data from it. 496 | - If column count is larger than this, it is invalid data; we just skip parsing 497 | */ 498 | 499 | let Some(mut codec) = vwr.try_create_codec(false) else { 500 | // Even when there is system clipboard content, we're going to ignore it and use 501 | // internal clipboard if there's no way to parse it. 502 | return false; 503 | }; 504 | 505 | if let CursorState::Select(selections) = &self.cc_cursor { 506 | let Some(first) = selections.first().map(|x| x.0) else { 507 | // No selectgion present. Do nothing 508 | return false; 509 | }; 510 | 511 | let (.., col) = first.row_col(self.p.vis_cols.len()); 512 | col 513 | } else { 514 | // If there's no selection, we'll just ignore the system clipboard input 515 | return false; 516 | }; 517 | 518 | let selection_offset = if let CursorState::Select(sel) = &self.cc_cursor { 519 | sel.first().map_or(0, |idx| { 520 | let (_, c) = idx.0.row_col(self.p.vis_cols.len()); 521 | c.0 522 | }) 523 | } else { 524 | 0 525 | }; 526 | 527 | let view = tsv::ParsedTsv::parse(contents); 528 | let table_width = view.calc_table_width(); 529 | 530 | if table_width > self.p.vis_cols.len() { 531 | // If the copied data has more columns than current table, we'll just ignore it. 532 | return false; 533 | } 534 | 535 | // If any cell is failed to be parsed, we'll just give up all parsing then use internal 536 | // clipboard instead. 537 | 538 | let mut slab = Vec::new(); 539 | let mut pastes = Vec::new(); 540 | 541 | for (row_offset, row_data) in view.iter_rows() { 542 | let slab_id = slab.len(); 543 | slab.push(codec.create_empty_decoded_row()); 544 | 545 | // The restoration point of pastes stack. 546 | let pastes_restore = pastes.len(); 547 | 548 | for (column, data) in row_data { 549 | let col_idx = column + selection_offset; 550 | 551 | if col_idx > self.p.vis_cols.len() { 552 | // If the column is out of range, we'll just ignore it. 553 | return false; 554 | } 555 | 556 | match codec.decode_column(data, col_idx, &mut slab[slab_id]) { 557 | Ok(_) => { 558 | pastes.push(( 559 | VisRowOffset(row_offset), 560 | ColumnIdx(col_idx), 561 | RowSlabIndex(slab_id), 562 | )); 563 | } 564 | Err(DecodeErrorBehavior::SkipCell) => { 565 | // Skip this cell. 566 | } 567 | Err(DecodeErrorBehavior::SkipRow) => { 568 | pastes.drain(pastes_restore..); 569 | slab.pop(); 570 | break; 571 | } 572 | Err(DecodeErrorBehavior::Abort) => { 573 | return false; 574 | } 575 | } 576 | } 577 | } 578 | 579 | // Replace the clipboard content from the parsed data. 580 | self.clipboard = Some(Clipboard { 581 | slab: slab.into_boxed_slice(), 582 | pastes: pastes.into_boxed_slice(), 583 | }); 584 | 585 | true 586 | } 587 | 588 | fn try_dump_clipboard_content>( 589 | clipboard: &Clipboard, 590 | vwr: &mut V, 591 | ) -> Option { 592 | // clipboard MUST be sorted before dumping; XXX: add assertion? 593 | #[allow(unused_mut)] 594 | let mut codec = vwr.try_create_codec(true)?; 595 | 596 | let mut width = 0; 597 | let mut height = 0; 598 | 599 | // We're going to offset the column to the minimum column index to make the selection copy 600 | // more intuitive. If not, the copied data will be shifted to the right if the selection is 601 | // not the very first column. 602 | let mut min_column = usize::MAX; 603 | 604 | for (row, column, ..) in clipboard.pastes.iter() { 605 | width = width.max(column.0 + 1); 606 | height = height.max(row.0 + 1); 607 | min_column = min_column.min(column.0); 608 | } 609 | 610 | let column_offset = min_column; 611 | let mut buf_out = String::new(); 612 | let mut buf_tmp = String::new(); 613 | let mut row_cursor = 0; 614 | 615 | for (row, columns, ..) in &clipboard.pastes.iter().chunk_by(|(row, ..)| *row) { 616 | while row_cursor < row.0 { 617 | tsv::write_newline(&mut buf_out); 618 | row_cursor += 1; 619 | } 620 | 621 | let mut column_cursor = 0; 622 | 623 | for (_, column, data_idx) in columns.into_iter() { 624 | while column_cursor < column.0 - column_offset { 625 | tsv::write_tab(&mut buf_out); 626 | column_cursor += 1; 627 | } 628 | 629 | let data = &clipboard.slab[data_idx.0]; 630 | codec.encode_column(data, column.0, &mut buf_tmp); 631 | 632 | tsv::write_content(&mut buf_out, &buf_tmp); 633 | buf_tmp.clear(); 634 | } 635 | } 636 | 637 | Some(buf_out) 638 | } 639 | 640 | fn handle_desired_selection(&mut self) -> bool { 641 | let Some((next_sel, sel)) = self.cc_desired_selection.take().and_then(|x| { 642 | if let CursorState::Select(vec) = &mut self.cc_cursor { 643 | Some((x, vec)) 644 | } else { 645 | None 646 | } 647 | }) else { 648 | return false; 649 | }; 650 | 651 | // If there's any desired selections present for next validation, apply it. 652 | 653 | sel.clear(); 654 | let ncol = self.p.vis_cols.len(); 655 | 656 | for (row_id, columns) in next_sel { 657 | let vis_row = self.cc_row_id_to_vis[&row_id]; 658 | 659 | if columns.is_empty() { 660 | let p_left = vis_row.linear_index(ncol, VisColumnPos(0)); 661 | let p_right = vis_row.linear_index(ncol, VisColumnPos(ncol - 1)); 662 | 663 | sel.push(VisSelection(p_left, p_right)); 664 | } else { 665 | for col in columns { 666 | let Some(vis_c) = self.p.vis_cols.iter().position(|x| *x == col) else { 667 | continue; 668 | }; 669 | 670 | let p = vis_row.linear_index(ncol, VisColumnPos(vis_c)); 671 | sel.push(VisSelection(p, p)); 672 | } 673 | } 674 | } 675 | 676 | true 677 | } 678 | 679 | pub fn vis_cols(&self) -> &Vec { 680 | &self.p.vis_cols 681 | } 682 | 683 | pub fn force_mark_dirty(&mut self) { 684 | self.cc_dirty = true; 685 | } 686 | 687 | pub fn row_editing_cell(&mut self, row_id: RowIdx) -> Option<(bool, VisColumnPos)> { 688 | match &mut self.cc_cursor { 689 | CursorState::Edit { 690 | row, 691 | last_focus, 692 | next_focus, 693 | .. 694 | } if *row == row_id => Some((replace(next_focus, false), *last_focus)), 695 | _ => None, 696 | } 697 | } 698 | 699 | pub fn num_columns(&self) -> usize { 700 | self.p.num_columns 701 | } 702 | 703 | pub fn sort(&self) -> &[(ColumnIdx, IsAscending)] { 704 | &self.p.sort 705 | } 706 | 707 | pub fn unwrap_editing_row_data(&mut self) -> &mut R { 708 | match &mut self.cc_cursor { 709 | CursorState::Edit { edition, .. } => edition, 710 | _ => unreachable!(), 711 | } 712 | } 713 | 714 | pub fn is_editing(&self) -> bool { 715 | matches!(self.cc_cursor, CursorState::Edit { .. }) 716 | } 717 | 718 | pub fn is_selected(&self, row: VisRowPos, col: VisColumnPos) -> bool { 719 | if let CursorState::Select(selections) = &self.cc_cursor { 720 | selections 721 | .iter() 722 | .any(|sel| self.vis_sel_contains(*sel, row, col)) 723 | } else { 724 | false 725 | } 726 | } 727 | 728 | pub fn is_selected_cci(&self, row: VisRowPos, col: VisColumnPos) -> bool { 729 | self.cci_selection.is_some_and(|(pivot, current)| { 730 | self.vis_sel_contains( 731 | VisSelection::from_points(self.p.vis_cols.len(), pivot, current), 732 | row, 733 | col, 734 | ) 735 | }) 736 | } 737 | 738 | pub fn is_interactive_row(&self, row: VisRowPos) -> Option { 739 | let (r, c) = self.cc_interactive_cell.row_col(self.p.vis_cols.len()); 740 | (r == row).then_some(c) 741 | } 742 | 743 | pub fn interactive_cell(&self) -> (VisRowPos, VisColumnPos) { 744 | self.cc_interactive_cell.row_col(self.p.vis_cols.len()) 745 | } 746 | 747 | pub fn cci_sel_update(&mut self, current: VisLinearIdx) { 748 | if let Some((_, pivot)) = &mut self.cci_selection { 749 | *pivot = current; 750 | } else { 751 | self.cci_selection = Some((current, current)); 752 | } 753 | } 754 | 755 | pub fn cci_sel_update_row(&mut self, row: VisRowPos) { 756 | [0, self.p.vis_cols.len() - 1].map(|col| { 757 | self.cci_sel_update(row.linear_index(self.p.vis_cols.len(), VisColumnPos(col))) 758 | }); 759 | } 760 | 761 | pub fn has_cci_selection(&self) -> bool { 762 | self.cci_selection.is_some() 763 | } 764 | 765 | pub fn vis_sel_contains(&self, sel: VisSelection, row: VisRowPos, col: VisColumnPos) -> bool { 766 | sel.contains(self.p.vis_cols.len(), row, col) 767 | } 768 | 769 | fn get_highlight_changes<'a>( 770 | &'a self, 771 | table: &'a DataTable, 772 | sel: &[VisSelection], 773 | ) -> (Vec<&'a R>, Vec<&'a R>) { 774 | let mut ohs: BTreeSet<&VisSelection> = BTreeSet::default(); 775 | let nhs: BTreeSet<&VisSelection> = sel.iter().collect(); 776 | 777 | if let CursorState::Select(s) = &self.cc_cursor { 778 | ohs = s.iter().collect(); 779 | } 780 | 781 | // IMPORTANT the new highlight may include a selection that includes the old highlight selection 782 | // this happens when making a second multi-select using shift 783 | // e.g. old: 1..=5, 10..=10, new: 1..=5, 10..=15 784 | 785 | /// Flatten a set of ranges into a set of linear indices, e.g. [(1,3), (6,8)] -> [1,2,3,6,7,8] 786 | fn flatten_ranges(ranges: &[(usize, usize)]) -> HashSet { 787 | ranges.iter() 788 | .flat_map(|&(start, end)| start..=end) 789 | .collect() 790 | } 791 | 792 | /// Only keep elements in old_rows that are NOT in new_rows 793 | fn deselected_rows(old_rows: &HashSet, new_rows: &HashSet) -> Vec { 794 | let missing: Vec = old_rows 795 | .difference(&new_rows) 796 | .copied() 797 | .collect(); 798 | 799 | missing 800 | } 801 | 802 | let nhs_range = self.make_row_range(&nhs); 803 | let ohs_range = self.make_row_range(&ohs); 804 | 805 | let nhs_rows = flatten_ranges(&nhs_range); 806 | let ohs_rows = flatten_ranges(&ohs_range); 807 | 808 | let deselected_rows = deselected_rows(&ohs_rows, &nhs_rows); 809 | 810 | let highlighted: Vec<&R> = nhs_rows 811 | .into_iter() 812 | .sorted() 813 | .map(|r| { 814 | let row_id = self.cc_rows[r]; 815 | &table.rows[row_id.0] 816 | }) 817 | .collect(); 818 | let unhighlighted: Vec<&R> = deselected_rows 819 | .into_iter() 820 | .sorted() 821 | .map(|r| { 822 | let row_id = self.cc_rows[r]; 823 | &table.rows[row_id.0] 824 | }) 825 | .collect(); 826 | 827 | (highlighted, unhighlighted) 828 | } 829 | 830 | fn make_row_range<'a>(&'a self, nhs: &BTreeSet<&VisSelection>) -> Vec<(usize, usize)> { 831 | nhs.iter().map(|sel| { 832 | let (start_ic_r, _ic_c) = sel.0.row_col(self.p.vis_cols.len()); 833 | let (end_ic_r, _ic_c) = sel.1.row_col(self.p.vis_cols.len()); 834 | (start_ic_r.0, end_ic_r.0) 835 | }).collect::>() 836 | } 837 | 838 | pub fn push_new_command>( 839 | &mut self, 840 | table: &mut DataTable, 841 | vwr: &mut V, 842 | command: Command, 843 | capacity: usize, 844 | ) { 845 | if self.is_editing() && !matches!(command, Command::CcCancelEdit | Command::CcCommitEdit) { 846 | // If any non-editing command is pushed while editing, commit it first 847 | self.push_new_command(table, vwr, Command::CcCommitEdit, capacity); 848 | } 849 | 850 | // Generate redo argument from command 851 | let restore = match command { 852 | Command::CcHideColumn(column_idx) => { 853 | if self.p.vis_cols.len() == 1 { 854 | return; 855 | } 856 | 857 | let mut vis_cols = self.p.vis_cols.clone(); 858 | let idx = vis_cols.iter().position(|x| *x == column_idx).unwrap(); 859 | vis_cols.remove(idx); 860 | 861 | self.push_new_command(table, vwr, Command::SetVisibleColumns(vis_cols), capacity); 862 | return; 863 | } 864 | Command::CcShowColumn { what, at } => { 865 | assert!(self.p.vis_cols.iter().all(|x| *x != what)); 866 | 867 | let mut vis_cols = self.p.vis_cols.clone(); 868 | vis_cols.insert(at.0, what); 869 | 870 | self.push_new_command(table, vwr, Command::SetVisibleColumns(vis_cols), capacity); 871 | return; 872 | } 873 | Command::SetVisibleColumns(ref value) => { 874 | if self.p.vis_cols.iter().eq(value.iter()) { 875 | return; 876 | } 877 | 878 | vec![Command::SetVisibleColumns(self.p.vis_cols.clone())] 879 | } 880 | Command::CcReorderColumn { from, to } => { 881 | if from == to || to.0 > self.p.vis_cols.len() { 882 | // Reorder may deliver invalid parameter if there's multiple data 883 | // tables present at the same time; as the drag drop payload are 884 | // compatible between different tables... 885 | return; 886 | } 887 | 888 | let mut vis_cols = self.p.vis_cols.clone(); 889 | if from.0 < to.0 { 890 | vis_cols.insert(to.0, vis_cols[from.0]); 891 | vis_cols.remove(from.0); 892 | } else { 893 | vis_cols.remove(from.0).pipe(|x| vis_cols.insert(to.0, x)); 894 | } 895 | 896 | self.push_new_command(table, vwr, Command::SetVisibleColumns(vis_cols), capacity); 897 | return; 898 | } 899 | Command::CcEditStart(row_id, column_pos, current) => { 900 | // EditStart command is directly applied. 901 | self.cc_cursor = CursorState::Edit { 902 | edition: *current, 903 | next_focus: true, 904 | last_focus: column_pos, 905 | row: row_id, 906 | }; 907 | 908 | // Update interactive cell. 909 | self.cc_interactive_cell = 910 | self.cc_row_id_to_vis[&row_id].linear_index(self.p.vis_cols.len(), column_pos); 911 | 912 | // No redo argument is generated. 913 | return; 914 | } 915 | ref cmd @ (Command::CcCancelEdit | Command::CcCommitEdit) => { 916 | // This edition state become selection. Restorat 917 | let Some((row_id, edition, _)) = self.try_take_edition() else { 918 | return; 919 | }; 920 | 921 | if matches!(cmd, Command::CcCancelEdit) { 922 | // Cancellation does not affect to any state. 923 | return; 924 | } 925 | 926 | // Change command type of self. 927 | self.push_new_command( 928 | table, 929 | vwr, 930 | Command::SetRowValue(row_id, edition.into()), 931 | capacity, 932 | ); 933 | 934 | return; 935 | } 936 | 937 | Command::SetRowValue(row_id, _) => { 938 | vec![Command::SetRowValue( 939 | row_id, 940 | vwr.clone_row(&table.rows[row_id.0]).into(), 941 | )] 942 | } 943 | 944 | Command::CcSetCells { 945 | context, 946 | slab, 947 | values, 948 | } => { 949 | let mut values = values.to_vec(); 950 | 951 | values.retain(|(row, col, slab_id)| { 952 | 953 | if vwr.is_editable_cell(col.0, row.0, &table.rows[row.0]) { 954 | vwr.confirm_cell_write_by_ui( 955 | &table.rows[row.0], 956 | &slab[slab_id.0], 957 | col.0, 958 | context, 959 | ) 960 | } else { 961 | false 962 | } 963 | 964 | }); 965 | 966 | return self.push_new_command( 967 | table, 968 | vwr, 969 | Command::SetCells { 970 | slab, 971 | values: values.into_boxed_slice(), 972 | }, 973 | capacity, 974 | ); 975 | } 976 | 977 | Command::SetCells { ref values, .. } => { 978 | let mut keys = Vec::from_iter(values.iter().map(|(r, ..)| *r)); 979 | keys.dedup(); 980 | 981 | keys.iter() 982 | .map(|row_id| { 983 | Command::SetRowValue(*row_id, vwr.clone_row(&table.rows[row_id.0]).into()) 984 | }) 985 | .collect() 986 | } 987 | 988 | Command::SetColumnSort(ref sort) => { 989 | if self.p.sort.iter().eq(sort.iter()) { 990 | return; 991 | } 992 | 993 | vec![Command::SetColumnSort(self.p.sort.clone())] 994 | } 995 | Command::CcSetSelection(sel) => { 996 | if !sel.is_empty() { 997 | self.cc_interactive_cell = sel[0].0; 998 | 999 | let (ic_r, ic_c) = self.cc_interactive_cell.row_col(self.p.vis_cols.len()); 1000 | let row_id = self.cc_rows[ic_r.0]; 1001 | 1002 | let idx = self.vis_cols()[ic_c.0]; 1003 | 1004 | let row = &table.rows[row_id.0]; 1005 | 1006 | vwr.on_highlight_cell(row, idx.0); 1007 | } 1008 | 1009 | let (highlighted, unhighlighted) = self.get_highlight_changes(table, &sel); 1010 | vwr.on_highlight_change(&highlighted, &unhighlighted); 1011 | self.cc_cursor = CursorState::Select(sel); 1012 | return; 1013 | } 1014 | Command::InsertRows(pivot, ref values) => { 1015 | let values = (pivot.0..pivot.0 + values.len()).map(RowIdx).collect(); 1016 | vec![Command::RemoveRow(values)] 1017 | } 1018 | Command::RemoveRow(ref indices) => { 1019 | if indices.is_empty() { 1020 | // From various sources, it can be just 'empty' removal command 1021 | return; 1022 | } 1023 | 1024 | // Ensure indices are sorted. 1025 | debug_assert!(indices.windows(2).all(|x| x[0] < x[1])); 1026 | 1027 | // Collect contiguous chunks. 1028 | let mut chunks = vec![vec![indices[0]]]; 1029 | 1030 | for index in indices.windows(2) { 1031 | if index[0].0 + 1 == index[1].0 { 1032 | chunks.last_mut().unwrap().push(index[1]); 1033 | } else { 1034 | chunks.push(vec![index[1]]); 1035 | } 1036 | } 1037 | 1038 | chunks 1039 | .into_iter() 1040 | .map(|x| { 1041 | Command::InsertRows( 1042 | x[0], 1043 | x.into_iter() 1044 | .map(|x| vwr.clone_row(&table.rows[x.0])) 1045 | .collect(), 1046 | ) 1047 | }) 1048 | .collect() 1049 | } 1050 | Command::CcUpdateSystemClipboard(..) => { 1051 | // This command MUST've be consumed before calling this. 1052 | unreachable!() 1053 | } 1054 | }; 1055 | 1056 | // Discard all redos after this point. 1057 | self.undo_queue.drain(0..self.undo_cursor); 1058 | 1059 | // Discard all undos that exceed the capacity. 1060 | let new_len = capacity.saturating_sub(1).min(self.undo_queue.len()); 1061 | self.undo_queue.drain(new_len..); 1062 | 1063 | // Now it's the foremost element of undo queue. 1064 | self.undo_cursor = 0; 1065 | 1066 | // Apply the command. 1067 | self.cmd_apply(table, vwr, &command); 1068 | 1069 | // Push the command to the queue. 1070 | self.undo_queue.push_front(UndoArg { 1071 | apply: command, 1072 | restore, 1073 | }); 1074 | } 1075 | 1076 | fn cmd_apply>( 1077 | &mut self, 1078 | table: &mut DataTable, 1079 | vwr: &mut V, 1080 | cmd: &Command, 1081 | ) { 1082 | match cmd { 1083 | Command::SetVisibleColumns(cols) => { 1084 | self.validate_interactive_cell(cols.len()); 1085 | self.p.vis_cols.clear(); 1086 | self.p.vis_cols.extend(cols.iter().cloned()); 1087 | self.cc_dirty = true; 1088 | } 1089 | Command::SetColumnSort(new_sort) => { 1090 | self.p.sort.clear(); 1091 | self.p.sort.extend(new_sort.iter().cloned()); 1092 | self.cc_dirty = true; 1093 | } 1094 | Command::SetRowValue(row_id, value) => { 1095 | self.cc_num_frame_from_last_edit = 0; 1096 | table.dirty_flag = true; 1097 | let old_row = vwr.clone_row(&table.rows[row_id.0]); 1098 | table.rows[row_id.0] = vwr.clone_row(value); 1099 | 1100 | vwr.on_row_updated(row_id.0, &table.rows[row_id.0], &old_row); 1101 | } 1102 | Command::SetCells { slab, values } => { 1103 | self.cc_num_frame_from_last_edit = 0; 1104 | table.dirty_flag = true; 1105 | 1106 | let mut modified_rows: HashMap = HashMap::new(); 1107 | 1108 | for (row, col, value_id) in values.iter() { 1109 | let _ = modified_rows.entry(row.clone()).or_insert_with(|| vwr.clone_row(&table.rows[row.0])); 1110 | 1111 | vwr.set_cell_value(&slab[value_id.0], &mut table.rows[row.0], col.0); 1112 | } 1113 | 1114 | for (row, old_row) in modified_rows.iter() { 1115 | vwr.on_row_updated(row.0, &mut table.rows[row.0], old_row); 1116 | } 1117 | } 1118 | Command::InsertRows(pos, values) => { 1119 | self.cc_dirty = true; // It invalidates all current `RowId` occurrences. 1120 | table.dirty_flag = true; 1121 | 1122 | table 1123 | .rows 1124 | .splice(pos.0..pos.0, values.iter().map(|x| vwr.clone_row(x))); 1125 | let range = pos.0..pos.0 + values.len(); 1126 | 1127 | for row_index in range.clone() { 1128 | vwr.on_row_inserted(row_index, &mut table.rows[row_index]); 1129 | } 1130 | self.queue_select_rows(range.map(RowIdx)); 1131 | } 1132 | Command::RemoveRow(values) => { 1133 | debug_assert!(values.windows(2).all(|x| x[0] < x[1])); 1134 | self.cc_dirty = true; // It invalidates all current `RowId` occurrences. 1135 | table.dirty_flag = true; 1136 | 1137 | for row_index in values.iter() { 1138 | vwr.on_row_removed(row_index.0, &mut table.rows[row_index.0]); 1139 | } 1140 | 1141 | let mut index = 0; 1142 | table.rows.retain(|_| { 1143 | let idx_now = index.tap(|_| index += 1); 1144 | values.binary_search(&RowIdx(idx_now)).is_err() 1145 | }); 1146 | 1147 | self.queue_select_rows([]); 1148 | } 1149 | Command::CcHideColumn(..) 1150 | | Command::CcShowColumn { .. } 1151 | | Command::CcReorderColumn { .. } 1152 | | Command::CcEditStart(..) 1153 | | Command::CcCommitEdit 1154 | | Command::CcCancelEdit 1155 | | Command::CcSetSelection(..) 1156 | | Command::CcSetCells { .. } 1157 | | Command::CcUpdateSystemClipboard(..) => unreachable!(), 1158 | } 1159 | } 1160 | 1161 | fn queue_select_rows(&mut self, rows: impl IntoIterator) { 1162 | self.cc_desired_selection = Some(rows.into_iter().map(|r| (r, default())).collect()); 1163 | } 1164 | 1165 | fn validate_interactive_cell(&mut self, new_num_column: usize) { 1166 | let (r, c) = self.cc_interactive_cell.row_col(self.p.vis_cols.len()); 1167 | let rmax = self.cc_rows.len().saturating_sub(1); 1168 | let clen = self.p.vis_cols.len(); 1169 | 1170 | self.cc_interactive_cell = 1171 | VisLinearIdx(r.0.min(rmax) * clen + c.0.min(new_num_column.saturating_sub(1))); 1172 | } 1173 | 1174 | pub fn has_clipboard_contents(&self) -> bool { 1175 | self.clipboard.is_some() 1176 | } 1177 | 1178 | pub fn has_undo(&self) -> bool { 1179 | self.undo_cursor < self.undo_queue.len() 1180 | } 1181 | 1182 | pub fn has_redo(&self) -> bool { 1183 | self.undo_cursor > 0 1184 | } 1185 | 1186 | pub fn cursor_as_selection(&self) -> Option<&[VisSelection]> { 1187 | match &self.cc_cursor { 1188 | CursorState::Select(x) => Some(x), 1189 | CursorState::Edit { .. } => None, 1190 | } 1191 | } 1192 | 1193 | fn try_take_edition(&mut self) -> Option<(RowIdx, R, VisColumnPos)> { 1194 | if matches!(self.cc_cursor, CursorState::Edit { .. }) { 1195 | match replace(&mut self.cc_cursor, CursorState::Select(Vec::default())) { 1196 | CursorState::Edit { 1197 | row, 1198 | edition, 1199 | last_focus, 1200 | .. 1201 | } => Some((row, edition, last_focus)), 1202 | _ => unreachable!(), 1203 | } 1204 | } else { 1205 | None 1206 | } 1207 | } 1208 | 1209 | pub fn ui_action_context(&self) -> UiActionContext { 1210 | UiActionContext { 1211 | cursor: match &self.cc_cursor { 1212 | CursorState::Select(x) => { 1213 | if x.is_empty() { 1214 | UiCursorState::Idle 1215 | } else if x.len() == 1 && x[0].0 == x[0].1 { 1216 | UiCursorState::SelectOne 1217 | } else { 1218 | UiCursorState::SelectMany 1219 | } 1220 | } 1221 | CursorState::Edit { .. } => UiCursorState::Editing, 1222 | }, 1223 | } 1224 | } 1225 | 1226 | pub fn undo>(&mut self, table: &mut DataTable, vwr: &mut V) -> bool { 1227 | if self.undo_cursor == self.undo_queue.len() { 1228 | return false; 1229 | } 1230 | 1231 | let queue = take(&mut self.undo_queue); 1232 | { 1233 | let item = &queue[self.undo_cursor]; 1234 | for cmd in item.restore.iter() { 1235 | self.cmd_apply(table, vwr, cmd); 1236 | } 1237 | self.undo_cursor += 1; 1238 | } 1239 | self.undo_queue = queue; 1240 | 1241 | true 1242 | } 1243 | 1244 | pub fn redo>(&mut self, table: &mut DataTable, vwr: &mut V) -> bool { 1245 | if self.undo_cursor == 0 { 1246 | return false; 1247 | } 1248 | 1249 | let queue = take(&mut self.undo_queue); 1250 | { 1251 | self.undo_cursor -= 1; 1252 | self.cmd_apply(table, vwr, &queue[self.undo_cursor].apply); 1253 | } 1254 | self.undo_queue = queue; 1255 | 1256 | true 1257 | } 1258 | 1259 | pub fn set_interactive_cell(&mut self, row: VisRowPos, col: VisColumnPos) { 1260 | self.cc_interactive_cell = row.linear_index(self.p.vis_cols.len(), col); 1261 | } 1262 | 1263 | pub fn try_apply_ui_action( 1264 | &mut self, 1265 | table: &mut DataTable, 1266 | vwr: &mut impl RowViewer, 1267 | action: UiAction, 1268 | ) -> Vec> { 1269 | fn empty(_: T) -> Vec> { 1270 | default() 1271 | } 1272 | 1273 | self.cci_want_move_scroll = true; 1274 | 1275 | let (ic_r, ic_c) = self.cc_interactive_cell.row_col(self.p.vis_cols.len()); 1276 | match action { 1277 | UiAction::SelectionStartEditing => { 1278 | let row_id = self.cc_rows[ic_r.0]; 1279 | let src_row = &table.rows[row_id.0]; 1280 | if vwr.is_editable_cell(ic_c.0, ic_r.0, &src_row) { 1281 | let row = vwr.clone_row(src_row); 1282 | vec![Command::CcEditStart(row_id, ic_c, Box::new(row))] 1283 | } else { 1284 | vec![] 1285 | } 1286 | } 1287 | UiAction::CancelEdition => vec![Command::CcCancelEdit], 1288 | UiAction::CommitEdition => vec![Command::CcCommitEdit], 1289 | UiAction::CommitEditionAndMove(dir) => { 1290 | let pos = self.moved_position(self.cc_interactive_cell, dir); 1291 | let (r, c) = pos.row_col(self.p.vis_cols.len()); 1292 | 1293 | let mut commands = vec![ 1294 | Command::CcCommitEdit, 1295 | ]; 1296 | 1297 | let row_id = self.cc_rows[r.0]; 1298 | if vwr.is_editable_cell(c.0, r.0, &table.rows[row_id.0]) { 1299 | let row_value = if self.is_editing() && ic_r == r { 1300 | vwr.clone_row(self.unwrap_editing_row_data()) 1301 | } else { 1302 | vwr.clone_row(&table.rows[row_id.0]) 1303 | }; 1304 | 1305 | commands.push(Command::CcEditStart(row_id, c, row_value.into())); 1306 | } 1307 | 1308 | commands 1309 | } 1310 | UiAction::MoveSelection(dir) => { 1311 | let pos = self.moved_position(self.cc_interactive_cell, dir); 1312 | vec![Command::CcSetSelection(vec![VisSelection(pos, pos)])] 1313 | } 1314 | UiAction::Undo => self.undo(table, vwr).pipe(empty), 1315 | UiAction::Redo => self.redo(table, vwr).pipe(empty), 1316 | UiAction::CopySelection | UiAction::CutSelection => { 1317 | let sels = self.collect_selection(); 1318 | self.clipboard = None; 1319 | 1320 | if sels.is_empty() { 1321 | return vec![]; // we do nothing. 1322 | } 1323 | 1324 | // Copy contents to clipboard 1325 | let offset = sels.first().unwrap().0; 1326 | let mut slab = Vec::with_capacity(10); 1327 | let mut vis_map = HashMap::with_capacity(10); 1328 | 1329 | for vis_row in self.collect_selected_rows() { 1330 | vis_map.insert(vis_row, slab.len()); 1331 | slab.push(vwr.clone_row_as_copied_base(&table.rows[self.cc_rows[vis_row.0].0])); 1332 | } 1333 | 1334 | let clipboard = Clipboard { 1335 | slab: slab.into_boxed_slice(), 1336 | pastes: sels 1337 | .iter() 1338 | .map(|(v_r, v_c)| { 1339 | ( 1340 | VisRowOffset(v_r.0 - offset.0), 1341 | self.p.vis_cols[v_c.0], 1342 | RowSlabIndex(vis_map[v_r]), 1343 | ) 1344 | }) 1345 | .collect(), 1346 | } 1347 | .tap_mut(Clipboard::sort); 1348 | 1349 | let sys_clip = Self::try_dump_clipboard_content(&clipboard, vwr); 1350 | self.clipboard = Some(clipboard); 1351 | 1352 | if action == UiAction::CutSelection { 1353 | self.try_apply_ui_action(table, vwr, UiAction::DeleteSelection) 1354 | } else { 1355 | vec![] 1356 | } 1357 | .tap_mut(|v| { 1358 | // We only overwrite system clipboard when codec support is active. 1359 | if let Some(clip) = sys_clip { 1360 | v.push(Command::CcUpdateSystemClipboard(clip)); 1361 | } 1362 | }) 1363 | } 1364 | UiAction::SelectionDuplicateValues => { 1365 | let pivot_row = vwr.clone_row_as_copied_base(&table.rows[self.cc_rows[ic_r.0].0]); 1366 | let sels = self.collect_selection(); 1367 | 1368 | vec![Command::CcSetCells { 1369 | slab: [pivot_row].into(), 1370 | values: sels 1371 | .into_iter() 1372 | .map(|(r, c)| (self.cc_rows[r.0], self.p.vis_cols[c.0], RowSlabIndex(0))) 1373 | .collect(), 1374 | context: CellWriteContext::Paste, 1375 | }] 1376 | } 1377 | UiAction::PasteInPlace => { 1378 | let Some(clip) = &self.clipboard else { 1379 | return vec![]; 1380 | }; 1381 | 1382 | let values = 1383 | Vec::from_iter(clip.pastes.iter().filter_map(|(offset, col, slab_id)| { 1384 | let vis_r = VisRowPos(ic_r.0 + offset.0); 1385 | (vis_r.0 < self.cc_rows.len()) 1386 | .then(|| (self.cc_rows[vis_r.0], *col, *slab_id)) 1387 | })); 1388 | 1389 | let desired = self.cc_desired_selection.get_or_insert(default()); 1390 | desired.clear(); 1391 | 1392 | for (row, group) in &values.iter().chunk_by(|(row, ..)| *row) { 1393 | desired.push((row, group.map(|(_, c, ..)| *c).collect())) 1394 | } 1395 | 1396 | vec![Command::CcSetCells { 1397 | slab: clip.slab.iter().map(|x| vwr.clone_row(x)).collect(), 1398 | values: values.into_boxed_slice(), 1399 | context: CellWriteContext::Paste, 1400 | }] 1401 | } 1402 | UiAction::PasteInsert => { 1403 | let Some(clip) = &self.clipboard else { 1404 | return vec![]; 1405 | }; 1406 | 1407 | let mut last = usize::MAX; 1408 | let mut rows = clip 1409 | .pastes 1410 | .iter() 1411 | .filter(|&(offset, ..)| replace(&mut last, offset.0) != offset.0) 1412 | .map(|(offset, ..)| { 1413 | ( 1414 | *offset, 1415 | vwr.new_empty_row_for(EmptyRowCreateContext::InsertNewLine), 1416 | ) 1417 | }) 1418 | .collect::>(); 1419 | 1420 | for (offset, column, slab_id) in &*clip.pastes { 1421 | vwr.set_cell_value( 1422 | &clip.slab[slab_id.0], 1423 | rows.get_mut(offset).unwrap(), 1424 | column.0, 1425 | ); 1426 | } 1427 | 1428 | let pos = if self.p.sort.is_empty() { 1429 | self.cc_rows[ic_r.0] 1430 | } else { 1431 | RowIdx(table.rows.len()) 1432 | }; 1433 | 1434 | let row_values = rows.into_values().collect(); 1435 | vec![Command::InsertRows(pos, row_values)] 1436 | } 1437 | UiAction::DuplicateRow => { 1438 | if vwr.allow_row_insertions() { 1439 | let rows = self 1440 | .collect_selected_rows() 1441 | .into_iter() 1442 | .map(|x| self.cc_rows[x.0]) 1443 | .map(|r| vwr.clone_row_for_insertion(&table.rows[r.0])) 1444 | .collect(); 1445 | 1446 | let pos = if self.p.sort.is_empty() { 1447 | self.cc_rows[ic_r.0] 1448 | } else { 1449 | RowIdx(table.rows.len()) 1450 | }; 1451 | 1452 | vec![Command::InsertRows(pos, rows)] 1453 | } else { 1454 | vec![] 1455 | } 1456 | } 1457 | UiAction::DeleteSelection => { 1458 | let default = vwr.new_empty_row_for(EmptyRowCreateContext::DeletionDefault); 1459 | let sels = self.collect_selection(); 1460 | let slab = vec![default].into_boxed_slice(); 1461 | 1462 | vec![Command::CcSetCells { 1463 | slab, 1464 | values: sels 1465 | .into_iter() 1466 | .map(|(r, c)| (self.cc_rows[r.0], self.p.vis_cols[c.0], RowSlabIndex(0))) 1467 | .collect(), 1468 | context: CellWriteContext::Clear, 1469 | }] 1470 | } 1471 | UiAction::DeleteRow => { 1472 | if vwr.allow_row_deletions() { 1473 | let rows = self 1474 | .collect_selected_rows() 1475 | .into_iter() 1476 | .map(|x| self.cc_rows[x.0]) 1477 | .filter(|row| vwr.confirm_row_deletion_by_ui(&table.rows[row.0])) 1478 | .collect(); 1479 | 1480 | vec![Command::RemoveRow(rows)] 1481 | } else { 1482 | vec![] 1483 | } 1484 | } 1485 | UiAction::SelectAll => { 1486 | if self.cc_rows.is_empty() { 1487 | return vec![]; 1488 | } 1489 | 1490 | vec![Command::CcSetSelection(vec![VisSelection( 1491 | VisLinearIdx(0), 1492 | VisRowPos(self.cc_rows.len().saturating_sub(1)).linear_index( 1493 | self.p.vis_cols.len(), 1494 | VisColumnPos(self.p.vis_cols.len() - 1), 1495 | ), 1496 | )])] 1497 | } 1498 | 1499 | action @ (UiAction::NavPageDown 1500 | | UiAction::NavPageUp 1501 | | UiAction::NavTop 1502 | | UiAction::NavBottom) => { 1503 | let ofst = match action { 1504 | UiAction::NavPageDown => self.cci_page_row_count as isize, 1505 | UiAction::NavPageUp => -(self.cci_page_row_count as isize), 1506 | UiAction::NavTop => isize::MIN, 1507 | UiAction::NavBottom => isize::MAX, 1508 | _ => unreachable!(), 1509 | }; 1510 | 1511 | let new_ic_r = (ic_r.0 as isize) 1512 | .saturating_add(ofst) 1513 | .clamp(0, self.cc_rows.len().saturating_sub(1) as _); 1514 | self.cc_interactive_cell = 1515 | VisLinearIdx(new_ic_r as usize * self.p.vis_cols.len() + ic_c.0); 1516 | 1517 | self.validate_interactive_cell(self.p.vis_cols.len()); 1518 | vec![Command::CcSetSelection(vec![VisSelection( 1519 | self.cc_interactive_cell, 1520 | self.cc_interactive_cell, 1521 | )])] 1522 | } 1523 | } 1524 | } 1525 | 1526 | fn collect_selection(&self) -> BTreeSet<(VisRowPos, VisColumnPos)> { 1527 | let mut set = BTreeSet::new(); 1528 | 1529 | if let CursorState::Select(selections) = &self.cc_cursor { 1530 | for sel in selections.iter() { 1531 | let (top, left) = sel.0.row_col(self.p.vis_cols.len()); 1532 | let (bottom, right) = sel.1.row_col(self.p.vis_cols.len()); 1533 | 1534 | for r in top.0..=bottom.0 { 1535 | for c in left.0..=right.0 { 1536 | set.insert((VisRowPos(r), VisColumnPos(c))); 1537 | } 1538 | } 1539 | } 1540 | } 1541 | 1542 | set 1543 | } 1544 | 1545 | fn collect_selected_rows(&self) -> BTreeSet { 1546 | let mut rows = BTreeSet::new(); 1547 | 1548 | if let CursorState::Select(selections) = &self.cc_cursor { 1549 | for sel in selections.iter() { 1550 | let (top, _) = sel.0.row_col(self.p.vis_cols.len()); 1551 | let (bottom, _) = sel.1.row_col(self.p.vis_cols.len()); 1552 | 1553 | for r in top.0..=bottom.0 { 1554 | rows.insert(VisRowPos(r)); 1555 | } 1556 | } 1557 | } 1558 | 1559 | rows 1560 | } 1561 | 1562 | fn moved_position(&self, pos: VisLinearIdx, dir: MoveDirection) -> VisLinearIdx { 1563 | let (VisRowPos(r), VisColumnPos(c)) = pos.row_col(self.p.vis_cols.len()); 1564 | 1565 | let (rmax, cmax) = ( 1566 | self.cc_rows.len().saturating_sub(1), 1567 | self.p.vis_cols.len().saturating_sub(1), 1568 | ); 1569 | 1570 | let (nr, nc) = match dir { 1571 | MoveDirection::Up => match (r, c) { 1572 | (0, c) => (0, c), 1573 | (r, c) => (r - 1, c), 1574 | }, 1575 | MoveDirection::Down => match (r, c) { 1576 | (r, c) if r == rmax => (r, c), 1577 | (r, c) => (r + 1, c), 1578 | }, 1579 | MoveDirection::Left => match (r, c) { 1580 | (0, 0) => (0, 0), 1581 | (r, 0) => (r - 1, cmax), 1582 | (r, c) => (r, c - 1), 1583 | }, 1584 | MoveDirection::Right => match (r, c) { 1585 | (r, c) if r == rmax && c == cmax => (r, c), 1586 | (r, c) if c == cmax => (r + 1, 0), 1587 | (r, c) => (r, c + 1), 1588 | }, 1589 | }; 1590 | 1591 | VisLinearIdx(nr * self.p.vis_cols.len() + nc) 1592 | } 1593 | 1594 | pub fn cci_take_selection(&mut self, mods: egui::Modifiers) -> Option> { 1595 | let ncol = self.p.vis_cols.len(); 1596 | let cci_sel = self 1597 | .cci_selection 1598 | .take() 1599 | .map(|(_0, _1)| VisSelection::from_points(ncol, _0, _1))?; 1600 | 1601 | if mods.is_none() { 1602 | return Some(vec![cci_sel]); 1603 | } 1604 | 1605 | let mut sel = self.cursor_as_selection().unwrap_or_default().to_owned(); 1606 | let idx_contains = sel.iter().position(|x| x.contains_rect(ncol, cci_sel)); 1607 | if sel.is_empty() { 1608 | sel.push(cci_sel); 1609 | return Some(sel); 1610 | } 1611 | 1612 | if mods.command_only() { 1613 | if let Some(idx) = idx_contains { 1614 | sel.remove(idx); 1615 | } else { 1616 | sel.push(cci_sel); 1617 | } 1618 | } 1619 | 1620 | if mods.cmd_ctrl_matches(Modifiers::SHIFT) { 1621 | let last = sel.last_mut().unwrap(); 1622 | if cci_sel.is_point() && last.is_point() { 1623 | *last = last.union(ncol, cci_sel); 1624 | } else if idx_contains.is_none() { 1625 | sel.push(cci_sel); 1626 | }; 1627 | } 1628 | 1629 | Some(sel) 1630 | } 1631 | } 1632 | 1633 | /* ------------------------------------------ Commands ------------------------------------------ */ 1634 | 1635 | /// NOTE: `Cc` prefix stands for cache command which won't be stored in undo/redo queue, since they 1636 | /// are not called from `cmd_apply` method. 1637 | pub(crate) enum Command { 1638 | CcHideColumn(ColumnIdx), 1639 | CcShowColumn { 1640 | what: ColumnIdx, 1641 | at: VisColumnPos, 1642 | }, 1643 | CcReorderColumn { 1644 | from: VisColumnPos, 1645 | to: VisColumnPos, 1646 | }, 1647 | 1648 | SetColumnSort(Vec<(ColumnIdx, IsAscending)>), 1649 | SetVisibleColumns(Vec), 1650 | 1651 | CcSetSelection(Vec), // Cache - Set Selection 1652 | 1653 | SetRowValue(RowIdx, Box), 1654 | CcSetCells { 1655 | slab: Box<[R]>, 1656 | values: Box<[(RowIdx, ColumnIdx, RowSlabIndex)]>, 1657 | context: CellWriteContext, 1658 | }, 1659 | SetCells { 1660 | slab: Box<[R]>, 1661 | values: Box<[(RowIdx, ColumnIdx, RowSlabIndex)]>, 1662 | }, 1663 | 1664 | InsertRows(RowIdx, Box<[R]>), 1665 | RemoveRow(Vec), 1666 | 1667 | CcEditStart(RowIdx, VisColumnPos, Box), 1668 | CcCancelEdit, 1669 | CcCommitEdit, 1670 | 1671 | CcUpdateSystemClipboard(String), 1672 | } 1673 | --------------------------------------------------------------------------------