use crate::color::Rgba;
use bitflags::bitflags;
bitflags! {
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash)]
pub struct TextAttributes: u32 {
const BOLD = 0x01;
const DIM = 0x02;
const ITALIC = 0x04;
const UNDERLINE = 0x08;
const BLINK = 0x10;
const INVERSE = 0x20;
const HIDDEN = 0x40;
const STRIKETHROUGH = 0x80;
}
}
impl TextAttributes {
pub const FLAGS_MASK: u32 = 0x0000_00FF;
pub const LINK_ID_MASK: u32 = 0xFFFF_FF00;
pub const LINK_ID_SHIFT: u32 = 8;
pub const MAX_LINK_ID: u32 = 0x00FF_FFFF;
#[must_use]
pub const fn link_id(self) -> Option<u32> {
let id = (self.bits() & Self::LINK_ID_MASK) >> Self::LINK_ID_SHIFT;
if id == 0 { None } else { Some(id) }
}
#[must_use]
pub const fn with_link_id(self, link_id: u32) -> Self {
let id = link_id & Self::MAX_LINK_ID;
let bits = (self.bits() & Self::FLAGS_MASK) | (id << Self::LINK_ID_SHIFT);
Self::from_bits_retain(bits)
}
#[must_use]
pub const fn clear_link_id(self) -> Self {
Self::from_bits_retain(self.bits() & Self::FLAGS_MASK)
}
#[must_use]
pub const fn flags_only(self) -> Self {
Self::from_bits_retain(self.bits() & Self::FLAGS_MASK)
}
#[must_use]
pub const fn merge(self, other: Self) -> Self {
let flags = (self.bits() | other.bits()) & Self::FLAGS_MASK;
let link_bits = if (other.bits() & Self::LINK_ID_MASK) != 0 {
other.bits() & Self::LINK_ID_MASK
} else {
self.bits() & Self::LINK_ID_MASK
};
Self::from_bits_retain(flags | link_bits)
}
pub fn set_link_id(&mut self, link_id: u32) {
*self = self.with_link_id(link_id);
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Style {
pub fg: Option<Rgba>,
pub bg: Option<Rgba>,
pub attributes: TextAttributes,
}
impl Style {
pub const NONE: Self = Self {
fg: None,
bg: None,
attributes: TextAttributes::empty(),
};
#[must_use]
pub fn builder() -> StyleBuilder {
StyleBuilder::default()
}
#[must_use]
pub const fn fg(color: Rgba) -> Self {
Self {
fg: Some(color),
bg: None,
attributes: TextAttributes::empty(),
}
}
#[must_use]
pub const fn bg(color: Rgba) -> Self {
Self {
fg: None,
bg: Some(color),
attributes: TextAttributes::empty(),
}
}
#[must_use]
pub const fn bold() -> Self {
Self {
fg: None,
bg: None,
attributes: TextAttributes::BOLD,
}
}
#[must_use]
pub const fn italic() -> Self {
Self {
fg: None,
bg: None,
attributes: TextAttributes::ITALIC,
}
}
#[must_use]
pub const fn underline() -> Self {
Self {
fg: None,
bg: None,
attributes: TextAttributes::UNDERLINE,
}
}
#[must_use]
pub const fn dim() -> Self {
Self {
fg: None,
bg: None,
attributes: TextAttributes::DIM,
}
}
#[must_use]
pub const fn inverse() -> Self {
Self {
fg: None,
bg: None,
attributes: TextAttributes::INVERSE,
}
}
#[must_use]
pub const fn strikethrough() -> Self {
Self {
fg: None,
bg: None,
attributes: TextAttributes::STRIKETHROUGH,
}
}
#[must_use]
pub const fn with_fg(self, color: Rgba) -> Self {
Self {
fg: Some(color),
..self
}
}
#[must_use]
pub const fn with_bg(self, color: Rgba) -> Self {
Self {
bg: Some(color),
..self
}
}
#[must_use]
pub const fn with_attributes(self, attrs: TextAttributes) -> Self {
Self {
attributes: self.attributes.merge(attrs),
..self
}
}
#[must_use]
pub const fn with_bold(self) -> Self {
self.with_attributes(TextAttributes::BOLD)
}
#[must_use]
pub const fn with_italic(self) -> Self {
self.with_attributes(TextAttributes::ITALIC)
}
#[must_use]
pub const fn with_underline(self) -> Self {
self.with_attributes(TextAttributes::UNDERLINE)
}
#[must_use]
pub const fn with_link(self, link_id: u32) -> Self {
Self {
attributes: self.attributes.with_link_id(link_id),
..self
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.fg.is_none() && self.bg.is_none() && self.attributes.is_empty()
}
#[must_use]
pub fn merge(self, other: Self) -> Self {
Self {
fg: other.fg.or(self.fg),
bg: other.bg.or(self.bg),
attributes: self.attributes.merge(other.attributes),
}
}
}
#[derive(Clone, Debug, Default)]
pub struct StyleBuilder {
style: Style,
}
impl StyleBuilder {
#[must_use]
pub fn fg(mut self, color: Rgba) -> Self {
self.style.fg = Some(color);
self
}
#[must_use]
pub fn bg(mut self, color: Rgba) -> Self {
self.style.bg = Some(color);
self
}
#[must_use]
pub fn bold(mut self) -> Self {
self.style.attributes |= TextAttributes::BOLD;
self
}
#[must_use]
pub fn dim(mut self) -> Self {
self.style.attributes |= TextAttributes::DIM;
self
}
#[must_use]
pub fn italic(mut self) -> Self {
self.style.attributes |= TextAttributes::ITALIC;
self
}
#[must_use]
pub fn underline(mut self) -> Self {
self.style.attributes |= TextAttributes::UNDERLINE;
self
}
#[must_use]
pub fn blink(mut self) -> Self {
self.style.attributes |= TextAttributes::BLINK;
self
}
#[must_use]
pub fn inverse(mut self) -> Self {
self.style.attributes |= TextAttributes::INVERSE;
self
}
#[must_use]
pub fn hidden(mut self) -> Self {
self.style.attributes |= TextAttributes::HIDDEN;
self
}
#[must_use]
pub fn strikethrough(mut self) -> Self {
self.style.attributes |= TextAttributes::STRIKETHROUGH;
self
}
#[must_use]
pub fn link(mut self, link_id: u32) -> Self {
self.style.attributes = self.style.attributes.with_link_id(link_id);
self
}
#[must_use]
pub fn build(self) -> Style {
self.style
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_style_builder() {
let style = Style::builder()
.fg(Rgba::RED)
.bg(Rgba::BLACK)
.bold()
.underline()
.build();
assert_eq!(style.fg, Some(Rgba::RED));
assert_eq!(style.bg, Some(Rgba::BLACK));
assert!(style.attributes.contains(TextAttributes::BOLD));
assert!(style.attributes.contains(TextAttributes::UNDERLINE));
}
#[test]
fn test_style_merge() {
let base = Style::fg(Rgba::RED).with_bold();
let overlay = Style::bg(Rgba::BLUE).with_italic();
let merged = base.merge(overlay);
assert_eq!(merged.fg, Some(Rgba::RED));
assert_eq!(merged.bg, Some(Rgba::BLUE));
assert!(merged.attributes.contains(TextAttributes::BOLD));
assert!(merged.attributes.contains(TextAttributes::ITALIC));
}
#[test]
fn test_const_styles() {
assert!(Style::bold().attributes.contains(TextAttributes::BOLD));
assert!(Style::italic().attributes.contains(TextAttributes::ITALIC));
assert!(
Style::underline()
.attributes
.contains(TextAttributes::UNDERLINE)
);
}
#[test]
fn test_text_attributes_link_id_packing() {
let attrs = TextAttributes::BOLD.with_link_id(0x12_3456);
assert!(attrs.contains(TextAttributes::BOLD));
assert_eq!(attrs.link_id(), Some(0x12_3456));
assert_eq!(attrs.flags_only(), TextAttributes::BOLD);
}
#[test]
fn test_text_attributes_merge_link_id_preference() {
let base = TextAttributes::BOLD.with_link_id(1);
let overlay_no_link = TextAttributes::ITALIC;
let merged = base.merge(overlay_no_link);
assert_eq!(merged.link_id(), Some(1));
assert!(merged.contains(TextAttributes::BOLD));
assert!(merged.contains(TextAttributes::ITALIC));
let overlay_with_link = TextAttributes::UNDERLINE.with_link_id(2);
let merged_with_link = base.merge(overlay_with_link);
assert_eq!(merged_with_link.link_id(), Some(2));
assert!(merged_with_link.contains(TextAttributes::BOLD));
assert!(merged_with_link.contains(TextAttributes::UNDERLINE));
}
#[test]
fn test_text_attributes_link_id_masking() {
let attrs = TextAttributes::empty().with_link_id(0x1FF_FFFF);
assert_eq!(attrs.link_id(), Some(TextAttributes::MAX_LINK_ID));
}
}