beyonder_core/tui.rs
1//! TUI cell type shared between terminal emulation and GPU rendering.
2
3use serde::{Deserialize, Serialize};
4
5/// Underline rendering style for a cell.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7pub enum UnderlineStyle {
8 None,
9 Single,
10 Double,
11 Curly,
12 Dotted,
13 Dashed,
14}
15
16impl Default for UnderlineStyle {
17 fn default() -> Self {
18 UnderlineStyle::None
19 }
20}
21
22/// A single rendered cell in a TUI screen grid.
23#[derive(Debug, Clone)]
24pub struct TuiCell {
25 /// Full grapheme cluster (base + combining/ZWJ sequence). A single terminal
26 /// cell may contain multiple codepoints, e.g. `👨👩👧` (ZWJ family),
27 /// `👋🏽` (skin-tone modifier), `1️⃣` (keycap), `🇯🇵` (regional-indicator flag).
28 pub grapheme: String,
29 /// Foreground RGB color (linear, 0.0–1.0).
30 pub fg: [f32; 3],
31 /// Background RGB color. `None` means default background (transparent/Base).
32 pub bg: Option<[f32; 3]>,
33 pub bold: bool,
34 pub italic: bool,
35 pub underline: UnderlineStyle,
36 pub strikethrough: bool,
37 /// OSC 8 hyperlink URI (shared via `Arc` because runs of cells share the same link).
38 pub link: Option<std::sync::Arc<String>>,
39}
40
41impl TuiCell {
42 /// First codepoint of the grapheme, or `\0` if empty. Useful for
43 /// single-codepoint classification (whitespace/null/box-drawing).
44 pub fn first_char(&self) -> char {
45 self.grapheme.chars().next().unwrap_or('\0')
46 }
47
48 /// True if the cell is a null spacer (wide-char follow-up) or blank.
49 pub fn is_null(&self) -> bool {
50 self.grapheme.is_empty() || self.grapheme == "\0"
51 }
52}