Skip to main content

egui_material3/
spreadsheet.rs

1//! Material Design Spreadsheet Component
2//!
3//! A spreadsheet widget with DuckDB backend for data storage and manipulation.
4//! Supports importing/exporting CSV, Excel, Parquet, and Arrow formats.
5
6#[cfg(feature = "spreadsheet")]
7use crate::button::MaterialButton;
8#[cfg(feature = "spreadsheet")]
9use crate::theme::get_global_color;
10#[cfg(feature = "spreadsheet")]
11use std::collections::{HashMap, HashSet};
12#[cfg(feature = "spreadsheet")]
13use std::path::PathBuf;
14#[cfg(feature = "spreadsheet")]
15use std::sync::{Arc, Mutex};
16
17#[cfg(feature = "spreadsheet")]
18use duckdb::{params, Connection, Result as DuckResult};
19#[cfg(feature = "spreadsheet")]
20use egui::{
21    Color32, FontFamily, FontId, Id, Rect, Response, Sense, TextEdit, Ui, Vec2, Widget,
22};
23#[cfg(feature = "spreadsheet")]
24use egui_async::{Bind, StateWithData};
25
26// Re-export for convenience
27#[cfg(feature = "spreadsheet")]
28pub use egui_extras::{Column, TableBuilder};
29
30/// Column definition for spreadsheet
31#[cfg(feature = "spreadsheet")]
32#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
33pub struct ColumnDef {
34    pub name: String,
35    pub col_type: ColumnType,
36    pub width: f32,
37}
38
39/// Supported column data types
40#[cfg(feature = "spreadsheet")]
41#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
42pub enum ColumnType {
43    Text,
44    Integer,
45    Real,
46    Boolean,
47}
48
49#[cfg(feature = "spreadsheet")]
50impl ColumnType {
51    fn to_sql(&self) -> &'static str {
52        match self {
53            ColumnType::Text => "TEXT",
54            ColumnType::Integer => "INTEGER",
55            ColumnType::Real => "REAL",
56            ColumnType::Boolean => "BOOLEAN",
57        }
58    }
59}
60
61/// A single row of data
62#[cfg(feature = "spreadsheet")]
63#[derive(Clone, Debug)]
64pub struct RowData {
65    pub id: usize,
66    pub values: Vec<String>,
67}
68
69/// File format for import/export
70#[cfg(feature = "spreadsheet")]
71#[derive(Clone, Debug, PartialEq)]
72pub enum FileFormat {
73    Csv,
74    Excel,
75    Parquet,
76    Arrow,
77}
78
79#[cfg(feature = "spreadsheet")]
80impl FileFormat {
81    pub fn from_path(path: &std::path::Path) -> Option<Self> {
82        path.extension()?.to_str().and_then(|ext| match ext.to_lowercase().as_str() {
83            "csv" => Some(FileFormat::Csv),
84            "xls" | "xlsx" => Some(FileFormat::Excel),
85            "parquet" => Some(FileFormat::Parquet),
86            "arrow" => Some(FileFormat::Arrow),
87            _ => None,
88        })
89    }
90}
91
92/// DuckDB-backed data model for spreadsheet
93#[cfg(feature = "spreadsheet")]
94pub struct SpreadsheetDataModel {
95    conn: Connection,
96    table_name: String,
97    columns: Vec<ColumnDef>,
98    row_count: usize,
99}
100
101#[cfg(feature = "spreadsheet")]
102impl SpreadsheetDataModel {
103    /// Create a new in-memory spreadsheet data model
104    pub fn new(columns: Vec<ColumnDef>) -> DuckResult<Self> {
105        let conn = Connection::open_in_memory()?;
106        let table_name = "spreadsheet_data".to_string();
107
108        let mut model = Self {
109            conn,
110            table_name: table_name.clone(),
111            columns: columns.clone(),
112            row_count: 0,
113        };
114
115        model.create_table()?;
116        Ok(model)
117    }
118
119    fn create_table(&mut self) -> DuckResult<()> {
120        let mut col_defs = vec!["id INTEGER PRIMARY KEY".to_string()];
121        for (idx, col) in self.columns.iter().enumerate() {
122            col_defs.push(format!("col{} {}", idx, col.col_type.to_sql()));
123        }
124
125        let create_sql = format!(
126            "CREATE TABLE IF NOT EXISTS {} ({})",
127            self.table_name,
128            col_defs.join(", ")
129        );
130
131        self.conn.execute_batch(&create_sql)?;
132        Ok(())
133    }
134
135    /// Insert multiple rows
136    pub fn insert_rows(&mut self, rows: Vec<Vec<String>>) -> DuckResult<()> {
137        for row_values in rows {
138            self.insert_row(row_values)?;
139        }
140        Ok(())
141    }
142
143    /// Insert a single row
144    pub fn insert_row(&mut self, values: Vec<String>) -> DuckResult<()> {
145        let placeholders: Vec<_> = (0..=self.columns.len()).map(|_| "?").collect();
146        let insert_sql = format!(
147            "INSERT INTO {} VALUES ({})",
148            self.table_name,
149            placeholders.join(", ")
150        );
151
152        let mut stmt = self.conn.prepare(&insert_sql)?;
153        let row_id = self.row_count;
154        self.row_count += 1;
155
156        // Build params dynamically
157        let mut params_vec: Vec<Box<dyn duckdb::ToSql>> = vec![Box::new(row_id)];
158        for value in values {
159            params_vec.push(Box::new(value));
160        }
161
162        // Convert to params slice
163        let params_refs: Vec<&dyn duckdb::ToSql> = params_vec.iter().map(|b| &**b as &dyn duckdb::ToSql).collect();
164        stmt.execute(params_refs.as_slice())?;
165
166        Ok(())
167    }
168
169    /// Query all rows
170    pub fn query_rows(&self) -> DuckResult<Vec<RowData>> {
171        let query_sql = format!("SELECT * FROM {} ORDER BY id", self.table_name);
172        let mut stmt = self.conn.prepare(&query_sql)?;
173
174        let col_count = self.columns.len();
175        let rows = stmt.query_map([], |row| {
176            let id: usize = row.get(0)?;
177            let mut values = Vec::new();
178            for i in 0..col_count {
179                // Convert all column types to strings for display
180                let val: String = match row.get_ref(i + 1) {
181                    Ok(val_ref) => {
182                        use duckdb::types::ValueRef;
183                        match val_ref {
184                            ValueRef::Null => String::new(),
185                            ValueRef::Boolean(b) => b.to_string(),
186                            ValueRef::TinyInt(i) => i.to_string(),
187                            ValueRef::SmallInt(i) => i.to_string(),
188                            ValueRef::Int(i) => i.to_string(),
189                            ValueRef::BigInt(i) => i.to_string(),
190                            ValueRef::HugeInt(i) => i.to_string(),
191                            ValueRef::UTinyInt(i) => i.to_string(),
192                            ValueRef::USmallInt(i) => i.to_string(),
193                            ValueRef::UInt(i) => i.to_string(),
194                            ValueRef::UBigInt(i) => i.to_string(),
195                            ValueRef::Float(f) => f.to_string(),
196                            ValueRef::Double(f) => f.to_string(),
197                            ValueRef::Text(s) => String::from_utf8_lossy(s).to_string(),
198                            ValueRef::Blob(b) => format!("<blob {} bytes>", b.len()),
199                            _ => row.get::<_, String>(i + 1).unwrap_or_default(),
200                        }
201                    }
202                    Err(_) => String::new(),
203                };
204                values.push(val);
205            }
206            Ok(RowData { id, values })
207        })?;
208
209        let mut result = Vec::new();
210        for row in rows {
211            result.push(row?);
212        }
213        Ok(result)
214    }
215
216    /// Update a single cell
217    pub fn update_cell(&mut self, row_id: usize, col_idx: usize, value: String) -> Result<(), String> {
218        // Validate value against column type
219        if col_idx < self.columns.len() {
220            let col_type = &self.columns[col_idx].col_type;
221            
222            // For numeric types, validate that the value can be parsed
223            match col_type {
224                ColumnType::Integer => {
225                    if !value.is_empty() && value.parse::<i64>().is_err() {
226                        return Err(format!("'{}' is not a valid integer", value));
227                    }
228                }
229                ColumnType::Real => {
230                    if !value.is_empty() && value.parse::<f64>().is_err() {
231                        return Err(format!("'{}' is not a valid number", value));
232                    }
233                }
234                ColumnType::Boolean => {
235                    if !value.is_empty() && value.parse::<bool>().is_err() {
236                        // Accept common boolean representations
237                        let lower = value.to_lowercase();
238                        if lower != "true" && lower != "false" && lower != "1" && lower != "0" {
239                            return Err(format!("'{}' is not a valid boolean (use true/false or 1/0)", value));
240                        }
241                    }
242                }
243                ColumnType::Text => {} // Text accepts anything
244            }
245        }
246        
247        let update_sql = format!(
248            "UPDATE {} SET col{} = ? WHERE id = ?",
249            self.table_name, col_idx
250        );
251        self.conn.execute(&update_sql, params![value, row_id])
252            .map_err(|e| e.to_string())?;
253        Ok(())
254    }
255
256    /// Delete a row
257    pub fn delete_row(&mut self, row_id: usize) -> DuckResult<()> {
258        let delete_sql = format!("DELETE FROM {} WHERE id = ?", self.table_name);
259        self.conn.execute(&delete_sql, params![row_id])?;
260        Ok(())
261    }
262
263    /// Export to CSV
264    pub fn export_csv(&self, path: &std::path::Path) -> Result<(), String> {
265        use std::fs::File;
266        use std::io::Write;
267
268        let rows = self.query_rows().map_err(|e| e.to_string())?;
269        let mut file = File::create(path).map_err(|e| e.to_string())?;
270
271        // Write header
272        let header: Vec<String> = self.columns.iter().map(|c| c.name.clone()).collect();
273        writeln!(file, "{}", header.join(",")).map_err(|e| e.to_string())?;
274
275        // Write rows
276        for row in rows {
277            writeln!(file, "{}", row.values.join(",")).map_err(|e| e.to_string())?;
278        }
279
280        Ok(())
281    }
282
283    /// Import from CSV
284    pub fn import_csv(&mut self, path: &std::path::Path) -> Result<(), String> {
285        use std::fs::File;
286        use std::io::BufRead;
287
288        let file = File::open(path).map_err(|e| format!("Cannot open file: {}", e))?;
289        let reader = std::io::BufReader::new(file);
290        let all_lines: Vec<String> = reader.lines()
291            .collect::<Result<Vec<_>, _>>()
292            .map_err(|e| format!("Failed to read file: {}", e))?;
293        
294        if all_lines.is_empty() {
295            return Err("CSV file is empty".to_string());
296        }
297        
298        if all_lines.len() < 2 {
299            return Err("CSV file has only one line".to_string());
300        }
301
302        let first_line = &all_lines[0];
303        let second_line = &all_lines[1];
304        let last_line = all_lines.last().unwrap();
305        
306        // Detect delimiter by comparing counts in first, second, and last lines
307        let delimiters = [',', ';', '\t'];
308        let mut best_delimiter = ',';
309        let mut best_score = 0;
310        
311        for &delim in &delimiters {
312            let count1 = first_line.matches(delim).count();
313            let count2 = second_line.matches(delim).count();
314            let count_last = last_line.matches(delim).count();
315            
316            // Score based on consistency across lines and total count
317            if count1 > 0 && count1 == count2 && count2 == count_last {
318                // Perfect consistency
319                let score = count1 * 100;
320                if score > best_score {
321                    best_score = score;
322                    best_delimiter = delim;
323                }
324            } else if count1 > 0 && count2 > 0 {
325                // Partial consistency - prefer if counts are similar
326                let min_count = count1.min(count2).min(count_last);
327                let max_count = count1.max(count2).max(count_last);
328                if max_count > 0 && min_count > 0 {
329                    let score = (min_count * 50) / max_count;
330                    if score > best_score {
331                        best_score = score;
332                        best_delimiter = delim;
333                    }
334                }
335            }
336        }
337        
338        let delimiter = best_delimiter;
339        let delimiter_name = match delimiter {
340            ',' => "comma",
341            ';' => "semicolon",
342            '\t' => "tab",
343            _ => "unknown",
344        };
345        
346        let first_values: Vec<&str> = first_line.split(delimiter).collect();
347        let second_values: Vec<&str> = second_line.split(delimiter).collect();
348        
349        let col_count = first_values.len();
350        
351        // Determine if first line is a header (heuristic: check if types differ)
352        let first_line_is_header = first_values.iter().zip(second_values.iter()).any(|(v1, v2)| {
353            let v1_is_num = v1.trim().parse::<f64>().is_ok();
354            let v2_is_num = v2.trim().parse::<f64>().is_ok();
355            v1_is_num != v2_is_num
356        });
357        
358        // Create new column definitions
359        let new_columns: Vec<ColumnDef> = if first_line_is_header {
360            first_values.iter().enumerate().map(|(i, name)| {
361                ColumnDef {
362                    name: name.trim().to_string(),
363                    col_type: ColumnType::Text,
364                    width: 100.0,
365                }
366            }).collect()
367        } else {
368            (0..col_count).map(|i| {
369                ColumnDef {
370                    name: format!("column{}", i + 1),
371                    col_type: ColumnType::Text,
372                    width: 100.0,
373                }
374            }).collect()
375        };
376        
377        eprintln!("Detected {} columns with {} delimiter", col_count, delimiter_name);
378        eprintln!("First line is header: {}", first_line_is_header);
379        
380        // Recreate table with new columns
381        self.columns = new_columns;
382        self.conn.execute_batch(&format!("DROP TABLE IF EXISTS {}", self.table_name))
383            .map_err(|e| format!("Failed to drop table: {}", e))?;
384        self.row_count = 0;
385        self.create_table()
386            .map_err(|e| format!("Failed to create table: {}", e))?;
387        
388        // Prepare data rows
389        let start_idx = if first_line_is_header { 1 } else { 0 };
390        let data_lines: Vec<&String> = all_lines.iter()
391            .skip(start_idx)
392            .filter(|line| !line.trim().is_empty())
393            .collect();
394        
395        // Insert data rows
396        for (idx, line) in data_lines.iter().enumerate() {
397            let values: Vec<String> = line.split(delimiter).map(|s| s.trim().to_string()).collect();
398            
399            // Validate column count
400            if values.len() != col_count {
401                return Err(format!(
402                    "CSV row {} has {} columns, but expected {} columns",
403                    idx + if first_line_is_header { 2 } else { 1 },
404                    values.len(),
405                    col_count
406                ));
407            }
408            
409            self.insert_row(values)
410                .map_err(|e| format!("Failed to insert row {}: {}", idx + 1, e))?;
411        }
412
413        eprintln!("Successfully imported {} rows from CSV", data_lines.len());
414        Ok(())
415    }
416
417    /// Export to Parquet using DuckDB's built-in support
418    pub fn export_parquet(&self, path: &std::path::Path) -> Result<(), String> {
419        let export_sql = format!(
420            "COPY {} TO '{}' (FORMAT PARQUET)",
421            self.table_name,
422            path.to_string_lossy()
423        );
424        self.conn
425            .execute_batch(&export_sql)
426            .map_err(|e| e.to_string())?;
427        Ok(())
428    }
429
430    /// Import from Parquet using DuckDB's built-in support
431    pub fn import_parquet(&mut self, path: &std::path::Path) -> Result<(), String> {
432        // Clear existing data before importing
433        self.conn.execute(&format!("DELETE FROM {}", self.table_name), [])
434            .map_err(|e| format!("Failed to clear table: {}", e))?;
435        self.row_count = 0;
436
437        // Read parquet data and insert row by row to handle type conversions
438        let query_sql = format!("SELECT * FROM read_parquet('{}')", path.to_string_lossy());
439        let mut stmt = self.conn.prepare(&query_sql)
440            .map_err(|e| format!("Failed to read parquet file: {}", e))?;
441        
442        let col_count = self.columns.len();
443        let rows = stmt.query_map([], |row| {
444            let mut values = Vec::new();
445            for i in 0..col_count {
446                // Convert all values to strings for compatibility
447                let val: String = match row.get_ref(i) {
448                    Ok(val_ref) => {
449                        use duckdb::types::ValueRef;
450                        match val_ref {
451                            ValueRef::Null => String::new(),
452                            ValueRef::Boolean(b) => b.to_string(),
453                            ValueRef::TinyInt(i) => i.to_string(),
454                            ValueRef::SmallInt(i) => i.to_string(),
455                            ValueRef::Int(i) => i.to_string(),
456                            ValueRef::BigInt(i) => i.to_string(),
457                            ValueRef::HugeInt(i) => i.to_string(),
458                            ValueRef::UTinyInt(i) => i.to_string(),
459                            ValueRef::USmallInt(i) => i.to_string(),
460                            ValueRef::UInt(i) => i.to_string(),
461                            ValueRef::UBigInt(i) => i.to_string(),
462                            ValueRef::Float(f) => f.to_string(),
463                            ValueRef::Double(f) => f.to_string(),
464                            ValueRef::Text(s) => String::from_utf8_lossy(s).to_string(),
465                            _ => String::new(),
466                        }
467                    }
468                    Err(_) => String::new(),
469                };
470                values.push(val);
471            }
472            Ok(values)
473        })
474        .map_err(|e| format!("Failed to query parquet data: {}", e))?;
475
476        // Collect all rows first to avoid borrow conflicts
477        let all_rows: Vec<Vec<String>> = rows
478            .map(|row_result| row_result.map_err(|e| format!("Failed to process row: {}", e)))
479            .collect::<Result<Vec<_>, String>>()?;
480        
481        // Drop stmt to release the borrow on self.conn
482        drop(stmt);
483
484        // Insert all rows
485        for values in all_rows {
486            self.insert_row(values).map_err(|e| format!("Failed to insert row: {}", e))?;
487        }
488
489        Ok(())
490    }
491}
492
493/// Actions that can be performed on spreadsheet
494#[cfg(feature = "spreadsheet")]
495#[derive(Debug, Clone)]
496pub enum SpreadsheetAction {
497    CellEdited { row_id: usize, col_idx: usize, value: String },
498    RowAdded,
499    RowDeleted(usize),
500    DataLoaded(PathBuf),
501    DataSaved(PathBuf),
502}
503
504/// Material Design Spreadsheet widget
505#[cfg(feature = "spreadsheet")]
506pub struct MaterialSpreadsheet {
507    id: Id,
508    pub data_model: Arc<Mutex<SpreadsheetDataModel>>,
509    cached_rows: Vec<RowData>,
510    editing_cell: Option<(usize, usize)>,
511    edit_buffer: String,
512    selected_rows: HashSet<usize>,
513    allow_editing: bool,
514    allow_selection: bool,
515    striped: bool,
516    row_height: f32,
517    load_bind: Bind<Vec<RowData>, String>,
518    save_bind: Bind<(), String>,
519    load_processed: bool, // Track if we've processed the load result
520}
521
522#[cfg(feature = "spreadsheet")]
523impl MaterialSpreadsheet {
524    /// Create a new spreadsheet with the given columns
525    pub fn new(id: &str, columns: Vec<ColumnDef>) -> Result<Self, String> {
526        let data_model = SpreadsheetDataModel::new(columns).map_err(|e| e.to_string())?;
527
528        Ok(Self {
529            id: Id::new(id),
530            data_model: Arc::new(Mutex::new(data_model)),
531            cached_rows: Vec::new(),
532            editing_cell: None,
533            edit_buffer: String::new(),
534            selected_rows: HashSet::new(),
535            allow_editing: true,
536            allow_selection: true,
537            striped: true,
538            row_height: 36.0,
539            load_bind: Bind::new(false),
540            save_bind: Bind::new(false),
541            load_processed: false,
542        })
543    }
544
545    /// Set whether cells can be edited
546    pub fn allow_editing(mut self, allow: bool) -> Self {
547        self.allow_editing = allow;
548        self
549    }
550
551    /// Set whether rows can be selected
552    pub fn allow_selection(mut self, allow: bool) -> Self {
553        self.allow_selection = allow;
554        self
555    }
556
557    /// Set striped rows
558    pub fn striped(mut self, striped: bool) -> Self {
559        self.striped = striped;
560        self
561    }
562
563    /// Set whether cells can be edited (mutable setter)
564    pub fn set_allow_editing(&mut self, allow: bool) {
565        self.allow_editing = allow;
566    }
567
568    /// Set whether rows can be selected (mutable setter)
569    pub fn set_allow_selection(&mut self, allow: bool) {
570        self.allow_selection = allow;
571    }
572
573    /// Set striped rows (mutable setter)
574    pub fn set_striped(&mut self, striped: bool) {
575        self.striped = striped;
576    }
577
578    /// Add a new empty row
579    pub fn add_row(&mut self) -> Result<(), String> {
580        let mut model = self.data_model.lock().unwrap();
581        let col_count = model.columns.len();
582        let empty_values = vec![String::new(); col_count];
583        model.insert_row(empty_values).map_err(|e| e.to_string())?;
584        // Refresh cached rows to show the new row
585        self.cached_rows = model.query_rows().map_err(|e| e.to_string())?;
586        Ok(())
587    }
588
589    /// Delete a row by ID
590    pub fn delete_row(&mut self, row_id: usize) -> Result<(), String> {
591        let mut model = self.data_model.lock().unwrap();
592        model.delete_row(row_id).map_err(|e| e.to_string())?;
593        Ok(())
594    }
595
596    /// Refresh cached data from database
597    pub fn refresh_data(&mut self) -> Result<(), String> {
598        let model = self.data_model.lock().unwrap();
599        self.cached_rows = model.query_rows().map_err(|e| e.to_string())?;
600        Ok(())
601    }
602
603    /// Load data from file (async)
604    pub fn load_from_file(&mut self, path: PathBuf) {
605        self.load_processed = false; // Reset flag for new load
606        let model = Arc::clone(&self.data_model);
607        self.load_bind.refresh(async move {
608            let format = FileFormat::from_path(&path).ok_or_else(|| "Unknown file format".to_string())?;
609            
610            let mut locked_model = model.lock().unwrap();
611            match format {
612                FileFormat::Csv => locked_model.import_csv(&path)?,
613                FileFormat::Parquet => locked_model.import_parquet(&path)?,
614                FileFormat::Excel => return Err("Excel import not yet implemented".to_string()),
615                FileFormat::Arrow => return Err("Arrow import not yet implemented".to_string()),
616            }
617            
618            // Return all rows after loading
619            locked_model.query_rows().map_err(|e| e.to_string())
620        });
621    }
622
623    /// Save data to file (async)
624    pub fn save_to_file(&mut self, path: PathBuf) {
625        let model = Arc::clone(&self.data_model);
626        self.save_bind.refresh(async move {
627            let format = FileFormat::from_path(&path).ok_or_else(|| "Unknown file format".to_string())?;
628            
629            let locked_model = model.lock().unwrap();
630            match format {
631                FileFormat::Csv => locked_model.export_csv(&path)?,
632                FileFormat::Parquet => locked_model.export_parquet(&path)?,
633                FileFormat::Excel => return Err("Excel export not yet implemented".to_string()),
634                FileFormat::Arrow => return Err("Arrow export not yet implemented".to_string()),
635            }
636            
637            Ok(())
638        });
639    }
640
641    /// Get the column definitions
642    pub fn columns(&self) -> Vec<ColumnDef> {
643        self.data_model.lock().unwrap().columns.clone()
644    }
645
646    /// Get the current rows
647    pub fn rows(&self) -> Vec<RowData> {
648        self.cached_rows.clone()
649    }
650
651    /// Show the spreadsheet UI (alternative to Widget trait)
652    pub fn show(&mut self, ui: &mut Ui) -> Response {
653        // Register egui-async plugin (requires egui 0.33+)
654        ui.ctx().plugin_or_default::<egui_async::EguiAsyncPlugin>();
655
656        // Handle async load state
657        match self.load_bind.state() {
658            StateWithData::Pending => {
659                ui.ctx().request_repaint();
660                return ui.label("Loading...").interact(Sense::hover());
661            }
662            StateWithData::Finished(rows) => {
663                // Only update cached_rows once when load completes
664                if !self.load_processed {
665                    eprintln!("DEBUG: Load finished, updating cached_rows with {} rows", rows.len());
666                    self.cached_rows = rows.clone();
667                    self.load_processed = true;
668                }
669            }
670            StateWithData::Failed(err) => {
671                return ui.label(format!("Load error: {}", err)).interact(Sense::hover());
672            }
673            StateWithData::Idle => {}
674        }
675
676        // Handle async save state
677        match self.save_bind.state() {
678            StateWithData::Pending => {
679                ui.ctx().request_repaint();
680            }
681            StateWithData::Failed(err) => {
682                ui.label(format!("Save error: {}", err));
683            }
684            _ => {}
685        }
686
687        // Get column definitions
688        let columns = self.data_model.lock().unwrap().columns.clone();
689
690        // Get theme colors
691        let surface = get_global_color("surface");
692        let on_surface = get_global_color("on-surface");
693        let primary = get_global_color("primary");
694        let surface_variant = get_global_color("surface-variant");
695
696        // Build table
697        let text_height = ui.text_style_height(&egui::TextStyle::Body);
698        let available_height = ui.available_height();
699
700        let mut table = TableBuilder::new(ui)
701            .striped(self.striped)
702            .resizable(true)
703            .cell_layout(egui::Layout::left_to_right(egui::Align::Center))
704            .min_scrolled_height(0.0)
705            .max_scroll_height(available_height);
706
707        // Add columns
708        for col in columns.iter() {
709            table = table.column(Column::initial(col.width).at_least(50.0).resizable(true));
710        }
711
712        // Clone cached rows for rendering to avoid borrow issues
713        let display_rows = self.cached_rows.clone();
714        eprintln!("DEBUG: Rendering table with {} cached rows", display_rows.len());
715        if !display_rows.is_empty() && display_rows.len() > 4 {
716            eprintln!("DEBUG: Row 4 data: {:?}", display_rows[4].values);
717        }
718
719        // Use UI memory to store pending cell updates
720        let pending_update_id = self.id.with("pending_cell_update");
721        
722        table
723            .header(30.0, |mut header| {
724                for col in columns.iter() {
725                    header.col(|ui| {
726                        // Paint header background color like datatable
727                        let rect = ui.max_rect();
728                        ui.painter().rect_filled(rect, egui::Rounding::ZERO, surface_variant);
729                        
730                        ui.style_mut().visuals.override_text_color = Some(on_surface);
731                        ui.strong(&col.name);
732                    });
733                }
734            })
735            .body(|mut body| {
736                for row_data in &display_rows {
737                    body.row(self.row_height, |mut row| {
738                        for (col_idx, value) in row_data.values.iter().enumerate() {
739                            row.col(|ui| {
740                                let is_editing = self.editing_cell == Some((row_data.id, col_idx));
741
742                                if is_editing {
743                                    // Edit mode with TextEdit
744                                    eprintln!("DEBUG: Rendering TextEdit - row_id: {}, col_idx: {}, buffer: '{}'", row_data.id, col_idx, self.edit_buffer);
745                                    let edit_response = ui.add(
746                                        TextEdit::singleline(&mut self.edit_buffer)
747                                            .desired_width(f32::INFINITY)
748                                    );
749                                    eprintln!("DEBUG: TextEdit state - has_focus: {}, lost_focus: {}, gained_focus: {}", 
750                                        edit_response.has_focus(), edit_response.lost_focus(), edit_response.gained_focus());
751
752                                    // Handle Enter to save, Escape to cancel, or save on blur
753                                    if edit_response.lost_focus() {
754                                        let escape_pressed = ui.input(|i| i.key_pressed(egui::Key::Escape));
755                                        eprintln!("DEBUG: TextEdit lost focus - escape_pressed: {}", escape_pressed);
756                                        
757                                        if !escape_pressed {
758                                            eprintln!("DEBUG: Storing cell update - row_id: {}, col_idx: {}, value: '{}'", row_data.id, col_idx, self.edit_buffer);
759                                            // Store the update in UI memory for processing after rendering
760                                            ui.memory_mut(|mem| {
761                                                mem.data.insert_temp(pending_update_id, (row_data.id, col_idx, self.edit_buffer.clone()));
762                                            });
763                                        } else {
764                                            eprintln!("DEBUG: Edit cancelled with Escape");
765                                        }
766                                        // Always exit edit mode when losing focus
767                                        self.editing_cell = None;
768                                    }
769
770                                    if edit_response.gained_focus() {
771                                        edit_response.request_focus();
772                                        eprintln!("DEBUG: Requested focus for TextEdit");
773                                    }
774                                } else {
775                                    // View mode with label
776                                    let label_response = ui.label(value);
777
778                                    // Single-click to edit (changed from double-click)
779                                    if self.allow_editing && label_response.clicked() {
780                                        eprintln!("DEBUG: Starting edit mode - row_id: {}, col_idx: {}, current_value: {}", row_data.id, col_idx, value);
781                                        self.editing_cell = Some((row_data.id, col_idx));
782                                        self.edit_buffer = value.clone();
783                                    }
784                                }
785                            });
786                        }
787                    });
788                }
789            });
790
791        // Apply any pending cell update after rendering
792        if let Some((row_id, col_idx, new_value)) = ui.memory(|mem| {
793            mem.data.get_temp::<(usize, usize, String)>(pending_update_id)
794        }) {
795            eprintln!("DEBUG: Retrieved cell update - row_id: {}, col_idx: {}, value: {}", row_id, col_idx, new_value);
796            
797            // Clear the pending update
798            ui.memory_mut(|mem| {
799                mem.data.remove::<(usize, usize, String)>(pending_update_id);
800            });
801            
802            let mut model = self.data_model.lock().unwrap();
803            match model.update_cell(row_id, col_idx, new_value.clone()) {
804                Ok(_) => {
805                    eprintln!("DEBUG: Cell updated in database successfully");
806                    // Refresh cached rows to show the update
807                    match model.query_rows() {
808                        Ok(rows) => {
809                            eprintln!("DEBUG: Refreshed {} rows from database", rows.len());
810                            if rows.len() > 4 {
811                                eprintln!("DEBUG: After query, row 4 data: {:?}", rows[4].values);
812                            }
813                            self.cached_rows = rows;
814                            // Request repaint so the updated data appears immediately
815                            ui.ctx().request_repaint();
816                            eprintln!("DEBUG: Requested repaint");
817                        }
818                        Err(e) => {
819                            eprintln!("DEBUG: Failed to query rows: {}", e);
820                        }
821                    }
822                }
823                Err(e) => {
824                    eprintln!("DEBUG: Failed to update cell: {}", e);
825                    
826                    // Show error to user - store in temp memory for display
827                    ui.memory_mut(|mem| {
828                        mem.data.insert_temp(
829                            self.id.with("cell_error"),
830                            e.clone()
831                        );
832                    });
833                    ui.ctx().request_repaint();
834                }
835            }
836        } else {
837            eprintln!("DEBUG: No pending cell update found");
838        }
839
840        // Display error message if there's a cell error
841        if let Some(error_msg) = ui.memory(|mem| {
842            mem.data.get_temp::<String>(self.id.with("cell_error"))
843        }) {
844            // Clear the error
845            ui.memory_mut(|mem| {
846                mem.data.remove::<String>(self.id.with("cell_error"));
847            });
848            
849            // Show error message at the top
850            ui.ctx().debug_painter().text(
851                ui.max_rect().center_top() + egui::vec2(0.0, 10.0),
852                egui::Align2::CENTER_TOP,
853                &error_msg,
854                egui::FontId::proportional(14.0),
855                get_global_color("error"),
856            );
857            
858            // Keep showing the error for a bit
859            ui.ctx().request_repaint_after(std::time::Duration::from_secs(3));
860        }
861
862        ui.interact(ui.max_rect(), self.id, Sense::hover())
863    }
864}
865
866#[cfg(feature = "spreadsheet")]
867impl Widget for MaterialSpreadsheet {
868    fn ui(mut self, ui: &mut Ui) -> Response {
869        self.show(ui)
870    }
871}
872
873/// Helper function to create a column definition
874#[cfg(feature = "spreadsheet")]
875pub fn column(name: impl Into<String>, col_type: ColumnType, width: f32) -> ColumnDef {
876    ColumnDef {
877        name: name.into(),
878        col_type,
879        width,
880    }
881}
882
883/// Helper function to create a text column
884#[cfg(feature = "spreadsheet")]
885pub fn text_column(name: impl Into<String>, width: f32) -> ColumnDef {
886    column(name, ColumnType::Text, width)
887}
888
889/// Helper function to create a number column
890#[cfg(feature = "spreadsheet")]
891pub fn number_column(name: impl Into<String>, width: f32) -> ColumnDef {
892    column(name, ColumnType::Real, width)
893}
894
895/// Helper function to create an integer column
896#[cfg(feature = "spreadsheet")]
897pub fn integer_column(name: impl Into<String>, width: f32) -> ColumnDef {
898    column(name, ColumnType::Integer, width)
899}