1use compact_str::CompactString;
6use presentar_core::{
7 Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event, Key,
8 LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
9};
10use std::any::Any;
11use std::time::Duration;
12
13#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
15pub enum StatusLevel {
16 #[default]
17 Ok,
18 Warning,
19 Critical,
20 Unknown,
21}
22
23impl StatusLevel {
24 #[must_use]
26 pub fn render(self) -> (char, Color) {
27 match self {
28 Self::Ok => ('●', Color::new(0.2, 0.8, 0.2, 1.0)),
29 Self::Warning => ('●', Color::new(0.9, 0.7, 0.1, 1.0)),
30 Self::Critical => ('●', Color::new(0.9, 0.2, 0.2, 1.0)),
31 Self::Unknown => ('○', Color::new(0.5, 0.5, 0.5, 1.0)),
32 }
33 }
34}
35
36#[derive(Debug, Clone, Default)]
38pub enum CellValue {
39 #[default]
40 Null,
41 Bool(bool),
42 Int64(i64),
43 Float64(f64),
44 String(CompactString),
45 Sparkline(Vec<f64>),
47 SparkBar(Vec<f64>),
49 SparkWinLoss(Vec<i8>),
51 TrendArrow(f64),
53 MicroBar {
55 value: f64,
56 max: f64,
57 },
58 ProgressBar(f64),
60 StatusDot(StatusLevel),
62}
63
64impl CellValue {
65 #[must_use]
67 pub fn render(&self, width: usize) -> (String, Color) {
68 match self {
69 Self::Null => (String::new(), Color::new(0.5, 0.5, 0.5, 1.0)),
70 Self::Bool(b) => (if *b { "true" } else { "false" }.to_string(), Color::WHITE),
71 Self::Int64(n) => (format!("{n}"), Color::WHITE),
72 Self::Float64(f) => (format!("{f:.2}"), Color::WHITE),
73 Self::String(s) => (s.to_string(), Color::WHITE),
74 Self::Sparkline(values) => (
75 Self::render_sparkline(values, width),
76 Color::new(0.3, 0.7, 1.0, 1.0),
77 ),
78 Self::SparkBar(values) => (
79 Self::render_sparkbar(values, width),
80 Color::new(0.5, 0.8, 0.5, 1.0),
81 ),
82 Self::SparkWinLoss(values) => (
83 Self::render_winloss(values, width),
84 Color::new(0.7, 0.7, 0.7, 1.0),
85 ),
86 Self::TrendArrow(delta) => Self::render_trend(*delta),
87 Self::MicroBar { value, max } => (
88 Self::render_microbar(*value, *max, width),
89 Color::new(0.4, 0.6, 0.9, 1.0),
90 ),
91 Self::ProgressBar(pct) => (
92 Self::render_progress(*pct, width),
93 Color::new(0.3, 0.8, 0.3, 1.0),
94 ),
95 Self::StatusDot(level) => {
96 let (ch, color) = level.render();
97 (ch.to_string(), color)
98 }
99 }
100 }
101
102 fn render_sparkline(values: &[f64], width: usize) -> String {
103 const BARS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
104
105 if values.is_empty() {
106 return " ".repeat(width);
107 }
108
109 let min = values.iter().copied().fold(f64::INFINITY, f64::min);
110 let max = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
111 let range = (max - min).max(1e-10);
112
113 let sample_width = width.min(values.len());
114 let step = values.len() / sample_width.max(1);
115
116 (0..sample_width)
117 .map(|i| {
118 let idx = (i * step).min(values.len() - 1);
119 let v = values[idx];
120 if !v.is_finite() {
121 return ' ';
122 }
123 let norm = ((v - min) / range * 7.0).round() as usize;
124 BARS[norm.min(7)]
125 })
126 .collect()
127 }
128
129 fn render_sparkbar(values: &[f64], width: usize) -> String {
130 const BLOCKS: [char; 4] = ['░', '▒', '▓', '█'];
131
132 if values.is_empty() {
133 return " ".repeat(width);
134 }
135
136 let max = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
137 let max = max.max(1e-10);
138
139 let sample_width = width.min(values.len());
140 let step = values.len() / sample_width.max(1);
141
142 (0..sample_width)
143 .map(|i| {
144 let idx = (i * step).min(values.len() - 1);
145 let v = values[idx];
146 if !v.is_finite() || v < 0.0 {
147 return ' ';
148 }
149 let norm = ((v / max) * 3.0).round() as usize;
150 BLOCKS[norm.min(3)]
151 })
152 .collect()
153 }
154
155 fn render_winloss(values: &[i8], width: usize) -> String {
156 let sample_width = width.min(values.len());
157 let step = values.len() / sample_width.max(1);
158
159 (0..sample_width)
160 .map(|i| {
161 let idx = (i * step).min(values.len() - 1);
162 match values[idx].cmp(&0) {
163 std::cmp::Ordering::Greater => '▲',
164 std::cmp::Ordering::Less => '▼',
165 std::cmp::Ordering::Equal => '─',
166 }
167 })
168 .collect()
169 }
170
171 fn render_trend(delta: f64) -> (String, Color) {
172 let (arrow, color) = if delta > 0.1 {
173 ('↑', Color::new(0.2, 0.8, 0.2, 1.0))
174 } else if delta > 0.02 {
175 ('↗', Color::new(0.5, 0.8, 0.3, 1.0))
176 } else if delta > -0.02 {
177 ('→', Color::new(0.7, 0.7, 0.7, 1.0))
178 } else if delta > -0.1 {
179 ('↘', Color::new(0.8, 0.5, 0.3, 1.0))
180 } else {
181 ('↓', Color::new(0.9, 0.2, 0.2, 1.0))
182 };
183 (format!("{arrow} {delta:+.1}%"), color)
184 }
185
186 fn render_microbar(value: f64, max: f64, width: usize) -> String {
187 let pct = (value / max.max(1e-10)).clamp(0.0, 1.0);
188 let filled = ((width as f64) * pct).round() as usize;
189 let empty = width.saturating_sub(filled);
190 format!("{}{}", "█".repeat(filled), "░".repeat(empty))
191 }
192
193 fn render_progress(pct: f64, width: usize) -> String {
194 let pct = pct.clamp(0.0, 100.0);
195 let filled = ((width as f64) * (pct / 100.0)).round() as usize;
196 let empty = width.saturating_sub(filled);
197 format!("{}{} {:.0}%", "▓".repeat(filled), "░".repeat(empty), pct)
198 }
199}
200
201#[derive(Debug, Clone)]
203pub struct Column {
204 pub name: CompactString,
206 pub values: Vec<CellValue>,
208 pub width: usize,
210 pub align: ColumnAlign,
212}
213
214#[derive(Debug, Clone, Copy, Default)]
216pub enum ColumnAlign {
217 #[default]
218 Left,
219 Right,
220 Center,
221}
222
223impl Column {
224 #[must_use]
226 pub fn new(name: impl Into<CompactString>) -> Self {
227 Self {
228 name: name.into(),
229 values: Vec::new(),
230 width: 10,
231 align: ColumnAlign::default(),
232 }
233 }
234
235 #[must_use]
237 pub fn with_width(mut self, width: usize) -> Self {
238 self.width = width;
239 self
240 }
241
242 #[must_use]
244 pub fn with_align(mut self, align: ColumnAlign) -> Self {
245 self.align = align;
246 self
247 }
248
249 #[must_use]
251 pub fn with_values(mut self, values: Vec<CellValue>) -> Self {
252 self.values = values;
253 self
254 }
255
256 #[must_use]
258 pub fn from_f64(name: impl Into<CompactString>, values: &[f64]) -> Self {
259 Self {
260 name: name.into(),
261 values: values.iter().map(|&v| CellValue::Float64(v)).collect(),
262 width: 10,
263 align: ColumnAlign::Right,
264 }
265 }
266
267 #[must_use]
269 pub fn from_i64(name: impl Into<CompactString>, values: &[i64]) -> Self {
270 Self {
271 name: name.into(),
272 values: values.iter().map(|&v| CellValue::Int64(v)).collect(),
273 width: 10,
274 align: ColumnAlign::Right,
275 }
276 }
277
278 #[must_use]
280 pub fn from_strings(name: impl Into<CompactString>, values: &[&str]) -> Self {
281 Self {
282 name: name.into(),
283 values: values
284 .iter()
285 .map(|&s| CellValue::String(CompactString::from(s)))
286 .collect(),
287 width: 15,
288 align: ColumnAlign::Left,
289 }
290 }
291
292 #[must_use]
294 pub fn sparkline_from_rows(name: impl Into<CompactString>, rows: Vec<Vec<f64>>) -> Self {
295 Self {
296 name: name.into(),
297 values: rows.into_iter().map(CellValue::Sparkline).collect(),
298 width: 12,
299 align: ColumnAlign::Left,
300 }
301 }
302}
303
304#[derive(Debug, Clone)]
306pub struct DataFrame {
307 columns: Vec<Column>,
308 visible_rows: usize,
310 scroll_offset: usize,
312 selected_row: Option<usize>,
314 show_header: bool,
316 show_row_numbers: bool,
318 bounds: Rect,
320}
321
322impl DataFrame {
323 #[must_use]
325 pub fn new() -> Self {
326 Self {
327 columns: Vec::new(),
328 visible_rows: 20,
329 scroll_offset: 0,
330 selected_row: None,
331 show_header: true,
332 show_row_numbers: true,
333 bounds: Rect::default(),
334 }
335 }
336
337 #[must_use]
339 pub fn with_column(mut self, column: Column) -> Self {
340 self.columns.push(column);
341 self
342 }
343
344 #[must_use]
346 pub fn with_visible_rows(mut self, rows: usize) -> Self {
347 self.visible_rows = rows;
348 self
349 }
350
351 #[must_use]
353 pub fn with_header(mut self, show: bool) -> Self {
354 self.show_header = show;
355 self
356 }
357
358 #[must_use]
360 pub fn with_row_numbers(mut self, show: bool) -> Self {
361 self.show_row_numbers = show;
362 self
363 }
364
365 #[must_use]
367 pub fn row_count(&self) -> usize {
368 self.columns.first().map_or(0, |c| c.values.len())
369 }
370
371 #[must_use]
373 pub fn column_count(&self) -> usize {
374 self.columns.len()
375 }
376
377 pub fn add_sparkline_column(&mut self, name: &str, source_cols: &[usize]) {
379 let row_count = self.row_count();
380 let mut sparkline_data = Vec::with_capacity(row_count);
381
382 for row_idx in 0..row_count {
383 let values: Vec<f64> = source_cols
384 .iter()
385 .filter_map(|&col_idx| {
386 self.columns.get(col_idx).and_then(|col| {
387 col.values.get(row_idx).and_then(|v| match v {
388 CellValue::Float64(f) => Some(*f),
389 CellValue::Int64(i) => Some(*i as f64),
390 _ => None,
391 })
392 })
393 })
394 .collect();
395 sparkline_data.push(CellValue::Sparkline(values));
396 }
397
398 self.columns.push(Column {
399 name: CompactString::from(name),
400 values: sparkline_data,
401 width: 12,
402 align: ColumnAlign::Left,
403 });
404 }
405
406 pub fn scroll_to(&mut self, row: usize) {
408 let row_count = self.row_count();
409 if row < row_count {
410 self.scroll_offset = row.min(row_count.saturating_sub(self.visible_rows));
411 }
412 }
413
414 pub fn select_row(&mut self, row: Option<usize>) {
416 self.selected_row = row;
417 }
418
419 fn render_cell(&self, value: &CellValue, width: usize, align: ColumnAlign) -> (String, Color) {
420 let (content, color) = value.render(width);
421 let padded = match align {
422 ColumnAlign::Left => format!("{content:<width$}"),
423 ColumnAlign::Right => format!("{content:>width$}"),
424 ColumnAlign::Center => format!("{content:^width$}"),
425 };
426 let truncated: String = padded.chars().take(width).collect();
428 (truncated, color)
429 }
430}
431
432impl Default for DataFrame {
433 fn default() -> Self {
434 Self::new()
435 }
436}
437
438impl Widget for DataFrame {
439 fn type_id(&self) -> TypeId {
440 TypeId::of::<Self>()
441 }
442
443 fn measure(&self, constraints: Constraints) -> Size {
444 let total_width: usize = self.columns.iter().map(|c| c.width + 1).sum();
445 Size::new(
446 constraints.max_width.min(total_width as f32 + 5.0),
447 constraints.max_height.min(self.visible_rows as f32 + 2.0),
448 )
449 }
450
451 fn layout(&mut self, bounds: Rect) -> LayoutResult {
452 self.bounds = bounds;
453 self.visible_rows = (bounds.height as usize).saturating_sub(2);
454 LayoutResult {
455 size: Size::new(bounds.width, bounds.height),
456 }
457 }
458
459 fn paint(&self, canvas: &mut dyn Canvas) {
460 if self.bounds.width < 10.0 || self.bounds.height < 3.0 {
461 return;
462 }
463
464 let header_style = TextStyle {
465 color: Color::new(0.9, 0.9, 0.9, 1.0),
466 ..Default::default()
467 };
468
469 let row_num_style = TextStyle {
470 color: Color::new(0.5, 0.5, 0.5, 1.0),
471 ..Default::default()
472 };
473
474 let selected_style = TextStyle {
475 color: Color::new(0.2, 0.2, 0.2, 1.0),
476 ..Default::default()
477 };
478
479 let row_num_width = if self.show_row_numbers { 5 } else { 0 };
480 let mut y = self.bounds.y;
481
482 if self.show_header {
484 let mut x = self.bounds.x + row_num_width as f32;
485
486 if self.show_row_numbers {
487 canvas.draw_text("#", Point::new(self.bounds.x, y), &row_num_style);
488 }
489
490 for col in &self.columns {
491 let header: String = col.name.chars().take(col.width).collect();
492 canvas.draw_text(&header, Point::new(x, y), &header_style);
493 x += col.width as f32 + 1.0;
494 }
495 y += 1.0;
496
497 let sep_width = (self.bounds.width as usize).min(120);
499 canvas.draw_text(
500 &"─".repeat(sep_width),
501 Point::new(self.bounds.x, y),
502 &row_num_style,
503 );
504 y += 1.0;
505 }
506
507 let row_count = self.row_count();
509 let end_row = (self.scroll_offset + self.visible_rows).min(row_count);
510
511 for row_idx in self.scroll_offset..end_row {
512 let mut x = self.bounds.x + row_num_width as f32;
513 let is_selected = self.selected_row == Some(row_idx);
514
515 if self.show_row_numbers {
517 let num = format!("{row_idx:>4}");
518 canvas.draw_text(&num, Point::new(self.bounds.x, y), &row_num_style);
519 }
520
521 for col in &self.columns {
523 if let Some(value) = col.values.get(row_idx) {
524 let (content, color) = self.render_cell(value, col.width, col.align);
525
526 let style = if is_selected {
527 selected_style.clone()
528 } else {
529 TextStyle {
530 color,
531 ..Default::default()
532 }
533 };
534
535 canvas.draw_text(&content, Point::new(x, y), &style);
536 }
537 x += col.width as f32 + 1.0;
538 }
539
540 y += 1.0;
541 }
542 }
543
544 fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
545 if let Event::KeyDown { key, .. } = event {
546 match key {
547 Key::Up | Key::K => {
548 if let Some(row) = self.selected_row {
549 if row > 0 {
550 self.selected_row = Some(row - 1);
551 if row - 1 < self.scroll_offset {
552 self.scroll_offset = row - 1;
553 }
554 }
555 } else if self.row_count() > 0 {
556 self.selected_row = Some(0);
557 }
558 }
559 Key::Down | Key::J => {
560 let row_count = self.row_count();
561 if let Some(row) = self.selected_row {
562 if row + 1 < row_count {
563 self.selected_row = Some(row + 1);
564 if row + 1 >= self.scroll_offset + self.visible_rows {
565 self.scroll_offset = (row + 2).saturating_sub(self.visible_rows);
566 }
567 }
568 } else if row_count > 0 {
569 self.selected_row = Some(0);
570 }
571 }
572 Key::PageUp => {
573 self.scroll_offset = self.scroll_offset.saturating_sub(self.visible_rows);
574 if let Some(row) = self.selected_row {
575 if row >= self.visible_rows {
576 self.selected_row = Some(row - self.visible_rows);
577 } else {
578 self.selected_row = Some(0);
579 }
580 }
581 }
582 Key::PageDown => {
583 let row_count = self.row_count();
584 self.scroll_offset = (self.scroll_offset + self.visible_rows)
585 .min(row_count.saturating_sub(self.visible_rows));
586 if let Some(row) = self.selected_row {
587 let new_row = (row + self.visible_rows).min(row_count.saturating_sub(1));
588 self.selected_row = Some(new_row);
589 }
590 }
591 _ => {}
592 }
593 }
594 None
595 }
596
597 fn children(&self) -> &[Box<dyn Widget>] {
598 &[]
599 }
600
601 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
602 &mut []
603 }
604}
605
606impl Brick for DataFrame {
607 fn brick_name(&self) -> &'static str {
608 "DataFrame"
609 }
610
611 fn assertions(&self) -> &[BrickAssertion] {
612 static ASSERTIONS: &[BrickAssertion] = &[
613 BrickAssertion::max_latency_ms(16),
614 BrickAssertion::max_latency_ms(50), ];
616 ASSERTIONS
617 }
618
619 fn budget(&self) -> BrickBudget {
620 BrickBudget::uniform(16)
621 }
622
623 fn verify(&self) -> BrickVerification {
624 let mut passed = Vec::new();
625 let mut failed = Vec::new();
626
627 if self.bounds.width >= 10.0 && self.bounds.height >= 3.0 {
629 passed.push(BrickAssertion::max_latency_ms(16));
630 } else {
631 failed.push((
632 BrickAssertion::max_latency_ms(16),
633 "Size too small".to_string(),
634 ));
635 }
636
637 let row_count = self.row_count();
639 for col in &self.columns {
640 if col.values.len() != row_count {
641 failed.push((
642 BrickAssertion::max_latency_ms(16),
643 format!("Column {} has inconsistent length", col.name),
644 ));
645 }
646 }
647
648 BrickVerification {
649 passed,
650 failed,
651 verification_time: Duration::from_micros(10),
652 }
653 }
654
655 fn to_html(&self) -> String {
656 String::new()
657 }
658
659 fn to_css(&self) -> String {
660 String::new()
661 }
662}
663
664#[cfg(test)]
665mod tests {
666 use super::*;
667 use crate::direct::{CellBuffer, DirectTerminalCanvas};
668
669 #[test]
672 fn test_dataframe_new() {
673 let df = DataFrame::new();
674 assert_eq!(df.row_count(), 0);
675 assert_eq!(df.column_count(), 0);
676 }
677
678 #[test]
679 fn test_dataframe_default() {
680 let df = DataFrame::default();
681 assert_eq!(df.row_count(), 0);
682 assert_eq!(df.column_count(), 0);
683 }
684
685 #[test]
686 fn test_dataframe_with_columns() {
687 let df = DataFrame::new()
688 .with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]))
689 .with_column(Column::from_f64("B", &[4.0, 5.0, 6.0]));
690 assert_eq!(df.row_count(), 3);
691 assert_eq!(df.column_count(), 2);
692 }
693
694 #[test]
695 fn test_dataframe_with_visible_rows() {
696 let df = DataFrame::new().with_visible_rows(50);
697 assert_eq!(df.visible_rows, 50);
698 }
699
700 #[test]
701 fn test_dataframe_with_header() {
702 let df = DataFrame::new().with_header(false);
703 assert!(!df.show_header);
704 }
705
706 #[test]
707 fn test_dataframe_with_row_numbers() {
708 let df = DataFrame::new().with_row_numbers(false);
709 assert!(!df.show_row_numbers);
710 }
711
712 #[test]
715 fn test_cell_value_null() {
716 let (rendered, _) = CellValue::Null.render(5);
717 assert!(rendered.is_empty());
718 }
719
720 #[test]
721 fn test_cell_value_bool_true() {
722 let (rendered, _) = CellValue::Bool(true).render(5);
723 assert_eq!(rendered, "true");
724 }
725
726 #[test]
727 fn test_cell_value_bool_false() {
728 let (rendered, _) = CellValue::Bool(false).render(5);
729 assert_eq!(rendered, "false");
730 }
731
732 #[test]
733 fn test_cell_value_int64() {
734 let (rendered, _) = CellValue::Int64(42).render(5);
735 assert_eq!(rendered, "42");
736 }
737
738 #[test]
739 fn test_cell_value_float64() {
740 let (rendered, _) = CellValue::Float64(1.23456).render(10);
741 assert!(rendered.contains("1.23"));
742 }
743
744 #[test]
745 fn test_cell_value_string() {
746 let (rendered, _) = CellValue::String(CompactString::from("hello")).render(10);
747 assert_eq!(rendered, "hello");
748 }
749
750 #[test]
751 fn test_cell_value_render_sparkline() {
752 let values = vec![1.0, 5.0, 3.0, 8.0, 2.0];
753 let (rendered, _) = CellValue::Sparkline(values).render(5);
754 assert_eq!(rendered.chars().count(), 5);
755 }
756
757 #[test]
758 fn test_cell_value_render_sparkline_empty() {
759 let (rendered, _) = CellValue::Sparkline(vec![]).render(5);
760 assert_eq!(rendered.len(), 5);
761 }
762
763 #[test]
764 fn test_cell_value_render_sparkline_with_nan() {
765 let values = vec![1.0, f64::NAN, 3.0, f64::INFINITY, 2.0];
766 let (rendered, _) = CellValue::Sparkline(values).render(5);
767 assert_eq!(rendered.chars().count(), 5);
769 }
770
771 #[test]
772 fn test_cell_value_render_sparkline_constant() {
773 let values = vec![5.0, 5.0, 5.0, 5.0];
775 let (rendered, _) = CellValue::Sparkline(values).render(4);
776 assert_eq!(rendered.chars().count(), 4);
777 }
778
779 #[test]
780 fn test_cell_value_render_progress() {
781 let (rendered, _) = CellValue::ProgressBar(50.0).render(10);
782 assert!(rendered.contains("50%"));
783 }
784
785 #[test]
786 fn test_cell_value_render_progress_zero() {
787 let (rendered, _) = CellValue::ProgressBar(0.0).render(10);
788 assert!(rendered.contains("0%"));
789 }
790
791 #[test]
792 fn test_cell_value_render_progress_hundred() {
793 let (rendered, _) = CellValue::ProgressBar(100.0).render(10);
794 assert!(rendered.contains("100%"));
795 }
796
797 #[test]
798 fn test_cell_value_render_progress_clamp() {
799 let (rendered, _) = CellValue::ProgressBar(150.0).render(10);
801 assert!(rendered.contains("100%"));
802
803 let (rendered2, _) = CellValue::ProgressBar(-10.0).render(10);
804 assert!(rendered2.contains("0%"));
805 }
806
807 #[test]
808 fn test_cell_value_render_trend_up() {
809 let (rendered, color) = CellValue::TrendArrow(0.15).render(10);
810 assert!(rendered.contains('↑'));
811 assert!(color.g > color.r); }
813
814 #[test]
815 fn test_cell_value_render_trend_slight_up() {
816 let (rendered, _) = CellValue::TrendArrow(0.05).render(10);
817 assert!(rendered.contains('↗'));
818 }
819
820 #[test]
821 fn test_cell_value_render_trend_flat() {
822 let (rendered, _) = CellValue::TrendArrow(0.0).render(10);
823 assert!(rendered.contains('→'));
824 }
825
826 #[test]
827 fn test_cell_value_render_trend_slight_down() {
828 let (rendered, _) = CellValue::TrendArrow(-0.05).render(10);
829 assert!(rendered.contains('↘'));
830 }
831
832 #[test]
833 fn test_cell_value_render_trend_down() {
834 let (rendered, color) = CellValue::TrendArrow(-0.15).render(10);
835 assert!(rendered.contains('↓'));
836 assert!(color.r > color.g); }
838
839 #[test]
840 fn test_cell_value_render_status() {
841 let (rendered, color) = CellValue::StatusDot(StatusLevel::Ok).render(1);
842 assert_eq!(rendered, "●");
843 assert!(color.g > 0.5);
844 }
845
846 #[test]
847 fn test_cell_value_render_microbar() {
848 let (rendered, _) = CellValue::MicroBar {
849 value: 5.0,
850 max: 10.0,
851 }
852 .render(10);
853 assert!(rendered.contains('█'));
854 assert!(rendered.contains('░'));
855 }
856
857 #[test]
858 fn test_cell_value_render_microbar_full() {
859 let (rendered, _) = CellValue::MicroBar {
860 value: 10.0,
861 max: 10.0,
862 }
863 .render(10);
864 assert_eq!(rendered.chars().filter(|&c| c == '█').count(), 10);
866 }
867
868 #[test]
869 fn test_cell_value_render_microbar_zero_max() {
870 let (rendered, _) = CellValue::MicroBar {
871 value: 5.0,
872 max: 0.0,
873 }
874 .render(10);
875 assert!(!rendered.is_empty());
877 }
878
879 #[test]
880 fn test_cell_value_default() {
881 let value = CellValue::default();
882 assert!(matches!(value, CellValue::Null));
883 }
884
885 #[test]
888 fn test_sparkbar() {
889 let values = vec![0.25, 0.5, 0.75, 1.0];
890 let (rendered, _) = CellValue::SparkBar(values).render(4);
891 assert_eq!(rendered.chars().count(), 4);
892 }
893
894 #[test]
895 fn test_sparkbar_empty() {
896 let (rendered, _) = CellValue::SparkBar(vec![]).render(5);
897 assert_eq!(rendered.len(), 5);
898 }
899
900 #[test]
901 fn test_sparkbar_with_nan() {
902 let values = vec![0.5, f64::NAN, 0.75];
903 let (rendered, _) = CellValue::SparkBar(values).render(3);
904 assert_eq!(rendered.chars().count(), 3);
905 }
906
907 #[test]
908 fn test_sparkbar_with_negative() {
909 let values = vec![0.5, -0.5, 0.75];
910 let (rendered, _) = CellValue::SparkBar(values).render(3);
911 assert_eq!(rendered.chars().count(), 3);
912 }
913
914 #[test]
917 fn test_sparkwinloss() {
918 let values = vec![1, -1, 0, 1, -1];
919 let (rendered, _) = CellValue::SparkWinLoss(values).render(5);
920 assert!(rendered.contains('▲'));
921 assert!(rendered.contains('▼'));
922 assert!(rendered.contains('─'));
923 }
924
925 #[test]
926 fn test_sparkwinloss_empty() {
927 let values: Vec<i8> = vec![];
928 let (rendered, _) = CellValue::SparkWinLoss(values).render(5);
929 assert!(rendered.is_empty());
930 }
931
932 #[test]
935 fn test_status_levels() {
936 assert!(matches!(StatusLevel::default(), StatusLevel::Ok));
937
938 let (_, ok_color) = StatusLevel::Ok.render();
939 let (_, warn_color) = StatusLevel::Warning.render();
940 let (_, crit_color) = StatusLevel::Critical.render();
941
942 assert!(ok_color.g > ok_color.r);
943 assert!(warn_color.r > 0.5 && warn_color.g > 0.5);
944 assert!(crit_color.r > crit_color.g);
945 }
946
947 #[test]
948 fn test_status_level_unknown() {
949 let (ch, _) = StatusLevel::Unknown.render();
950 assert_eq!(ch, '○');
951 }
952
953 #[test]
954 fn test_status_level_clone_eq() {
955 let s1 = StatusLevel::Ok;
956 let s2 = s1;
957 assert_eq!(s1, s2);
958 }
959
960 #[test]
961 fn test_status_level_debug() {
962 let status = StatusLevel::Warning;
963 let debug = format!("{:?}", status);
964 assert!(debug.contains("Warning"));
965 }
966
967 #[test]
970 fn test_column_new() {
971 let col = Column::new("Test");
972 assert_eq!(col.name.as_str(), "Test");
973 assert!(col.values.is_empty());
974 assert_eq!(col.width, 10);
975 }
976
977 #[test]
978 fn test_column_alignment() {
979 let col = Column::new("Test")
980 .with_align(ColumnAlign::Right)
981 .with_width(10);
982 assert!(matches!(col.align, ColumnAlign::Right));
983 assert_eq!(col.width, 10);
984 }
985
986 #[test]
987 fn test_column_with_values() {
988 let col = Column::new("Test").with_values(vec![CellValue::Int64(1), CellValue::Int64(2)]);
989 assert_eq!(col.values.len(), 2);
990 }
991
992 #[test]
993 fn test_column_from_f64() {
994 let col = Column::from_f64("Numbers", &[1.0, 2.0, 3.0]);
995 assert_eq!(col.values.len(), 3);
996 assert!(matches!(col.align, ColumnAlign::Right));
997 }
998
999 #[test]
1000 fn test_column_from_i64() {
1001 let col = Column::from_i64("Ints", &[1, 2, 3]);
1002 assert_eq!(col.values.len(), 3);
1003 assert!(matches!(col.align, ColumnAlign::Right));
1004 }
1005
1006 #[test]
1007 fn test_column_from_strings() {
1008 let col = Column::from_strings("Names", &["Alice", "Bob"]);
1009 assert_eq!(col.values.len(), 2);
1010 assert!(matches!(col.align, ColumnAlign::Left));
1011 assert_eq!(col.width, 15);
1012 }
1013
1014 #[test]
1015 fn test_column_sparkline_from_rows() {
1016 let rows = vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]];
1017 let col = Column::sparkline_from_rows("Sparklines", rows);
1018 assert_eq!(col.values.len(), 2);
1019 assert_eq!(col.width, 12);
1020 }
1021
1022 #[test]
1023 fn test_column_clone() {
1024 let col = Column::new("Test").with_width(20);
1025 let cloned = col.clone();
1026 assert_eq!(cloned.name, col.name);
1027 assert_eq!(cloned.width, 20);
1028 }
1029
1030 #[test]
1031 fn test_column_debug() {
1032 let col = Column::new("Test");
1033 let debug = format!("{:?}", col);
1034 assert!(debug.contains("Column"));
1035 }
1036
1037 #[test]
1040 fn test_column_align_default() {
1041 let align = ColumnAlign::default();
1042 assert!(matches!(align, ColumnAlign::Left));
1043 }
1044
1045 #[test]
1046 fn test_column_align_center() {
1047 let col = Column::new("Test").with_align(ColumnAlign::Center);
1048 assert!(matches!(col.align, ColumnAlign::Center));
1049 }
1050
1051 #[test]
1054 fn test_dataframe_sparkline_column() {
1055 let mut df = DataFrame::new()
1056 .with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]))
1057 .with_column(Column::from_f64("B", &[4.0, 5.0, 6.0]))
1058 .with_column(Column::from_f64("C", &[7.0, 8.0, 9.0]));
1059
1060 df.add_sparkline_column("Trend", &[0, 1, 2]);
1061 assert_eq!(df.column_count(), 4);
1062
1063 let sparkline_col = &df.columns[3];
1065 assert_eq!(sparkline_col.values.len(), 3);
1066 }
1067
1068 #[test]
1069 fn test_dataframe_sparkline_column_invalid_source() {
1070 let mut df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]));
1071
1072 df.add_sparkline_column("Trend", &[0, 99]);
1074 assert_eq!(df.column_count(), 2);
1075 }
1076
1077 #[test]
1080 fn test_dataframe_layout() {
1081 let mut df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]));
1082 let bounds = Rect::new(0.0, 0.0, 80.0, 24.0);
1083 let result = df.layout(bounds);
1084 assert!(result.size.width > 0.0);
1085 }
1086
1087 #[test]
1088 fn test_dataframe_paint() {
1089 let mut df = DataFrame::new()
1090 .with_column(Column::from_strings("Name", &["Alice", "Bob", "Carol"]))
1091 .with_column(Column::from_f64("Score", &[95.0, 87.0, 92.0]));
1092
1093 let bounds = Rect::new(0.0, 0.0, 80.0, 24.0);
1094 df.layout(bounds);
1095
1096 let mut buffer = CellBuffer::new(80, 24);
1097 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1098 df.paint(&mut canvas);
1099 }
1100
1101 #[test]
1102 fn test_dataframe_paint_small_bounds() {
1103 let mut df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0]));
1104
1105 df.bounds = Rect::new(0.0, 0.0, 5.0, 2.0); let mut buffer = CellBuffer::new(10, 10);
1108 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1109 df.paint(&mut canvas); }
1111
1112 #[test]
1113 fn test_dataframe_paint_no_header() {
1114 let mut df = DataFrame::new()
1115 .with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]))
1116 .with_header(false);
1117
1118 let bounds = Rect::new(0.0, 0.0, 80.0, 24.0);
1119 df.layout(bounds);
1120
1121 let mut buffer = CellBuffer::new(80, 24);
1122 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1123 df.paint(&mut canvas);
1124 }
1125
1126 #[test]
1127 fn test_dataframe_paint_no_row_numbers() {
1128 let mut df = DataFrame::new()
1129 .with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]))
1130 .with_row_numbers(false);
1131
1132 let bounds = Rect::new(0.0, 0.0, 80.0, 24.0);
1133 df.layout(bounds);
1134
1135 let mut buffer = CellBuffer::new(80, 24);
1136 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1137 df.paint(&mut canvas);
1138 }
1139
1140 #[test]
1141 fn test_dataframe_paint_with_selection() {
1142 let mut df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]));
1143 df.select_row(Some(1));
1144
1145 let bounds = Rect::new(0.0, 0.0, 80.0, 24.0);
1146 df.layout(bounds);
1147
1148 let mut buffer = CellBuffer::new(80, 24);
1149 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1150 df.paint(&mut canvas);
1151 }
1152
1153 #[test]
1154 fn test_dataframe_paint_with_all_cell_types() {
1155 let mut df = DataFrame::new().with_column(Column::new("Types").with_values(vec![
1156 CellValue::Null,
1157 CellValue::Bool(true),
1158 CellValue::Int64(42),
1159 CellValue::Float64(1.23),
1160 CellValue::String(CompactString::from("text")),
1161 ]));
1162
1163 let bounds = Rect::new(0.0, 0.0, 80.0, 24.0);
1164 df.layout(bounds);
1165
1166 let mut buffer = CellBuffer::new(80, 24);
1167 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1168 df.paint(&mut canvas);
1169 }
1170
1171 #[test]
1174 fn test_dataframe_scroll() {
1175 let mut df = DataFrame::new().with_column(Column::from_f64(
1176 "A",
1177 &(0..100).map(|i| i as f64).collect::<Vec<_>>(),
1178 ));
1179
1180 df.visible_rows = 10;
1181 df.scroll_to(50);
1182 assert_eq!(df.scroll_offset, 50);
1183 }
1184
1185 #[test]
1186 fn test_dataframe_scroll_beyond_end() {
1187 let mut df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]));
1188 df.visible_rows = 10;
1189 df.scroll_to(100); assert!(df.scroll_offset <= df.row_count());
1192 }
1193
1194 #[test]
1195 fn test_dataframe_select() {
1196 let mut df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]));
1197
1198 df.select_row(Some(1));
1199 assert_eq!(df.selected_row, Some(1));
1200
1201 df.select_row(None);
1202 assert_eq!(df.selected_row, None);
1203 }
1204
1205 #[test]
1208 fn test_dataframe_event_up() {
1209 let mut df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]));
1210 df.select_row(Some(2));
1211
1212 let result = df.event(&Event::key_down(Key::Up));
1213 assert!(result.is_none());
1214 assert_eq!(df.selected_row, Some(1));
1215 }
1216
1217 #[test]
1218 fn test_dataframe_event_down() {
1219 let mut df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]));
1220 df.select_row(Some(0));
1221
1222 let result = df.event(&Event::key_down(Key::Down));
1223 assert!(result.is_none());
1224 assert_eq!(df.selected_row, Some(1));
1225 }
1226
1227 #[test]
1228 fn test_dataframe_event_k() {
1229 let mut df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]));
1230 df.select_row(Some(2));
1231
1232 let _ = df.event(&Event::key_down(Key::K));
1233 assert_eq!(df.selected_row, Some(1));
1234 }
1235
1236 #[test]
1237 fn test_dataframe_event_j() {
1238 let mut df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]));
1239 df.select_row(Some(0));
1240
1241 let _ = df.event(&Event::key_down(Key::J));
1242 assert_eq!(df.selected_row, Some(1));
1243 }
1244
1245 #[test]
1246 fn test_dataframe_event_up_at_top() {
1247 let mut df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]));
1248 df.select_row(Some(0));
1249
1250 let _ = df.event(&Event::key_down(Key::Up));
1251 assert_eq!(df.selected_row, Some(0)); }
1253
1254 #[test]
1255 fn test_dataframe_event_down_at_bottom() {
1256 let mut df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]));
1257 df.select_row(Some(2));
1258
1259 let _ = df.event(&Event::key_down(Key::Down));
1260 assert_eq!(df.selected_row, Some(2)); }
1262
1263 #[test]
1264 fn test_dataframe_event_up_no_selection() {
1265 let mut df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]));
1266 df.select_row(None);
1267
1268 let _ = df.event(&Event::key_down(Key::Up));
1269 assert_eq!(df.selected_row, Some(0)); }
1271
1272 #[test]
1273 fn test_dataframe_event_down_no_selection() {
1274 let mut df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]));
1275 df.select_row(None);
1276
1277 let _ = df.event(&Event::key_down(Key::Down));
1278 assert_eq!(df.selected_row, Some(0)); }
1280
1281 #[test]
1282 fn test_dataframe_event_pageup() {
1283 let mut df = DataFrame::new().with_column(Column::from_f64(
1284 "A",
1285 &(0..50).map(|i| i as f64).collect::<Vec<_>>(),
1286 ));
1287 df.visible_rows = 10;
1288 df.scroll_offset = 30;
1289 df.select_row(Some(35));
1290
1291 let _ = df.event(&Event::key_down(Key::PageUp));
1292 assert!(df.scroll_offset < 30);
1293 }
1294
1295 #[test]
1296 fn test_dataframe_event_pagedown() {
1297 let mut df = DataFrame::new().with_column(Column::from_f64(
1298 "A",
1299 &(0..50).map(|i| i as f64).collect::<Vec<_>>(),
1300 ));
1301 df.visible_rows = 10;
1302 df.scroll_offset = 0;
1303 df.select_row(Some(5));
1304
1305 let _ = df.event(&Event::key_down(Key::PageDown));
1306 assert!(df.scroll_offset > 0);
1307 }
1308
1309 #[test]
1310 fn test_dataframe_event_other_key() {
1311 let mut df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0]));
1312 df.select_row(Some(0));
1313
1314 let _ = df.event(&Event::key_down(Key::A));
1315 assert_eq!(df.selected_row, Some(0));
1317 }
1318
1319 #[test]
1320 fn test_dataframe_event_non_keydown() {
1321 let mut df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0]));
1322 df.select_row(Some(0));
1323
1324 let _ = df.event(&Event::FocusIn);
1325 assert_eq!(df.selected_row, Some(0));
1327 }
1328
1329 #[test]
1332 fn test_dataframe_measure() {
1333 let df =
1334 DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]).with_width(10));
1335 let size = df.measure(Constraints {
1336 min_width: 0.0,
1337 max_width: 100.0,
1338 min_height: 0.0,
1339 max_height: 50.0,
1340 });
1341 assert!(size.width > 0.0);
1342 assert!(size.height > 0.0);
1343 }
1344
1345 #[test]
1346 fn test_dataframe_type_id() {
1347 let df = DataFrame::new();
1348 let type_id = Widget::type_id(&df);
1349 assert_eq!(type_id, TypeId::of::<DataFrame>());
1350 }
1351
1352 #[test]
1353 fn test_dataframe_children() {
1354 let df = DataFrame::new();
1355 assert!(df.children().is_empty());
1356 }
1357
1358 #[test]
1359 fn test_dataframe_children_mut() {
1360 let mut df = DataFrame::new();
1361 assert!(df.children_mut().is_empty());
1362 }
1363
1364 #[test]
1367 fn test_dataframe_brick_name() {
1368 let df = DataFrame::new();
1369 assert_eq!(df.brick_name(), "DataFrame");
1370 }
1371
1372 #[test]
1373 fn test_dataframe_assertions() {
1374 let df = DataFrame::new();
1375 assert!(!df.assertions().is_empty());
1376 }
1377
1378 #[test]
1379 fn test_dataframe_budget() {
1380 let df = DataFrame::new();
1381 let budget = df.budget();
1382 assert!(budget.measure_ms > 0);
1383 }
1384
1385 #[test]
1386 fn test_dataframe_verify() {
1387 let mut df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]));
1388 df.bounds = Rect::new(0.0, 0.0, 80.0, 24.0);
1389 assert!(df.verify().is_valid());
1390 }
1391
1392 #[test]
1393 fn test_dataframe_verify_small_bounds() {
1394 let mut df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]));
1395 df.bounds = Rect::new(0.0, 0.0, 5.0, 2.0);
1396 let verification = df.verify();
1397 assert!(!verification.failed.is_empty());
1398 }
1399
1400 #[test]
1401 fn test_dataframe_verify_inconsistent_columns() {
1402 let mut df = DataFrame::new()
1403 .with_column(Column::from_f64("A", &[1.0, 2.0, 3.0]))
1404 .with_column(Column::from_f64("B", &[1.0, 2.0])); df.bounds = Rect::new(0.0, 0.0, 80.0, 24.0);
1406 let verification = df.verify();
1407 assert!(!verification.failed.is_empty());
1409 }
1410
1411 #[test]
1412 fn test_dataframe_to_html() {
1413 let df = DataFrame::new();
1414 assert!(df.to_html().is_empty());
1415 }
1416
1417 #[test]
1418 fn test_dataframe_to_css() {
1419 let df = DataFrame::new();
1420 assert!(df.to_css().is_empty());
1421 }
1422
1423 #[test]
1426 fn test_dataframe_clone() {
1427 let df = DataFrame::new().with_column(Column::from_f64("A", &[1.0, 2.0]));
1428 let cloned = df;
1429 assert_eq!(cloned.column_count(), 1);
1430 assert_eq!(cloned.row_count(), 2);
1431 }
1432
1433 #[test]
1434 fn test_dataframe_debug() {
1435 let df = DataFrame::new();
1436 let debug = format!("{:?}", df);
1437 assert!(debug.contains("DataFrame"));
1438 }
1439
1440 #[test]
1443 fn test_cell_value_clone() {
1444 let value = CellValue::Int64(42);
1445 let cloned = value;
1446 assert!(matches!(cloned, CellValue::Int64(42)));
1447 }
1448
1449 #[test]
1450 fn test_cell_value_debug() {
1451 let value = CellValue::String(CompactString::from("test"));
1452 let debug = format!("{:?}", value);
1453 assert!(debug.contains("String"));
1454 }
1455
1456 #[test]
1459 fn test_render_cell_left_align() {
1460 let df = DataFrame::new();
1461 let value = CellValue::String(CompactString::from("hi"));
1462 let (content, _) = df.render_cell(&value, 10, ColumnAlign::Left);
1463 assert!(content.starts_with("hi"));
1464 }
1465
1466 #[test]
1467 fn test_render_cell_right_align() {
1468 let df = DataFrame::new();
1469 let value = CellValue::String(CompactString::from("hi"));
1470 let (content, _) = df.render_cell(&value, 10, ColumnAlign::Right);
1471 assert!(content.ends_with("hi"));
1472 }
1473
1474 #[test]
1475 fn test_render_cell_center_align() {
1476 let df = DataFrame::new();
1477 let value = CellValue::String(CompactString::from("hi"));
1478 let (content, _) = df.render_cell(&value, 10, ColumnAlign::Center);
1479 assert!(content.len() <= 10);
1481 }
1482}