use crate::style::Style;
use alloc::string::String;
use alloc::sync::Arc;
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;
}
}
#[allow(clippy::derived_hash_with_manual_eq)]
#[derive(Clone, Debug, Eq, Hash)]
pub struct Tile {
pub(crate) glyph: char,
pub(crate) style: Style,
pub(crate) dx: i16,
pub(crate) dy: i16,
pub(crate) flags: TileFlags,
pub(crate) extra: Option<Arc<String>>,
}
impl PartialEq for Tile {
fn eq(&self, other: &Self) -> bool {
self.glyph == other.glyph
&& self.style == other.style
&& self.dx == other.dx
&& self.dy == other.dy
&& self.flags == other.flags
&& match (&self.extra, &other.extra) {
(None, None) => true,
(Some(a), Some(b)) => Arc::ptr_eq(a, b) || a == b,
_ => false,
}
}
}
impl Default for Tile {
fn default() -> Self {
Self {
glyph: ' ',
style: Style::default(),
dx: 0,
dy: 0,
flags: TileFlags::EMPTY,
extra: None,
}
}
}
impl Tile {
#[must_use]
pub const fn new(glyph: char, style: Style) -> Self {
Self {
glyph,
style,
dx: 0,
dy: 0,
flags: TileFlags::empty(),
extra: None,
}
}
#[must_use]
pub const fn glyph(&self) -> char {
self.glyph
}
#[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 is_empty(&self) -> bool {
self.flags.contains(TileFlags::EMPTY)
}
#[must_use]
pub fn extra(&self) -> Option<&str> {
self.extra.as_deref().map(String::as_str)
}
#[must_use]
pub fn grapheme(&self) -> Option<&str> {
self.extra.as_deref().map(String::as_str)
}
#[must_use]
pub const fn with_glyph(mut self, glyph: char) -> Self {
self.glyph = 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
}
#[cfg(feature = "egc")]
pub(crate) fn reset(&mut self) {
self.glyph = ' ';
self.style = Style::default();
self.dx = 0;
self.dy = 0;
self.flags = TileFlags::EMPTY;
self.extra = None;
}
}
#[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_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);
#[cfg(feature = "egc")]
assert!(tile.extra().is_none());
}
#[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_grapheme_single() {
let tile = Tile::new('A', Style::default());
assert_eq!(tile.grapheme(), None); }
#[cfg(feature = "egc")]
#[test]
fn test_tile_grapheme_multi() {
let extra_str = Arc::new(String::from("e\u{0301}"));
let tile = Tile {
glyph: 'e',
style: Style::default(),
dx: 0,
dy: 0,
flags: TileFlags::empty(),
extra: Some(extra_str),
};
assert_eq!(tile.grapheme(), Some("e\u{0301}"));
}
#[cfg(feature = "egc")]
#[test]
fn test_tile_eq_arc_fast_path_and_value_fallback() {
let tile_with = |s: &str| Tile {
glyph: 'e',
style: Style::default(),
dx: 0,
dy: 0,
flags: TileFlags::empty(),
extra: Some(Arc::new(String::from(s))),
};
let a = tile_with("e\u{0301}");
let cloned = a.clone();
assert!(Arc::ptr_eq(
a.extra.as_ref().unwrap(),
cloned.extra.as_ref().unwrap()
));
assert_eq!(a, cloned);
let b = tile_with("e\u{0301}");
assert!(!Arc::ptr_eq(
a.extra.as_ref().unwrap(),
b.extra.as_ref().unwrap()
));
assert_eq!(a, b);
assert_ne!(a, tile_with("a\u{0301}"));
assert_ne!(a, Tile::new('e', Style::default()));
}
#[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));
}
}