use crate::color::Style;
use crate::text::char_width;
#[cfg(feature = "egc")]
use alloc::string::String;
fn glyph_width(glyph: char) -> u8 {
u8::try_from(char_width(glyph)).unwrap_or(1)
}
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::EMPTY
}
}
impl Tile {
pub(crate) const EMPTY: Self = Self {
glyph: ' ',
style: Style {
fg: crate::color::Color::Default,
bg: crate::color::Color::Default,
},
width: 1,
dx: 0,
dy: 0,
flags: TileFlags::EMPTY,
span_w: 1,
span_h: 1,
};
#[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 span_anchor_index(&self, idx: usize, cols: usize) -> Option<usize> {
let Some((dx, dy)) = self.span_offset() else {
return None;
};
idx.checked_sub(dy as usize * cols + dx as usize)
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.flags.contains(TileFlags::EMPTY)
}
#[must_use]
pub const fn is_wide(&self) -> bool {
self.flags.contains(TileFlags::WIDE_CHAR)
}
#[must_use]
pub const fn is_wide_spacer(&self) -> bool {
self.flags.contains(TileFlags::WIDE_CHAR_SPACER)
}
#[must_use]
pub const fn is_span_anchor(&self) -> bool {
self.flags.contains(TileFlags::SPAN_ANCHOR)
}
#[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 | TileFlags::WIDE_CHAR | TileFlags::WIDE_CHAR_SPACER);
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;
}
pub(crate) fn clear_wide(&mut self) {
self.flags
.remove(TileFlags::WIDE_CHAR | TileFlags::WIDE_CHAR_SPACER);
}
}
#[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_with_style_preserves_glyph_and_width() {
let tile = Tile::new('æ¼¢', Style::default()).with_style(Style::new().fg(Color::RED));
assert_eq!(tile.glyph(), 'æ¼¢');
assert_eq!(tile.width(), 2);
assert_eq!(tile.style(), Style::new().fg(Color::RED));
}
#[test]
fn test_tile_with_offset_preserves_glyph_and_width() {
let tile = Tile::new('æ¼¢', Style::default()).with_offset(1, 1);
assert_eq!(tile.glyph(), 'æ¼¢');
assert_eq!(tile.width(), 2);
}
#[test]
fn test_tile_with_offset_chain_does_not_leak_prior_values() {
let tile = Tile::new('X', Style::default())
.with_offset(-3, 5)
.with_offset(2, -1);
assert_eq!(tile.dx, 2);
assert_eq!(tile.dy, -1);
}
#[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());
}
#[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_width_falls_back_to_one_for_control_characters() {
assert_eq!(Tile::new('\t', Style::default()).width(), 1);
assert_eq!(Tile::new('\u{7}', Style::default()).width(), 1);
assert_eq!(Tile::new('\u{1b}', Style::default()).width(), 1);
}
#[test]
fn test_tile_width_is_zero_for_zero_width_glyphs() {
assert_eq!(Tile::new('\u{0301}', Style::default()).width(), 0);
assert_eq!(Tile::new('\u{200d}', Style::default()).width(), 0);
}
#[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_with_glyph_clears_stale_wide_char_spacer_flag() {
let mut spacer = Tile::new(' ', Style::default());
spacer.flags = TileFlags::WIDE_CHAR_SPACER;
let rebuilt = spacer.with_glyph('!');
assert_eq!(rebuilt.glyph(), '!');
assert_eq!(rebuilt.width(), 1);
assert!(!rebuilt.flags().contains(TileFlags::WIDE_CHAR_SPACER));
assert!(!rebuilt.is_empty());
}
#[test]
fn test_tile_with_glyph_clears_stale_wide_char_flag() {
let mut wide = Tile::new('æ¼¢', Style::default());
wide.flags = TileFlags::WIDE_CHAR;
let rebuilt = wide.with_glyph('A');
assert_eq!(rebuilt.glyph(), 'A');
assert_eq!(rebuilt.width(), 1);
assert!(!rebuilt.flags().contains(TileFlags::WIDE_CHAR));
}
#[test]
fn test_tile_flag_predicates() {
let mut tile = Tile::new('A', Style::default());
assert!(!tile.is_wide());
assert!(!tile.is_wide_spacer());
assert!(!tile.is_span_anchor());
tile.flags = TileFlags::WIDE_CHAR;
assert!(tile.is_wide());
assert!(!tile.is_wide_spacer());
assert!(!tile.is_span_anchor());
tile.flags = TileFlags::WIDE_CHAR_SPACER;
assert!(!tile.is_wide());
assert!(tile.is_wide_spacer());
assert!(!tile.is_span_anchor());
tile.flags = TileFlags::SPAN_ANCHOR;
tile.span_w = 1;
tile.span_h = 1;
assert!(tile.is_span_anchor());
assert_eq!(tile.span(), (1, 1));
}
#[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_span_anchor_index_resolves_a_covered_cell_to_its_anchor() {
let mut covered = Tile::new(']', Style::default());
covered.flags = TileFlags::SPAN_COVERED;
covered.span_w = 1;
covered.span_h = 2;
assert_eq!(covered.span_anchor_index(23, 10), Some(2));
}
#[test]
fn test_tile_span_anchor_index_is_none_when_not_covered() {
assert_eq!(Tile::default().span_anchor_index(5, 10), None);
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_anchor_index(5, 10), None);
}
#[test]
fn test_tile_span_anchor_index_is_none_past_the_buffer_start() {
let mut covered = Tile::new(']', Style::default());
covered.flags = TileFlags::SPAN_COVERED;
covered.span_w = 1;
covered.span_h = 2;
assert_eq!(covered.span_anchor_index(1, 10), None);
}
#[test]
fn test_tile_span_anchor_index_does_not_detect_a_zero_stride() {
let mut covered = Tile::new(']', Style::default());
covered.flags = TileFlags::SPAN_COVERED;
covered.span_w = 1;
covered.span_h = 0;
assert_eq!(covered.span_anchor_index(1, 0), Some(0));
}
#[test]
fn test_tile_span_anchor_index_does_not_detect_crossing_a_row_block() {
let mut covered = Tile::new(']', Style::default());
covered.flags = TileFlags::SPAN_COVERED;
covered.span_w = 1;
covered.span_h = 0;
assert_eq!(covered.span_anchor_index(4, 4), Some(3));
}
#[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());
}
#[test]
fn test_tile_flags_default_is_not_empty_flag() {
assert_eq!(TileFlags::default(), TileFlags::empty());
assert_ne!(TileFlags::default(), TileFlags::EMPTY);
assert_eq!(Tile::default().flags(), TileFlags::EMPTY);
}
#[cfg(feature = "egc")]
#[test]
fn test_cap_grapheme_leaves_short_input_unchanged() {
assert_eq!(cap_grapheme(""), "");
assert_eq!(cap_grapheme("a"), "a");
assert_eq!(cap_grapheme("e\u{0301}"), "e\u{0301}");
}
#[cfg(feature = "egc")]
#[test]
fn test_cap_grapheme_leaves_exactly_the_cap_unchanged() {
let input: String = core::iter::repeat_n('\u{0301}', 8).collect();
assert_eq!(cap_grapheme(&input), input);
}
#[cfg(feature = "egc")]
#[test]
fn test_cap_grapheme_truncates_past_the_cap_on_a_codepoint_boundary() {
let input: String = core::iter::repeat_n('\u{0301}', 9).collect();
let capped = cap_grapheme(&input);
assert_eq!(capped.chars().count(), 8);
assert!(capped.is_char_boundary(capped.len()));
let expected: String = core::iter::repeat_n('\u{0301}', 8).collect();
assert_eq!(capped, expected);
}
}