use crate::style::Style;
#[cfg(feature = "egc")]
use alloc::string::String;
use unicode_width::UnicodeWidthChar;
fn glyph_width(glyph: char) -> u8 {
#[allow(clippy::cast_possible_truncation)]
let width = glyph.width().unwrap_or(1) as u8;
width
}
bitflags::bitflags! {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub struct TileFlags: u8 {
const WIDE_CHAR = 0b0000_0001;
const WIDE_CHAR_SPACER = 0b0000_0010;
const EMPTY = 0b0000_0100;
const HAS_EXTRA = 0b0000_1000;
const SPAN_ANCHOR = 0b0001_0000;
const SPAN_COVERED = 0b0010_0000;
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Tile {
pub(crate) glyph: char,
pub(crate) style: Style,
pub(crate) width: u8,
pub(crate) dx: i16,
pub(crate) dy: i16,
pub(crate) flags: TileFlags,
pub(crate) span_w: u8,
pub(crate) span_h: u8,
}
impl Default for Tile {
fn default() -> Self {
Self {
glyph: ' ',
style: Style::default(),
width: 1,
dx: 0,
dy: 0,
flags: TileFlags::EMPTY,
span_w: 1,
span_h: 1,
}
}
}
impl Tile {
#[must_use]
pub fn new(glyph: char, style: Style) -> Self {
Self {
glyph,
style,
width: glyph_width(glyph),
dx: 0,
dy: 0,
flags: TileFlags::empty(),
span_w: 1,
span_h: 1,
}
}
#[must_use]
pub const fn glyph(&self) -> char {
self.glyph
}
#[must_use]
pub const fn width(&self) -> u16 {
self.width as u16
}
#[must_use]
pub const fn style(&self) -> Style {
self.style
}
#[must_use]
pub const fn dx(&self) -> i16 {
self.dx
}
#[must_use]
pub const fn dy(&self) -> i16 {
self.dy
}
#[must_use]
pub const fn flags(&self) -> TileFlags {
self.flags
}
#[must_use]
pub const fn span(&self) -> (u16, u16) {
if self.flags.contains(TileFlags::SPAN_ANCHOR) {
(self.span_w as u16, self.span_h as u16)
} else {
(1, 1)
}
}
#[must_use]
pub const fn span_offset(&self) -> Option<(u16, u16)> {
if self.flags.contains(TileFlags::SPAN_COVERED) {
Some((self.span_w as u16, self.span_h as u16))
} else {
None
}
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.flags.contains(TileFlags::EMPTY)
}
#[must_use]
pub fn with_glyph(mut self, glyph: char) -> Self {
self.glyph = glyph;
self.width = glyph_width(glyph);
self.flags = self.flags.difference(TileFlags::EMPTY);
self
}
#[must_use]
pub const fn with_style(mut self, style: Style) -> Self {
self.style = style;
self.flags = self.flags.difference(TileFlags::EMPTY);
self
}
#[must_use]
pub const fn with_offset(mut self, dx: i16, dy: i16) -> Self {
self.dx = dx;
self.dy = dy;
self.flags = self.flags.difference(TileFlags::EMPTY);
self
}
pub(crate) fn reset(&mut self) {
self.glyph = ' ';
self.style = Style::default();
self.width = 1;
self.dx = 0;
self.dy = 0;
self.flags = TileFlags::EMPTY;
self.span_w = 1;
self.span_h = 1;
}
pub(crate) fn clear_span(&mut self) {
self.flags
.remove(TileFlags::SPAN_ANCHOR | TileFlags::SPAN_COVERED);
self.span_w = 1;
self.span_h = 1;
}
}
#[cfg(feature = "egc")]
pub(crate) fn cap_grapheme(grapheme: &str) -> String {
const MAX_CODEPOINTS: usize = 8;
if grapheme.chars().count() <= MAX_CODEPOINTS {
return String::from(grapheme);
}
grapheme.chars().take(MAX_CODEPOINTS).collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::color::Color;
#[test]
fn test_tile_size_is_stable_and_small() {
assert_eq!(size_of::<Tile>(), 20);
}
#[test]
fn test_tile_defaults() {
let tile = Tile::default();
assert_eq!(tile.glyph(), ' ');
assert_eq!(tile.style(), Style::default());
assert_eq!(tile.dx, 0);
assert_eq!(tile.dy, 0);
assert!(tile.is_empty());
assert_eq!(tile.flags(), TileFlags::EMPTY);
}
#[test]
fn test_tile_empty_semantics() {
assert!(Tile::default().is_empty());
assert!(!Tile::new(' ', Style::default()).is_empty());
assert!(!Tile::default().with_glyph(' ').is_empty());
assert!(!Tile::default().with_style(Style::default()).is_empty());
assert!(!Tile::default().with_offset(1, 1).is_empty());
}
#[test]
fn test_tile_builder() {
let style = Style::new().fg(Color::RED);
let tile = Tile::new('A', style);
assert_eq!(tile.glyph(), 'A');
assert_eq!(tile.style(), style);
let tile = tile.with_glyph('B');
assert_eq!(tile.glyph(), 'B');
}
#[test]
fn test_tile_with_offset() {
let tile = Tile::new('X', Style::default()).with_offset(-3, 5);
assert_eq!(tile.dx, -3);
assert_eq!(tile.dy, 5);
}
#[test]
fn test_tile_reset() {
let style = Style::new().fg(Color::RED);
let mut tile = Tile::new('X', style);
assert!(!tile.is_empty());
tile.reset();
assert_eq!(tile.glyph(), ' ');
assert_eq!(tile.style(), Style::default());
assert_eq!(tile.dx, 0);
assert_eq!(tile.dy, 0);
assert!(tile.is_empty());
}
#[cfg(feature = "egc")]
#[test]
fn test_tile_wide_flag() {
let mut tile = Tile::new('漢', Style::default());
tile.flags = TileFlags::WIDE_CHAR;
assert!(tile.flags().contains(TileFlags::WIDE_CHAR));
assert!(!tile.flags().contains(TileFlags::WIDE_CHAR_SPACER));
}
#[test]
fn test_tile_width_is_precomputed_from_glyph() {
assert_eq!(Tile::new('A', Style::default()).width(), 1);
assert_eq!(Tile::new('漢', Style::default()).width(), 2);
assert_eq!(Tile::default().width(), 1);
}
#[test]
fn test_tile_with_glyph_recomputes_width() {
let tile = Tile::new('A', Style::default()).with_glyph('漢');
assert_eq!(tile.glyph(), '漢');
assert_eq!(tile.width(), 2);
}
#[test]
fn test_tile_span_defaults_to_one_by_one() {
assert_eq!(Tile::default().span(), (1, 1));
assert_eq!(Tile::new('A', Style::default()).span(), (1, 1));
assert_eq!(Tile::default().span_offset(), None);
assert_eq!(Tile::new('A', Style::default()).span_offset(), None);
}
#[test]
fn test_tile_span_accessors_are_keyed_by_role() {
let mut anchor = Tile::new('C', Style::default());
anchor.flags = TileFlags::SPAN_ANCHOR;
anchor.span_w = 2;
anchor.span_h = 3;
assert_eq!(anchor.span(), (2, 3));
assert_eq!(anchor.span_offset(), None);
let mut covered = Tile::new(']', Style::default());
covered.flags = TileFlags::SPAN_COVERED;
covered.span_w = 1;
covered.span_h = 2;
assert_eq!(covered.span_offset(), Some((1, 2)));
assert_eq!(covered.span(), (1, 1));
}
#[test]
fn test_tile_clear_span_keeps_the_glyph() {
let mut tile = Tile::new('C', Style::default());
tile.flags = TileFlags::SPAN_ANCHOR;
tile.span_w = 2;
tile.span_h = 2;
tile.clear_span();
assert_eq!(tile.glyph(), 'C');
assert_eq!(tile.span(), (1, 1));
assert!(!tile.flags().contains(TileFlags::SPAN_ANCHOR));
}
#[test]
fn test_tile_reset_clears_span() {
let mut tile = Tile::new('C', Style::default());
tile.flags = TileFlags::SPAN_ANCHOR;
tile.span_w = 4;
tile.span_h = 4;
tile.reset();
assert_eq!(tile.span(), (1, 1));
assert_eq!(tile.span_offset(), None);
assert!(tile.is_empty());
}
}