use bitflags::bitflags;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Dimension {
Fixed(u16),
Percentage(f32),
Auto,
Content,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Spacing {
pub top: u16,
pub right: u16,
pub bottom: u16,
pub left: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Color {
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
White,
BrightBlack,
BrightRed,
BrightGreen,
BrightYellow,
BrightBlue,
BrightMagenta,
BrightCyan,
BrightWhite,
Rgb(u8, u8, u8),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Direction {
Vertical,
Horizontal,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Overflow {
None,
Hidden,
Scroll,
Auto,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TextWrap {
None,
Character,
Word,
WordBreak,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum WrapMode {
NoWrap,
Wrap,
WrapReverse,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Position {
Relative,
Absolute,
Fixed,
}
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BorderEdges: u8 {
const TOP = 0b00000001;
const RIGHT = 0b00000010;
const BOTTOM = 0b00000100;
const LEFT = 0b00001000;
const TOP_LEFT = 0b00010000;
const TOP_RIGHT = 0b00100000;
const BOTTOM_RIGHT = 0b01000000;
const BOTTOM_LEFT = 0b10000000;
const ALL = Self::TOP.bits() | Self::RIGHT.bits() | Self::BOTTOM.bits() | Self::LEFT.bits() |
Self::TOP_LEFT.bits() | Self::TOP_RIGHT.bits() | Self::BOTTOM_RIGHT.bits() | Self::BOTTOM_LEFT.bits();
const EDGES = Self::TOP.bits() | Self::RIGHT.bits() | Self::BOTTOM.bits() | Self::LEFT.bits();
const CORNERS = Self::TOP_LEFT.bits() | Self::TOP_RIGHT.bits() | Self::BOTTOM_RIGHT.bits() | Self::BOTTOM_LEFT.bits();
const HORIZONTAL = Self::TOP.bits() | Self::BOTTOM.bits();
const VERTICAL = Self::LEFT.bits() | Self::RIGHT.bits();
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BorderStyle {
#[default]
Single,
Double,
Thick,
Rounded,
Dashed,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Border {
pub enabled: bool,
pub style: BorderStyle,
pub color: Color,
pub edges: BorderEdges,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Style {
pub background: Option<Color>,
pub direction: Option<Direction>,
pub padding: Option<Spacing>,
pub overflow: Option<Overflow>,
pub width: Option<Dimension>,
pub height: Option<Dimension>,
pub border: Option<Border>,
pub position: Option<Position>,
pub z_index: Option<i32>,
pub top: Option<i16>,
pub right: Option<i16>,
pub bottom: Option<i16>,
pub left: Option<i16>,
pub wrap: Option<WrapMode>,
pub gap: Option<u16>,
pub margin: Option<Spacing>,
pub min_width: Option<u16>,
pub min_height: Option<u16>,
pub max_width: Option<u16>,
pub max_height: Option<u16>,
pub border_color: Option<Color>,
pub x: Option<u16>,
pub y: Option<u16>,
pub show_scrollbar: Option<bool>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct TextStyle {
pub color: Option<Color>,
pub background: Option<Color>,
pub bold: Option<bool>,
pub italic: Option<bool>,
pub underline: Option<bool>,
pub strikethrough: Option<bool>,
pub wrap: Option<TextWrap>,
}
pub struct StyleBuilder {
style: Style,
}
pub struct TextStyleBuilder {
style: TextStyle,
}
impl Color {
pub fn from_hex(hex: &str) -> Result<Self, &'static str> {
let hex = hex.strip_prefix('#').unwrap_or(hex);
match hex.len() {
1 => {
let digit = hex.chars().next().unwrap();
let value = parse_hex_digit(digit)?;
let expanded = (value << 4) | value;
Ok(Color::Rgb(expanded, expanded, expanded))
}
3 => {
let mut chars = hex.chars();
let r = parse_hex_digit(chars.next().unwrap())?;
let g = parse_hex_digit(chars.next().unwrap())?;
let b = parse_hex_digit(chars.next().unwrap())?;
let r_expanded = (r << 4) | r;
let g_expanded = (g << 4) | g;
let b_expanded = (b << 4) | b;
Ok(Color::Rgb(r_expanded, g_expanded, b_expanded))
}
6 => {
let r =
u8::from_str_radix(&hex[0..2], 16).map_err(|_| "Invalid hex color format")?;
let g =
u8::from_str_radix(&hex[2..4], 16).map_err(|_| "Invalid hex color format")?;
let b =
u8::from_str_radix(&hex[4..6], 16).map_err(|_| "Invalid hex color format")?;
Ok(Color::Rgb(r, g, b))
}
_ => Err("Hex color must be 1, 3, or 6 digits"),
}
}
pub fn hex(hex: &str) -> Self {
Self::from_hex(hex).expect("Invalid hex color")
}
pub fn rgb(r: u8, g: u8, b: u8) -> Self {
Color::Rgb(r, g, b)
}
}
fn parse_hex_digit(c: char) -> Result<u8, &'static str> {
let upper = c.to_ascii_uppercase();
match upper {
'0'..='9' => Ok(upper as u8 - b'0'),
'A'..='F' => Ok(upper as u8 - b'A' + 10),
_ => Err("Invalid hex digit"),
}
}
impl Style {
pub fn default_focus() -> Style {
Style {
border: Some(Border {
enabled: true,
style: BorderStyle::Single,
color: Color::Yellow,
edges: BorderEdges::ALL,
}),
..Default::default()
}
}
pub fn merge(base: Option<Style>, overlay: Option<Style>) -> Option<Style> {
match (base, overlay) {
(None, None) => None,
(Some(base), None) => Some(base),
(None, Some(overlay)) => Some(overlay),
(Some(mut base), Some(overlay)) => {
if overlay.background.is_some() {
base.background = overlay.background;
}
if overlay.direction.is_some() {
base.direction = overlay.direction;
}
if overlay.padding.is_some() {
base.padding = overlay.padding;
}
if overlay.overflow.is_some() {
base.overflow = overlay.overflow;
}
if overlay.width.is_some() {
base.width = overlay.width;
}
if overlay.height.is_some() {
base.height = overlay.height;
}
if overlay.border.is_some() {
base.border = overlay.border;
}
if overlay.position.is_some() {
base.position = overlay.position;
}
if overlay.z_index.is_some() {
base.z_index = overlay.z_index;
}
if overlay.top.is_some() {
base.top = overlay.top;
}
if overlay.right.is_some() {
base.right = overlay.right;
}
if overlay.bottom.is_some() {
base.bottom = overlay.bottom;
}
if overlay.left.is_some() {
base.left = overlay.left;
}
if overlay.wrap.is_some() {
base.wrap = overlay.wrap;
}
if overlay.gap.is_some() {
base.gap = overlay.gap;
}
if overlay.show_scrollbar.is_some() {
base.show_scrollbar = overlay.show_scrollbar;
}
Some(base)
}
}
}
pub fn background(mut self, color: Color) -> Self {
self.background = Some(color);
self
}
pub fn direction(mut self, direction: Direction) -> Self {
self.direction = Some(direction);
self
}
pub fn padding(mut self, padding: Spacing) -> Self {
self.padding = Some(padding);
self
}
pub fn overflow(mut self, overflow: Overflow) -> Self {
self.overflow = Some(overflow);
self
}
pub fn width(mut self, width: Dimension) -> Self {
self.width = Some(width);
self
}
pub fn height(mut self, height: Dimension) -> Self {
self.height = Some(height);
self
}
pub fn border(mut self, color: Color) -> Self {
self.border = Some(Border {
enabled: true,
style: BorderStyle::Single,
color,
edges: BorderEdges::ALL,
});
self
}
pub fn position(mut self, position: Position) -> Self {
self.position = Some(position);
self
}
pub fn z_index(mut self, z_index: i32) -> Self {
self.z_index = Some(z_index);
self
}
pub fn top(mut self, top: i16) -> Self {
self.top = Some(top);
self
}
pub fn right(mut self, right: i16) -> Self {
self.right = Some(right);
self
}
pub fn bottom(mut self, bottom: i16) -> Self {
self.bottom = Some(bottom);
self
}
pub fn left(mut self, left: i16) -> Self {
self.left = Some(left);
self
}
pub fn wrap(mut self, wrap: WrapMode) -> Self {
self.wrap = Some(wrap);
self
}
pub fn gap(mut self, gap: u16) -> Self {
self.gap = Some(gap);
self
}
pub fn show_scrollbar(mut self, show: bool) -> Self {
self.show_scrollbar = Some(show);
self
}
}
impl Border {
pub fn new(color: Color) -> Self {
Self {
enabled: true,
style: BorderStyle::Single,
color,
edges: BorderEdges::ALL,
}
}
pub fn with_style(style: BorderStyle, color: Color) -> Self {
Self {
enabled: true,
style,
color,
edges: BorderEdges::ALL,
}
}
pub fn with_edges(style: BorderStyle, color: Color, edges: BorderEdges) -> Self {
Self {
enabled: true,
style,
color,
edges,
}
}
}
impl Style {
#[deprecated(note = "Use Style::default() with builder methods instead")]
pub fn builder() -> StyleBuilder {
StyleBuilder {
style: Style::default(),
}
}
}
impl TextStyle {
pub fn merge(base: Option<TextStyle>, overlay: Option<TextStyle>) -> Option<TextStyle> {
match (base, overlay) {
(None, None) => None,
(Some(base), None) => Some(base),
(None, Some(overlay)) => Some(overlay),
(Some(mut base), Some(overlay)) => {
if overlay.color.is_some() {
base.color = overlay.color;
}
if overlay.background.is_some() {
base.background = overlay.background;
}
if overlay.bold.is_some() {
base.bold = overlay.bold;
}
if overlay.italic.is_some() {
base.italic = overlay.italic;
}
if overlay.underline.is_some() {
base.underline = overlay.underline;
}
if overlay.strikethrough.is_some() {
base.strikethrough = overlay.strikethrough;
}
if overlay.wrap.is_some() {
base.wrap = overlay.wrap;
}
Some(base)
}
}
}
#[deprecated(note = "Use TextStyle::default() with builder methods instead")]
pub fn builder() -> TextStyleBuilder {
TextStyleBuilder {
style: TextStyle {
color: None,
background: None,
bold: None,
italic: None,
underline: None,
strikethrough: None,
wrap: None,
},
}
}
pub fn color(mut self, color: Color) -> Self {
self.color = Some(color);
self
}
pub fn background(mut self, color: Color) -> Self {
self.background = Some(color);
self
}
pub fn bold(mut self, bold: bool) -> Self {
self.bold = Some(bold);
self
}
pub fn italic(mut self, italic: bool) -> Self {
self.italic = Some(italic);
self
}
pub fn underline(mut self, underline: bool) -> Self {
self.underline = Some(underline);
self
}
pub fn strikethrough(mut self, strikethrough: bool) -> Self {
self.strikethrough = Some(strikethrough);
self
}
pub fn wrap(mut self, wrap: TextWrap) -> Self {
self.wrap = Some(wrap);
self
}
}
impl TextStyleBuilder {
pub fn color(mut self, color: Color) -> Self {
self.style.color = Some(color);
self
}
pub fn background(mut self, color: Color) -> Self {
self.style.background = Some(color);
self
}
pub fn bold(mut self) -> Self {
self.style.bold = Some(true);
self
}
pub fn italic(mut self) -> Self {
self.style.italic = Some(true);
self
}
pub fn underline(mut self) -> Self {
self.style.underline = Some(true);
self
}
pub fn strikethrough(mut self) -> Self {
self.style.strikethrough = Some(true);
self
}
pub fn strong(self) -> Self {
self.bold()
}
pub fn emphasis(self) -> Self {
self.italic()
}
pub fn wrap(mut self, wrap: TextWrap) -> Self {
self.style.wrap = Some(wrap);
self
}
pub fn build(self) -> TextStyle {
self.style
}
}
impl Spacing {
pub fn all(value: u16) -> Self {
Self {
top: value,
right: value,
bottom: value,
left: value,
}
}
pub fn vertical(value: u16) -> Self {
Self {
top: value,
right: 0,
bottom: value,
left: 0,
}
}
pub fn horizontal(value: u16) -> Self {
Self {
top: 0,
right: value,
bottom: 0,
left: value,
}
}
}
impl StyleBuilder {
pub fn background(mut self, color: Color) -> Self {
self.style.background = Some(color);
self
}
pub fn direction(mut self, direction: Direction) -> Self {
self.style.direction = Some(direction);
self
}
pub fn padding(mut self, padding: Spacing) -> Self {
self.style.padding = Some(padding);
self
}
pub fn overflow(mut self, overflow: Overflow) -> Self {
self.style.overflow = Some(overflow);
self
}
pub fn width(mut self, width: Dimension) -> Self {
self.style.width = Some(width);
self
}
pub fn height(mut self, height: Dimension) -> Self {
self.style.height = Some(height);
self
}
pub fn border(mut self, color: Color) -> Self {
self.style.border = Some(Border {
enabled: true,
style: BorderStyle::Single,
color,
edges: BorderEdges::ALL,
});
self
}
pub fn position(mut self, position: Position) -> Self {
self.style.position = Some(position);
self
}
pub fn z_index(mut self, z_index: i32) -> Self {
self.style.z_index = Some(z_index);
self
}
pub fn top(mut self, top: i16) -> Self {
self.style.top = Some(top);
self
}
pub fn right(mut self, right: i16) -> Self {
self.style.right = Some(right);
self
}
pub fn bottom(mut self, bottom: i16) -> Self {
self.style.bottom = Some(bottom);
self
}
pub fn left(mut self, left: i16) -> Self {
self.style.left = Some(left);
self
}
pub fn wrap(mut self, wrap: WrapMode) -> Self {
self.style.wrap = Some(wrap);
self
}
pub fn gap(mut self, gap: u16) -> Self {
self.style.gap = Some(gap);
self
}
pub fn build(self) -> Style {
self.style
}
}
impl Default for Style {
fn default() -> Self {
Self {
background: None,
direction: None,
padding: None,
overflow: None,
width: None,
height: None,
border: None,
position: None,
z_index: None,
top: None,
right: None,
bottom: None,
left: None,
wrap: None,
gap: None,
margin: None,
min_width: None,
min_height: None,
max_width: None,
max_height: None,
border_color: None,
x: None,
y: None,
show_scrollbar: None,
}
}
}
impl Default for TextStyle {
fn default() -> Self {
Self {
color: None,
background: None,
bold: None,
italic: None,
underline: None,
strikethrough: None,
wrap: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hex_color_parsing() {
assert_eq!(Color::from_hex("0").unwrap(), Color::Rgb(0, 0, 0));
assert_eq!(Color::from_hex("#F").unwrap(), Color::Rgb(255, 255, 255));
assert_eq!(Color::from_hex("8").unwrap(), Color::Rgb(136, 136, 136));
assert_eq!(Color::from_hex("#F00").unwrap(), Color::Rgb(255, 0, 0));
assert_eq!(Color::from_hex("0F0").unwrap(), Color::Rgb(0, 255, 0));
assert_eq!(Color::from_hex("#00F").unwrap(), Color::Rgb(0, 0, 255));
assert_eq!(Color::from_hex("F53").unwrap(), Color::Rgb(255, 85, 51));
assert_eq!(Color::from_hex("#FF5733").unwrap(), Color::Rgb(255, 87, 51));
assert_eq!(Color::from_hex("2ECC71").unwrap(), Color::Rgb(46, 204, 113));
assert_eq!(
Color::from_hex("#3498DB").unwrap(),
Color::Rgb(52, 152, 219)
);
assert_eq!(Color::from_hex("#abc").unwrap(), Color::Rgb(170, 187, 204));
assert_eq!(Color::from_hex("#ABC").unwrap(), Color::Rgb(170, 187, 204));
assert_eq!(
Color::from_hex("aaBBcc").unwrap(),
Color::Rgb(170, 187, 204)
);
assert!(Color::from_hex("").is_err());
assert!(Color::from_hex("12").is_err());
assert!(Color::from_hex("1234").is_err());
assert!(Color::from_hex("12345").is_err());
assert!(Color::from_hex("1234567").is_err());
assert!(Color::from_hex("GGG").is_err());
assert!(Color::from_hex("#GGGGGG").is_err());
}
#[test]
fn test_hex_panic_method() {
let color = Color::hex("#FF5733");
assert_eq!(color, Color::Rgb(255, 87, 51));
}
#[test]
#[should_panic(expected = "Invalid hex color")]
fn test_hex_panic_on_invalid() {
Color::hex("invalid");
}
#[test]
fn test_rgb_constructor() {
assert_eq!(Color::rgb(255, 165, 0), Color::Rgb(255, 165, 0));
assert_eq!(Color::rgb(0, 0, 0), Color::Rgb(0, 0, 0));
assert_eq!(Color::rgb(255, 255, 255), Color::Rgb(255, 255, 255));
}
}