Skip to main content

flux_tui/components/
datagrid.rs

1use crossterm::event::KeyCode;
2use unicode_width::UnicodeWidthStr;
3
4use crate::canvas::*;
5
6use std::rc::Rc;
7
8use super::*;
9
10pub type DataGridSelectionChangedCallback = dyn Fn(&mut DataGrid);
11pub type DataGridKeyHandler = fn(&mut DataGrid, event: &mut WindowEvent);
12
13#[derive(Debug)]
14pub enum DataValue<'a> {
15	String(&'a str),
16	Integer(i32),
17	Boolean(bool),
18	Float(f32),
19}
20
21impl<'a> DataValue<'a> {
22	/// Returns the minimum size to render the data
23	/// `radix` specifies the base for numbers (i.e. i32 or f32)
24	pub fn width(&self, radix: Radix) -> TSize {
25		match self {
26			Self::String(s) => s.width() as TSize,
27			Self::Integer(i) => {
28				i.abs().checked_ilog(radix as i32).unwrap_or(0) as TSize
29					+ 1 + i.is_negative() as TSize
30					+ radix.prefix_glyphs().len() as TSize
31			}
32			Self::Float(f) => {
33				if f.is_nan() {
34					return 3;
35				}
36				else if *f == f32::INFINITY {
37					return 1;
38				}
39				else if *f == f32::NEG_INFINITY {
40					return 2;
41				}
42				let sign = (f.is_sign_negative() && *f != 0.0) as TSize;
43				let fract = {
44					let mut f2 = f.fract();
45					let mut cnt = 0;
46					while f2 != 0.0 {
47						f2 = (f2 * radix as usize as f32).fract();
48						cnt += 1;
49					}
50					cnt + 1
51				};
52				let int = (f.trunc() as i32)
53					.abs()
54					.checked_ilog(radix as i32)
55					.unwrap_or(0) as TSize
56					+ 1;
57				sign + fract + int + radix.prefix_glyphs().len() as TSize
58			}
59			Self::Boolean(_) => 3,
60		}
61	}
62}
63
64pub trait DataRow {
65	/// Returns the data of the cell
66	/// This will return None when the internal value is not set (NULL) or the column doesn't exist
67	fn get_data<'a>(&'a self, column: usize) -> Option<DataValue<'a>>;
68}
69
70pub trait DataSource {
71	/// Definitions of the columns
72	fn column_definitions(&self) -> &[ColumnDefiniton];
73
74	/// Number of rows
75	fn rows(&self) -> usize;
76
77	/// Returns the corresponding row data
78	/// This will panic if the row doesn't exist
79	fn get_row(&self, row: usize) -> &dyn DataRow;
80}
81
82#[derive(Default)]
83struct EmptyRow {}
84
85impl DataRow for EmptyRow {
86	fn get_data<'a>(&'a self, _column: usize) -> Option<DataValue<'a>> {
87		None
88	}
89}
90
91#[derive(Default)]
92struct EmptyDataSource(EmptyRow);
93
94impl DataSource for EmptyDataSource {
95	fn column_definitions(&self) -> &[ColumnDefiniton] {
96		&[]
97	}
98
99	fn rows(&self) -> usize {
100		usize::MIN
101	}
102
103	fn get_row(&self, _row: usize) -> &dyn DataRow {
104		&self.0
105	}
106}
107
108
109#[derive(Clone, PartialEq)]
110pub struct ColumnDefiniton {
111	name: String,
112	header_width: TSize,
113}
114
115impl ColumnDefiniton {
116	pub fn new<S: ToString>(name: S) -> Self {
117		let n = name.to_string();
118		let width = n.width() as TSize;
119		Self {
120			name: n,
121			header_width: width,
122		}
123	}
124}
125
126/// A grid-based component to dynamically present many different
127/// values (of possibly different datatypes)
128/// Design:
129/// ```text
130///  Row | Column1 | Column2 | Column3 |
131/// ─────┼─────────┼─────────┼─────────|
132///   1  | <NULL>  |  true   | textabc |
133///   2  | <NULL>  |  false  | <EMPTY> |
134///   3  |  1234   |  true   |   abc   |
135///```
136pub struct DataGrid {
137	base: WidgetBase,
138	data: Box<dyn DataSource>,
139	offset: Vector2D<usize>,
140	selection: Vector2D<usize>,
141	render_size: Vector2D<TSize>,
142	callback: Rc<DataGridSelectionChangedCallback>,
143	avail_size: TPoint,
144	col_widths: Vec<TSize>,
145	show_cols: usize,
146	key_handler: DataGridKeyHandler,
147	radix: Radix,
148	sel_color: Option<Color>,
149}
150
151impl Default for DataGrid {
152	fn default() -> Self {
153		Self::new()
154	}
155}
156
157impl DataGrid {
158	pub fn new() -> Self {
159		let mut s = Self {
160			base: WidgetBase::default(),
161			data: Box::new(EmptyDataSource::default()),
162			offset: Vector2D::default(),
163			selection: Vector2D::default(),
164			render_size: Vector2D::default(),
165			avail_size: Default::default(),
166			col_widths: Vec::default(),
167			show_cols: 0,
168			radix: Radix::default(),
169			sel_color: None,
170			callback: Rc::new(Self::default_callback),
171			key_handler: Self::default_key_handler,
172		};
173		s.base.constraints.y.min = NonZero::new(3).unwrap();
174		s.base.constraints.y.max = Size::Relative(Percent::from_int(100));
175		s
176	}
177
178	pub fn default_callback(_: &mut Self) {}
179
180	pub fn default_key_handler(&mut self, event: &mut WindowEvent) {
181		if let Event::Key(k) = event.raw() {
182			match k.code {
183				KeyCode::Up => self.move_selection_up(),
184				KeyCode::Down => self.move_selection_down(),
185				KeyCode::Left => self.move_selection_left(),
186				KeyCode::Right => self.move_selection_right(),
187				KeyCode::PageUp => self.move_page_up(),
188				KeyCode::PageDown => self.move_page_down(),
189				_ => {}
190			}
191		}
192	}
193
194	pub fn set_key_handler(&mut self, f: DataGridKeyHandler) {
195		self.key_handler = f;
196	}
197
198	pub fn set_callback<F: Fn(&mut Self) + 'static>(&mut self, callback: F) {
199		self.callback = Rc::new(callback);
200	}
201
202	pub fn set_radix(&mut self, radix: Radix) {
203		self.radix = radix;
204		self.reevaluate_columns();
205	}
206
207	pub fn set_selection_color(&mut self, color: Option<Color>) {
208		self.sel_color = color;
209	}
210
211	pub fn move_selection_up(&mut self) {
212		if self.selection.y > usize::MIN {
213			self.selection.y -= 1;
214			if self.offset.y > self.selection.y {
215				self.offset.y -= 1;
216			}
217			self.selection_changed();
218		}
219	}
220
221	pub fn move_selection_down(&mut self) {
222		if self.selection.y < self.data.rows() - 1 {
223			self.selection.y += 1;
224			if self.selection.y >= self.rendered_rows() + self.offset.y {
225				self.offset.y += 1;
226			}
227			self.selection_changed();
228		}
229	}
230
231	pub fn move_selection_left(&mut self) {
232		if self.selection.x > usize::MIN {
233			self.selection.x -= 1;
234			if self.offset.x > self.selection.x {
235				self.offset.x -= 1;
236				self.reevaluate_columns_range(true);
237			}
238			self.selection_changed();
239		}
240	}
241
242	pub fn move_selection_right(&mut self) {
243		if self.selection.x < self.data.column_definitions().len() - 1 {
244			self.selection.x += 1;
245			if self.selection.x > self.show_cols - 1 {
246				self.offset.x += 1;
247				self.reevaluate_columns_range(true);
248			}
249			self.selection_changed();
250		}
251	}
252
253	fn selection_changed(&mut self) {
254		let cb = self.callback.clone();
255		cb(self);
256	}
257
258	pub fn move_page_up(&mut self) {
259		self.selection.y = self.selection.y.saturating_sub(self.rendered_rows());
260		self.offset.y = self.offset.y.saturating_sub(self.rendered_rows());
261		self.selection_changed();
262	}
263
264	fn rendered_rows(&self) -> usize {
265		(self.avail_size.y as usize).saturating_sub(2)
266	}
267
268	pub fn move_page_down(&mut self) {
269		let y = self.selection.y + self.rendered_rows();
270		if y >= self.data.rows() {
271			self.selection.y = self.data.rows() - 1;
272			self.offset.y = self.data.rows().saturating_sub(self.rendered_rows());
273			self.selection_changed();
274		}
275		else {
276			self.selection.y = y;
277			self.offset.y += self.rendered_rows();
278			if self.offset.y + self.rendered_rows() > self.data.rows() {
279				self.offset.y = self.data.rows().saturating_sub(self.rendered_rows());
280			}
281			self.selection_changed();
282		}
283	}
284
285	fn reevaluate_columns_range(&mut self, provoke: bool) {
286		self.offset.x = 0;
287		self.show_cols = self.col_widths.len();
288		loop {
289			let left = self.selection.x != self.offset.x;
290			let width = self.col_widths[self.offset.x..self.show_cols]
291				.iter()
292				.sum::<TSize>()
293				+ self.show_cols as TSize
294				- 1;
295			if width > self.avail_size.x {
296				if left {
297					self.offset.x += 1;
298				}
299				else {
300					self.show_cols -= 1;
301				}
302			}
303			else {
304				break;
305			}
306		}
307
308		if !(self.offset.y..self.offset.y + self.rendered_rows()).contains(&self.selection.y) {
309			self.offset.y = self.selection.y.saturating_sub(self.rendered_rows() - 1);
310		}
311
312		let old = self.base.constraints.x;
313		if !self.col_widths.is_empty() {
314			self.base.constraints.x.min = NonZero::new(self.col_widths[self.offset.x]).unwrap();
315			self.base.constraints.x.max = Size::Fixed(
316				NonZero::new(
317					self.col_widths[self.offset.x..self.show_cols]
318						.iter()
319						.sum::<TSize>() + self.col_widths.len() as TSize
320						- 1,
321				)
322				.unwrap(),
323			);
324		}
325		else {
326			self.base.constraints.x.max = Size::Relative(Percent::from_int(100));
327		}
328
329		if provoke && self.base.constraints.x != old {
330			self.provoke_changed_property(WindowProperty::Size);
331		}
332	}
333
334	/// Should be called when columns or column sizes have been changed
335	pub fn reevaluate_columns(&mut self) {
336		self.col_widths.fill(0);
337		self.col_widths
338			.resize(self.data.column_definitions().len(), 0);
339		for (x, max) in self.col_widths.iter_mut().enumerate() {
340			for y in 0..self.data.rows() {
341				let row = self.data.get_row(y);
342				if let Some(data) = row.get_data(x) {
343					let w = data.width(self.radix);
344					if w > *max {
345						*max = w;
346					}
347				}
348			}
349
350			let c_name_w = self.data.column_definitions()[x].header_width;
351			if c_name_w > *max {
352				*max = c_name_w;
353			}
354		}
355
356		if self.render_size != Vector2D::new(0, 0) {
357			self.reevaluate_columns_range(true);
358		}
359	}
360
361	/// Sets the internal data source
362	pub fn set_data_source(&mut self, data: Box<dyn DataSource>) {
363		self.data = data;
364		self.reevaluate_columns();
365	}
366}
367
368impl Window for DataGrid {
369	fn render(&self, canvas: &mut crate::canvas::Canvas) {
370		// Draw table headers
371		let mut current_width = 0;
372		for idx in self.offset.x..self.show_cols {
373			let c = &self.data.column_definitions()[idx];
374			let mut row0 = canvas.get_row_variable_width(0).unwrap();
375			let mut row1 = canvas.get_row(1, GlyphWidth::Half).unwrap();
376			let next_w = self.col_widths[idx];
377			row0.skip_until(current_width);
378			row0.add_string(&c.name, None, None, Style::None, None).ok();
379			row0.skip_until(current_width + next_w);
380			if idx != self.show_cols - 1 {
381				row0.add_grapheme(BorderKind::Solid.line_style()[1], None, None, Style::None)
382					.ok();
383			}
384			for x in current_width..current_width + next_w {
385				let mut g1 = row1.get(x).unwrap();
386				g1.set_grapheme(BorderKind::Solid.line_style()[0]).ok();
387			}
388
389			if idx != self.show_cols - 1 {
390				let mut g1 = row1.get(current_width + next_w).unwrap();
391				g1.set_grapheme(BorderKind::Solid.cross_style()).ok();
392			}
393
394			current_width += next_w;
395			current_width += 1;
396		}
397
398		let mut max = self.data.rows();
399		if max > self.rendered_rows() {
400			max = self.rendered_rows();
401		}
402
403        //Render content
404		for y in 0..max {
405			let drow = self.data.get_row(y + self.offset.y);
406			let mut row = canvas.get_row_variable_width(2 + y as TSize).unwrap();
407			if let Some(bg) = self.base.colors.base_bg {
408				row.fill_background(bg);
409			}
410			let base_fg = self.base.colors.base_fg;
411			let mut current_width = 0;
412			for x in 0..self.show_cols - self.offset.x {
413				let index = self.offset.x + x;
414				let col = self.col_widths[index];
415				if let Some(value) = drow.get_data(index) {
416					row.skip_until(current_width);
417					if x > 0 {
418						row.add_grapheme(
419							BorderKind::Solid.line_style()[1],
420							base_fg,
421							None,
422							Style::None,
423						)
424						.ok();
425						current_width += 1;
426					}
427
428					let fg = match self.selection == Vector2D::new(x, y) + self.offset {
429						true => self.sel_color.or(base_fg),
430						false => base_fg,
431					};
432
433					match value {
434						DataValue::Float(f) => {
435							for g in self.radix.iter_float(f) {
436								row.add_grapheme(g, fg, None, Style::None).ok();
437							}
438						}
439						DataValue::Boolean(b) => {
440							row.add_grapheme(Checkbox::OPEN_BOX, fg, None, Style::None)
441								.ok();
442							if b {
443								row.add_grapheme(Checkbox::CROSS_MARK, fg, None, Style::None)
444									.ok();
445							}
446							else {
447								row.skip(1);
448							}
449							row.add_grapheme(Checkbox::CLOSE_BOX, fg, None, Style::ResetAfter)
450								.ok();
451						}
452						DataValue::Integer(i) => {
453							for g in self.radix.iter_integer(i) {
454								row.add_grapheme(g, fg, None, Style::None).ok();
455							}
456						}
457						DataValue::String(s) => {
458							row.add_string(s, fg, None, Style::None, None).ok();
459						}
460					}
461				}
462				current_width += col;
463			}
464		}
465	}
466
467	fn handle_event(&mut self, event: &mut WindowEvent) {
468		match event.raw() {
469			Event::Key(_) => (self.key_handler)(self, event),
470			Event::Resize(w, h) => {
471				self.render_size = Vector2D::new(*w, *h);
472				if self.offset.y + self.rendered_rows() > self.data.rows() {
473					self.offset.y = self.data.rows().saturating_sub(self.rendered_rows());
474				}
475			}
476			_ => {}
477		}
478	}
479
480	fn is_enabled(&self) -> bool {
481		self.base.enabled
482	}
483}
484
485
486impl WindowLayout for DataGrid {
487	fn desired_size_pre_hook(&mut self, available_size: TPoint) {
488		if self.avail_size != available_size {
489			self.avail_size = available_size;
490			self.reevaluate_columns_range(false);
491		}
492	}
493
494	fn desired_size(&self, available_size: TPoint) -> TPoint {
495		self.base.desired_size(available_size)
496	}
497
498	fn alignment(&self) -> (HorizontalAlignment, VerticalAlignment) {
499		self.base.alignment
500	}
501
502	fn margin(&self) -> Thickness {
503		self.base.margin
504	}
505
506	fn border(&self) -> BorderStyle {
507		self.base.border
508	}
509
510	fn is_visible(&self) -> bool {
511		self.base.visibility
512	}
513}
514
515impl HasWindowUID for DataGrid {
516	fn uid(&self) -> WindowUID {
517		self.base.uid
518	}
519}
520
521impl Widget for DataGrid {
522	fn set_alignment(
523		&mut self,
524		horizontal: super::HorizontalAlignment,
525		vertical: super::VerticalAlignment,
526	) {
527		self.base.alignment = (horizontal, vertical);
528		self.provoke_changed_property(WindowProperty::Alignment);
529	}
530
531	fn set_visibility(&mut self, visibility: bool) {
532		self.base.visibility = visibility;
533		self.provoke_changed_property(WindowProperty::IsVisible);
534	}
535
536	fn set_width(&mut self, width: super::Size) {
537		self.base.size.x = width;
538		self.provoke_changed_property(WindowProperty::Size);
539	}
540
541	fn set_height(&mut self, height: super::Size) {
542		self.base.size.y = height;
543		self.provoke_changed_property(WindowProperty::Size);
544	}
545
546	fn set_margin(&mut self, margin: Thickness) {
547		self.base.margin = margin;
548		self.provoke_changed_property(WindowProperty::Margin);
549	}
550
551	fn set_border(&mut self, border: BorderStyle) {
552		self.base.border = border;
553		self.provoke_changed_property(WindowProperty::Border);
554	}
555
556	fn set_enabled_state(&mut self, is_enabled: bool) {
557		self.base.enabled = is_enabled;
558	}
559
560	fn set_width_constraint(&mut self, width: SizeConstraint) {
561		self.base.constraints.x = width;
562		self.provoke_changed_property(WindowProperty::Size);
563	}
564
565	fn set_height_constraint(&mut self, height: SizeConstraint) {
566		self.base.constraints.y = height;
567		self.provoke_changed_property(WindowProperty::Size);
568	}
569}
570
571impl WidgetColors for DataGrid {
572	fn set_disabled_color(&mut self, color: Option<Color>) {
573		self.base.colors.disabled = color;
574	}
575
576	fn set_base_fg_color(&mut self, color: Option<Color>) {
577		self.base.colors.base_fg = color;
578	}
579
580	fn set_base_bg_color(&mut self, color: Option<Color>) {
581		self.base.colors.base_bg = color;
582	}
583}