use crate::render::style::{Color, Modifier, Style};
#[cfg(graphics_mode)]
use crate::render::symbol_map::{Tile, get_layered_symbol_map};
use serde::{Deserialize, Serialize};
fn default_scale() -> f32 {
1.0
}
pub type CellInfo = (u8, u8, Color, Color, Modifier);
pub const PUA_BASE: u32 = 0xF0000;
pub const PUA_END: u32 = 0xFFFFD;
pub const PUA_BLOCK_SIZE: u32 = 256;
pub fn cellsym_block(block: u8, idx: u8) -> String {
let codepoint = PUA_BASE + (block as u32) * PUA_BLOCK_SIZE + idx as u32;
debug_assert!(
codepoint <= PUA_END,
"invalid PUA codepoint U+{:X} from block={}, idx={}",
codepoint,
block,
idx
);
char::from_u32(codepoint).unwrap().to_string()
}
pub fn cellsym(idx: u8) -> String {
cellsym_block(0, idx)
}
pub fn decode_pua(ch: char) -> Option<(u8, u8)> {
let cp = ch as u32;
if (PUA_BASE..=PUA_END).contains(&cp) {
let offset = cp - PUA_BASE;
let block = (offset / PUA_BLOCK_SIZE) as u8;
let idx = (offset % PUA_BLOCK_SIZE) as u8;
Some((block, idx))
} else {
None
}
}
pub fn is_pua_sprite(ch: char) -> bool {
let cp = ch as u32;
(PUA_BASE..=PUA_END).contains(&cp)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TuiCharType {
TuiChar,
Emoji,
CJK,
}
pub fn is_tui_char(cp: u32) -> bool {
(0x0020..=0x007E).contains(&cp) || (0x2500..=0x257F).contains(&cp) || (0x2580..=0x259F).contains(&cp) || (0x2800..=0x28FF).contains(&cp) || (0xE000..=0xF8FF).contains(&cp) }
pub fn is_cjk(cp: u32) -> bool {
(0x4E00..=0x9FFF).contains(&cp) || (0x3400..=0x4DBF).contains(&cp) || (0x20000..=0x2A6DF).contains(&cp) || (0x2A700..=0x2B73F).contains(&cp) || (0x2B740..=0x2B81F).contains(&cp) || (0x3000..=0x303F).contains(&cp) || (0xFF00..=0xFFEF).contains(&cp) }
pub fn detect_tui_char_type(symbol: &str) -> TuiCharType {
if is_prerendered_emoji(symbol) {
return TuiCharType::Emoji;
}
if let Some(ch) = symbol.chars().next() {
let cp = ch as u32;
if is_cjk(cp) {
return TuiCharType::CJK;
}
}
TuiCharType::TuiChar
}
pub fn is_prerendered_emoji(symbol: &str) -> bool {
if let Some(ch) = symbol.chars().next() {
let cp = ch as u32;
let is_emoji_range = (0x1F000..=0x1FAFF).contains(&cp)
|| (0x2300..=0x23FF).contains(&cp)
|| (0x2600..=0x26FF).contains(&cp)
|| (0x2700..=0x27BF).contains(&cp)
|| (0x2B00..=0x2BFF).contains(&cp);
if is_emoji_range {
#[cfg(graphics_mode)]
{
return get_layered_symbol_map()
.map(|m| m.contains(symbol))
.unwrap_or(false);
}
#[cfg(not(graphics_mode))]
{
return true; }
}
}
false
}
#[repr(C)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Cell {
pub symbol: String,
pub fg: Color,
pub bg: Color,
pub modifier: Modifier,
#[serde(default = "default_scale")]
pub scale_x: f32,
#[serde(default = "default_scale")]
pub scale_y: f32,
#[cfg(graphics_mode)]
#[serde(skip)]
tile: Tile,
}
impl Cell {
pub fn set_symbol(&mut self, symbol: &str) -> &mut Cell {
self.symbol.clear();
self.symbol.push_str(symbol);
#[cfg(graphics_mode)]
{
self.tile = self.compute_tile();
}
self
}
#[cfg(graphics_mode)]
fn compute_tile(&self) -> Tile {
get_layered_symbol_map()
.map(|m| *m.resolve(&self.symbol))
.unwrap_or_default()
}
#[cfg(graphics_mode)]
pub fn get_cell_info(&self) -> CellInfo {
let (block, idx) = self.compute_block_idx();
(idx, block, self.fg, self.bg, self.modifier)
}
#[cfg(graphics_mode)]
pub fn get_tile(&self) -> Tile {
self.tile
}
#[cfg(graphics_mode)]
fn compute_block_idx(&self) -> (u8, u8) {
if let Some(ch) = self.symbol.chars().next() {
if let Some((block, idx)) = decode_pua(ch) {
return (block, idx);
}
}
if let Some(map) = get_layered_symbol_map() {
if let Some((block, idx)) = map.reverse_lookup(&self.symbol) {
return (block, idx);
}
}
(0, 32) }
#[cfg(not(graphics_mode))]
pub fn get_cell_info(&self) -> CellInfo {
let (block, idx) = self.compute_block_idx_fallback();
(idx, block, self.fg, self.bg, self.modifier)
}
#[cfg(not(graphics_mode))]
fn compute_block_idx_fallback(&self) -> (u8, u8) {
if let Some(ch) = self.symbol.chars().next() {
if let Some((block, idx)) = decode_pua(ch) {
return (block, idx);
}
}
(0, 32) }
pub fn set_char(&mut self, ch: char) -> &mut Cell {
self.symbol.clear();
self.symbol.push(ch);
#[cfg(graphics_mode)]
{
self.tile = self.compute_tile();
}
self
}
#[cfg(graphics_mode)]
pub fn set_texture(&mut self, block: u8) -> &mut Cell {
let idx = self.symbol.chars().next()
.and_then(decode_pua)
.map(|(_, i)| i)
.unwrap_or(32);
self.symbol = cellsym_block(block, idx);
self.tile = self.compute_tile();
self
}
pub fn set_fg(&mut self, color: Color) -> &mut Cell {
self.fg = color;
self
}
pub fn set_bg(&mut self, color: Color) -> &mut Cell {
self.bg = color;
self
}
pub fn set_scale(&mut self, sx: f32, sy: f32) -> &mut Cell {
self.scale_x = sx;
self.scale_y = sy;
self
}
pub fn set_scale_uniform(&mut self, s: f32) -> &mut Cell {
self.scale_x = s;
self.scale_y = s;
self
}
pub fn set_style(&mut self, style: Style) -> &mut Cell {
if let Some(c) = style.fg {
self.fg = c;
}
if let Some(c) = style.bg {
self.bg = c;
}
self.modifier.insert(style.add_modifier);
self.modifier.remove(style.sub_modifier);
if let Some(sx) = style.scale_x {
self.scale_x = sx;
}
if let Some(sy) = style.scale_y {
self.scale_y = sy;
}
self
}
pub fn style(&self) -> Style {
Style::default()
.fg(self.fg)
.bg(self.bg)
.add_modifier(self.modifier)
.scale(self.scale_x, self.scale_y)
}
pub fn reset(&mut self) {
self.symbol.clear();
self.symbol.push(' ');
self.fg = Color::Reset;
self.bg = Color::Reset;
self.modifier = Modifier::empty();
self.scale_x = 1.0;
self.scale_y = 1.0;
#[cfg(graphics_mode)]
{
self.tile = self.compute_tile();
}
}
pub fn reset_sprite(&mut self) {
self.symbol = cellsym(32); self.fg = Color::Reset;
self.bg = Color::Reset;
self.modifier = Modifier::empty();
self.scale_x = 1.0;
self.scale_y = 1.0;
#[cfg(graphics_mode)]
{
self.tile = self.compute_tile();
}
}
#[cfg(graphics_mode)]
pub fn is_blank(&self) -> bool {
let is_space = self.symbol == " " || {
if let Some(ch) = self.symbol.chars().next() {
if let Some((block, idx)) = decode_pua(ch) {
idx == 32 && block <= 1
} else {
false
}
} else {
true }
};
is_space && self.bg == Color::Reset
}
#[cfg(not(graphics_mode))]
pub fn is_blank(&self) -> bool {
self.symbol == " " && self.fg == Color::Reset && self.bg == Color::Reset
}
}
impl Default for Cell {
#[cfg(graphics_mode)]
fn default() -> Cell {
Cell {
symbol: " ".into(),
fg: Color::Reset,
bg: Color::Reset,
modifier: Modifier::empty(),
scale_x: 1.0,
scale_y: 1.0,
tile: Tile::default(),
}
}
#[cfg(not(graphics_mode))]
fn default() -> Cell {
Cell {
symbol: " ".into(),
fg: Color::Reset,
bg: Color::Reset,
modifier: Modifier::empty(),
scale_x: 1.0,
scale_y: 1.0,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pua_roundtrip_block0() {
for idx in [0u8, 1, 127, 128, 255] {
let encoded = cellsym_block(0, idx);
let ch = encoded.chars().next().unwrap();
let (block, decoded_idx) = decode_pua(ch).expect("should decode");
assert_eq!(block, 0);
assert_eq!(decoded_idx, idx);
}
}
#[test]
fn test_pua_roundtrip_all_blocks() {
for block in [0u8, 1, 50, 100, 159, 160, 200, 254] {
for idx in [0u8, 128, 255] {
let encoded = cellsym_block(block, idx);
let ch = encoded.chars().next().unwrap();
let (dec_block, dec_idx) = decode_pua(ch).expect("should decode");
assert_eq!(dec_block, block, "block mismatch for ({}, {})", block, idx);
assert_eq!(dec_idx, idx, "idx mismatch for ({}, {})", block, idx);
}
}
}
#[test]
fn test_pua_decode_non_pua() {
assert_eq!(decode_pua('A'), None);
assert_eq!(decode_pua(' '), None);
assert_eq!(decode_pua('\u{E000}'), None);
assert_eq!(decode_pua('π'), None);
}
#[test]
fn test_pua_codepoint_range() {
let first = cellsym_block(0, 0);
let cp = first.chars().next().unwrap() as u32;
assert_eq!(cp, PUA_BASE);
let last = cellsym_block(255, 253);
let cp = last.chars().next().unwrap() as u32;
assert_eq!(cp, PUA_END);
}
#[test]
fn test_cellsym_convenience() {
for idx in [0u8, 42, 160, 255] {
assert_eq!(cellsym(idx), cellsym_block(0, idx));
}
}
#[test]
fn test_is_pua_sprite() {
let pua_ch = cellsym_block(0, 0).chars().next().unwrap();
assert!(is_pua_sprite(pua_ch));
assert!(!is_pua_sprite('A'));
assert!(!is_pua_sprite('π'));
}
#[test]
fn test_tui_char_type_detection() {
assert_eq!(detect_tui_char_type("A"), TuiCharType::TuiChar);
assert_eq!(detect_tui_char_type(" "), TuiCharType::TuiChar);
assert_eq!(detect_tui_char_type("β"), TuiCharType::TuiChar);
assert_eq!(detect_tui_char_type("β"), TuiCharType::TuiChar);
assert_eq!(detect_tui_char_type("δΈ"), TuiCharType::CJK);
assert_eq!(detect_tui_char_type("δΈ"), TuiCharType::CJK);
}
}