1#![forbid(unsafe_code)]
2#![warn(missing_docs)]
3mod table;
38
39mod align;
40mod csv;
41mod filter;
42mod selection;
43mod sort;
44
45pub mod accessibility;
46pub mod async_source;
47pub mod clipboard;
48pub mod format;
49pub mod header;
50pub mod height;
51pub mod height_cache;
52pub mod nav;
53pub mod pagination;
54pub mod persistence;
55pub mod text_integration;
56pub mod theme_integration;
57
58#[cfg(feature = "egui-table")]
59mod egui_table;
60
61#[cfg(feature = "iced-table")]
62mod iced_table;
63
64pub use table::{RenderedCell, Table};
65
66pub use align::CellAlign;
67pub use csv::to_csv;
68pub use filter::{apply_all, filter_indices, ColumnFilter};
69pub use selection::{SelectionMode, SelectionModel};
70pub use sort::{sort_indices, SortDirection, SortState};
71
72pub use async_source::{AsyncRowSource, BoxFuture, PrefetchBuffer};
73pub use clipboard::{selection_to_tsv, CaptureClipboard, ClipboardSink, NullClipboard};
74pub use format::{CellFormatter, DateFormatter, DefaultFormatter, NumberFormatter};
75pub use header::{handle_row_click, move_column, HeaderSortState, TableIndex};
76pub use height::CumulativeHeights;
77pub use height_cache::{CumulativeHeightCache, RowCache};
78pub use nav::TableNav;
79pub use pagination::PaginationState;
80
81#[cfg(feature = "egui-table")]
82pub use egui_table::EguiTableState;
83
84#[cfg(feature = "iced-table")]
85pub use iced_table::{
86 render_iced, render_iced_sortable, render_iced_with_filters, render_iced_with_selection,
87};
88
89pub const DEFAULT_ROW_HEIGHT: f32 = 24.0;
92
93#[derive(Debug, Clone)]
99pub enum TableEvent {
100 RowSelected(usize),
104 CellEdited {
106 row: usize,
108 col: usize,
110 new_value: String,
112 },
113 SortChanged {
115 col: usize,
117 ascending: bool,
119 },
120 ColumnResized {
122 col: usize,
124 new_width: f32,
126 },
127 FilterChanged {
129 col: usize,
131 new_filter: String,
133 },
134}
135
136#[derive(Debug, Clone, PartialEq)]
138pub enum TableError {
139 ReadOnly,
141 OutOfBounds {
143 row: usize,
145 col: usize,
147 },
148 InvalidValue(String),
150}
151
152impl std::fmt::Display for TableError {
153 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 match self {
155 TableError::ReadOnly => write!(f, "table source is read-only"),
156 TableError::OutOfBounds { row, col } => {
157 write!(f, "cell ({row}, {col}) is out of bounds")
158 }
159 TableError::InvalidValue(msg) => write!(f, "invalid cell value: {msg}"),
160 }
161 }
162}
163
164impl std::error::Error for TableError {}
165
166pub trait RowSource {
168 fn row_count(&self) -> usize;
170 fn row(&self, index: usize) -> Vec<Cell>;
172 fn column_defs(&self) -> &[ColumnDef];
174
175 fn set_cell(&mut self, _row: usize, _col: usize, _value: Cell) -> Result<(), TableError> {
181 Err(TableError::ReadOnly)
182 }
183
184 fn row_height(&self, _index: usize) -> f32 {
189 DEFAULT_ROW_HEIGHT
190 }
191
192 fn children(&self, _row: usize) -> Option<Vec<usize>> {
197 None
198 }
199
200 fn indent_level(&self, _row: usize) -> usize {
205 0
206 }
207
208 fn footer(&self) -> Option<Vec<Cell>> {
213 None
214 }
215}
216
217impl<T: RowSource + ?Sized> RowSource for Box<T> {
220 fn row_count(&self) -> usize {
221 (**self).row_count()
222 }
223 fn row(&self, index: usize) -> Vec<Cell> {
224 (**self).row(index)
225 }
226 fn column_defs(&self) -> &[ColumnDef] {
227 (**self).column_defs()
228 }
229 fn set_cell(&mut self, row: usize, col: usize, value: Cell) -> Result<(), TableError> {
230 (**self).set_cell(row, col, value)
231 }
232 fn row_height(&self, index: usize) -> f32 {
233 (**self).row_height(index)
234 }
235 fn children(&self, row: usize) -> Option<Vec<usize>> {
236 (**self).children(row)
237 }
238 fn indent_level(&self, row: usize) -> usize {
239 (**self).indent_level(row)
240 }
241 fn footer(&self) -> Option<Vec<Cell>> {
242 (**self).footer()
243 }
244}
245
246pub trait CellRenderer: std::fmt::Debug + Send {
252 fn render_str(&self) -> String;
254}
255
256fn unix_ms_to_iso8601(ms: i64) -> String {
263 let days = if ms >= 0 {
265 ms / 86_400_000
266 } else {
267 (ms - 86_399_999) / 86_400_000
269 };
270
271 let jdn = days + 2_440_588_i64;
273
274 let a = jdn + 32044;
275 let b = (4 * a + 3) / 146097;
276 let c = a - (146097 * b) / 4;
277 let d = (4 * c + 3) / 1461;
278 let e = c - (1461 * d) / 4;
279 let m = (5 * e + 2) / 153;
280 let day = e - (153 * m + 2) / 5 + 1;
281 let month = m + 3 - 12 * (m / 10);
282 let year = 100 * b + d - 4800 + m / 10;
283
284 format!("{year:04}-{month:02}-{day:02}")
285}
286
287#[derive(Debug)]
289pub enum Cell {
290 Text(String),
292 Int(i64),
294 Float(f64),
296 Bool(bool),
298 Empty,
300 Date(i64),
304 Currency {
309 amount_cents: i64,
311 code: String,
313 },
314 Link {
318 label: String,
320 url: String,
322 },
323 Image {
327 uri: String,
329 },
330 Custom(Box<dyn CellRenderer>),
335}
336
337impl Clone for Cell {
338 fn clone(&self) -> Self {
339 match self {
340 Cell::Text(s) => Cell::Text(s.clone()),
341 Cell::Int(n) => Cell::Int(*n),
342 Cell::Float(v) => Cell::Float(*v),
343 Cell::Bool(b) => Cell::Bool(*b),
344 Cell::Empty => Cell::Empty,
345 Cell::Date(ms) => Cell::Date(*ms),
346 Cell::Currency { amount_cents, code } => Cell::Currency {
347 amount_cents: *amount_cents,
348 code: code.clone(),
349 },
350 Cell::Link { label, url } => Cell::Link {
351 label: label.clone(),
352 url: url.clone(),
353 },
354 Cell::Image { uri } => Cell::Image { uri: uri.clone() },
355 Cell::Custom(_) => Cell::Empty,
357 }
358 }
359}
360
361impl std::fmt::Display for Cell {
362 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
363 match self {
364 Cell::Text(s) => write!(f, "{s}"),
365 Cell::Int(n) => write!(f, "{n}"),
366 Cell::Float(v) => write!(f, "{v}"),
367 Cell::Bool(b) => write!(f, "{b}"),
368 Cell::Empty => Ok(()),
369 Cell::Date(ms) => write!(f, "{}", unix_ms_to_iso8601(*ms)),
370 Cell::Currency { amount_cents, code } => {
371 let major = amount_cents / 100;
372 let minor = amount_cents.abs() % 100;
373 if *amount_cents < 0 && major == 0 {
375 write!(f, "-0.{minor:02} {code}")
376 } else {
377 write!(f, "{major}.{minor:02} {code}")
378 }
379 }
380 Cell::Link { label, .. } => write!(f, "{label}"),
381 Cell::Image { uri } => write!(f, "[image: {uri}]"),
382 Cell::Custom(renderer) => write!(f, "{}", renderer.render_str()),
383 }
384 }
385}
386
387impl Cell {
388 pub fn is_empty(&self) -> bool {
390 matches!(self, Cell::Empty)
391 }
392
393 pub fn compare(&self, other: &Cell) -> std::cmp::Ordering {
400 use std::cmp::Ordering;
401 match (self, other) {
402 (Cell::Int(a), Cell::Int(b)) => a.cmp(b),
403 (Cell::Float(a), Cell::Float(b)) => a.total_cmp(b),
404 (Cell::Int(a), Cell::Float(b)) => (*a as f64).total_cmp(b),
406 (Cell::Float(a), Cell::Int(b)) => a.total_cmp(&(*b as f64)),
407 (Cell::Text(a), Cell::Text(b)) => a.cmp(b),
408 (Cell::Bool(a), Cell::Bool(b)) => a.cmp(b),
409 (Cell::Empty, Cell::Empty) => Ordering::Equal,
410 (Cell::Date(a), Cell::Date(b)) => a.cmp(b),
411 (
412 Cell::Currency {
413 amount_cents: a, ..
414 },
415 Cell::Currency {
416 amount_cents: b, ..
417 },
418 ) => a.cmp(b),
419 _ => self.type_rank().cmp(&other.type_rank()),
421 }
422 }
423
424 fn type_rank(&self) -> u8 {
426 match self {
427 Cell::Empty => 0,
428 Cell::Bool(_) => 1,
429 Cell::Int(_) => 2,
430 Cell::Float(_) => 2, Cell::Text(_) => 3,
432 Cell::Date(_) => 4,
433 Cell::Currency { .. } => 5,
434 Cell::Link { .. } => 6,
435 Cell::Image { .. } => 7,
436 Cell::Custom(_) => 8,
437 }
438 }
439}
440
441impl From<&str> for Cell {
442 fn from(s: &str) -> Self {
443 Cell::Text(s.to_owned())
444 }
445}
446
447impl From<String> for Cell {
448 fn from(s: String) -> Self {
449 Cell::Text(s)
450 }
451}
452
453impl From<i64> for Cell {
454 fn from(n: i64) -> Self {
455 Cell::Int(n)
456 }
457}
458
459impl From<i32> for Cell {
460 fn from(n: i32) -> Self {
461 Cell::Int(n as i64)
462 }
463}
464
465impl From<f64> for Cell {
466 fn from(v: f64) -> Self {
467 Cell::Float(v)
468 }
469}
470
471impl From<bool> for Cell {
472 fn from(b: bool) -> Self {
473 Cell::Bool(b)
474 }
475}
476
477pub struct ColumnDef {
480 pub name: String,
482 pub width: f32,
484 pub min_width: f32,
486 pub max_width: f32,
488 pub resizable: bool,
490 pub formatter: Option<Box<dyn CellFormatter>>,
492 pub align: Option<CellAlign>,
494}
495
496impl Clone for ColumnDef {
497 fn clone(&self) -> Self {
498 Self {
500 name: self.name.clone(),
501 width: self.width,
502 min_width: self.min_width,
503 max_width: self.max_width,
504 resizable: self.resizable,
505 formatter: None,
506 align: self.align,
507 }
508 }
509}
510
511impl std::fmt::Debug for ColumnDef {
512 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
513 f.debug_struct("ColumnDef")
514 .field("name", &self.name)
515 .field("width", &self.width)
516 .field("min_width", &self.min_width)
517 .field("max_width", &self.max_width)
518 .field("resizable", &self.resizable)
519 .field("formatter", &self.formatter.as_ref().map(|_| "<formatter>"))
520 .field("align", &self.align)
521 .finish()
522 }
523}
524
525impl Default for ColumnDef {
526 fn default() -> Self {
527 Self {
528 name: String::new(),
529 width: 100.0,
530 min_width: 40.0,
531 max_width: 800.0,
532 resizable: true,
533 formatter: None,
534 align: None,
535 }
536 }
537}
538
539impl ColumnDef {
540 pub fn new(name: impl Into<String>) -> Self {
542 Self {
543 name: name.into(),
544 ..Self::default()
545 }
546 }
547}
548
549pub struct ColumnDefBuilder {
553 inner: ColumnDef,
554}
555
556impl ColumnDefBuilder {
557 pub fn new(name: impl Into<String>) -> Self {
559 Self {
560 inner: ColumnDef::new(name),
561 }
562 }
563
564 pub fn width(mut self, w: f32) -> Self {
566 self.inner.width = w;
567 self
568 }
569
570 pub fn min_width(mut self, w: f32) -> Self {
572 self.inner.min_width = w;
573 self
574 }
575
576 pub fn max_width(mut self, w: f32) -> Self {
578 self.inner.max_width = w;
579 self
580 }
581
582 pub fn resizable(mut self) -> Self {
584 self.inner.resizable = true;
585 self
586 }
587
588 pub fn formatter(mut self, f: impl CellFormatter + 'static) -> Self {
590 self.inner.formatter = Some(Box::new(f));
591 self
592 }
593
594 pub fn align(mut self, a: CellAlign) -> Self {
596 self.inner.align = Some(a);
597 self
598 }
599
600 pub fn build(self) -> ColumnDef {
602 self.inner
603 }
604}
605
606pub fn aggregate_sum(cells: &[Cell]) -> f64 {
611 cells
612 .iter()
613 .filter_map(|c| match c {
614 Cell::Int(n) => Some(*n as f64),
615 Cell::Float(f) => Some(*f),
616 _ => None,
617 })
618 .sum()
619}
620
621pub fn aggregate_count(cells: &[Cell]) -> usize {
623 cells.iter().filter(|c| !matches!(c, Cell::Empty)).count()
624}
625
626pub fn aggregate_avg(cells: &[Cell]) -> Option<f64> {
630 let nums: Vec<f64> = cells
631 .iter()
632 .filter_map(|c| match c {
633 Cell::Int(n) => Some(*n as f64),
634 Cell::Float(f) => Some(*f),
635 _ => None,
636 })
637 .collect();
638 if nums.is_empty() {
639 None
640 } else {
641 Some(nums.iter().sum::<f64>() / nums.len() as f64)
642 }
643}
644
645pub struct TableBuilder<S: RowSource> {
649 source: S,
650 page_size: usize,
651 zebra_striping: bool,
652}
653
654impl<S: RowSource> TableBuilder<S> {
655 pub fn new(source: S) -> Self {
657 Self {
658 source,
659 page_size: 50,
660 zebra_striping: false,
661 }
662 }
663
664 pub fn page_size(mut self, size: usize) -> Self {
666 self.page_size = size;
667 self
668 }
669
670 pub fn zebra_striping(mut self, enabled: bool) -> Self {
672 self.zebra_striping = enabled;
673 self
674 }
675
676 pub fn build(self) -> Table<S> {
678 Table::new(self.source)
679 .with_page_size(self.page_size)
680 .with_zebra_striping(self.zebra_striping)
681 }
682}
683
684#[cfg(test)]
687mod cell_type_tests {
688 use super::*;
689
690 #[test]
691 fn cell_date_epoch() {
692 assert_eq!(format!("{}", Cell::Date(0)), "1970-01-01");
693 }
694
695 #[test]
696 fn cell_date_one_day() {
697 assert_eq!(format!("{}", Cell::Date(86_400_000)), "1970-01-02");
698 }
699
700 #[test]
701 fn cell_date_leap_year_2000_02_29() {
702 let ms = 11_016_i64 * 86_400_000_i64;
709 assert_eq!(format!("{}", Cell::Date(ms)), "2000-02-29");
710 }
711
712 #[test]
713 fn cell_date_2100_02_28() {
714 let ms = 47_540_i64 * 86_400_000_i64;
721 assert_eq!(format!("{}", Cell::Date(ms)), "2100-02-28");
722 }
723
724 #[test]
725 fn cell_currency_display() {
726 let c = Cell::Currency {
727 amount_cents: 12345,
728 code: "EUR".to_string(),
729 };
730 assert_eq!(format!("{c}"), "123.45 EUR");
731 }
732
733 #[test]
734 fn cell_currency_negative() {
735 let c = Cell::Currency {
736 amount_cents: -100,
737 code: "USD".to_string(),
738 };
739 assert_eq!(format!("{c}"), "-1.00 USD");
740 }
741
742 #[test]
743 fn cell_currency_zero() {
744 let c = Cell::Currency {
745 amount_cents: 0,
746 code: "GBP".to_string(),
747 };
748 assert_eq!(format!("{c}"), "0.00 GBP");
749 }
750
751 #[test]
752 fn cell_link_shows_label() {
753 let c = Cell::Link {
754 label: "Click here".to_string(),
755 url: "https://example.com".to_string(),
756 };
757 assert_eq!(format!("{c}"), "Click here");
758 }
759
760 #[test]
761 fn cell_image_display() {
762 let c = Cell::Image {
763 uri: "https://example.com/img.png".to_string(),
764 };
765 assert_eq!(format!("{c}"), "[image: https://example.com/img.png]");
766 }
767
768 #[test]
769 fn cell_custom_delegates_to_render_str() {
770 #[derive(Debug)]
771 struct MyRenderer;
772 impl CellRenderer for MyRenderer {
773 fn render_str(&self) -> String {
774 "custom".to_string()
775 }
776 }
777 let c = Cell::Custom(Box::new(MyRenderer));
778 assert_eq!(format!("{c}"), "custom");
779 }
780}