use crate::components::text::TextAlign;
use crate::theme::Color;
use std::any::TypeId;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub enum StyleProperty {
Color(Color),
Background(Color),
TextAlign(TextAlign),
Padding(u16),
Gap(u16),
Bold(bool),
Dim(bool),
Italic(bool),
Underline(bool),
}
#[derive(Debug, Clone, Default)]
pub struct Style {
properties: HashMap<&'static str, StyleProperty>,
}
impl Style {
pub fn new() -> Self {
Style {
properties: HashMap::new(),
}
}
pub fn color(mut self, color: Color) -> Self {
self.properties.insert("color", StyleProperty::Color(color));
self
}
pub fn background(mut self, color: Color) -> Self {
self.properties
.insert("background", StyleProperty::Background(color));
self
}
pub fn text_align(mut self, align: TextAlign) -> Self {
self.properties
.insert("text_align", StyleProperty::TextAlign(align));
self
}
pub fn padding(mut self, padding: u16) -> Self {
self.properties
.insert("padding", StyleProperty::Padding(padding));
self
}
pub fn gap(mut self, gap: u16) -> Self {
self.properties.insert("gap", StyleProperty::Gap(gap));
self
}
pub fn bold(mut self, bold: bool) -> Self {
self.properties.insert("bold", StyleProperty::Bold(bold));
self
}
pub fn dim(mut self, dim: bool) -> Self {
self.properties.insert("dim", StyleProperty::Dim(dim));
self
}
pub fn italic(mut self, italic: bool) -> Self {
self.properties
.insert("italic", StyleProperty::Italic(italic));
self
}
pub fn underline(mut self, underline: bool) -> Self {
self.properties
.insert("underline", StyleProperty::Underline(underline));
self
}
pub fn get(&self, key: &str) -> Option<&StyleProperty> {
self.properties.get(key)
}
pub fn has(&self, key: &str) -> bool {
self.properties.contains_key(key)
}
pub fn merge(mut self, other: &Style) -> Self {
for (key, value) in &other.properties {
self.properties.insert(key, value.clone());
}
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Selector {
Type(TypeId),
Name(&'static str),
Class(&'static str),
Id(&'static str),
}
#[derive(Debug, Clone)]
pub struct StyleRule {
selector: Selector,
style: Style,
priority: u16,
}
impl StyleRule {
pub fn new(selector: Selector, style: Style) -> Self {
StyleRule {
selector,
style,
priority: 0,
}
}
pub fn with_priority(mut self, priority: u16) -> Self {
self.priority = priority;
self
}
pub fn selector(&self) -> &Selector {
&self.selector
}
pub fn style(&self) -> &Style {
&self.style
}
pub fn priority(&self) -> u16 {
self.priority
}
}
#[derive(Debug, Clone, Default)]
pub struct StyleSheet {
rules: Vec<StyleRule>,
}
impl StyleSheet {
pub fn new() -> Self {
StyleSheet { rules: Vec::new() }
}
pub fn add_rule(mut self, rule: StyleRule) -> Self {
self.rules.push(rule);
self
}
pub fn style_type<T: 'static>(self, style: Style) -> Self {
self.add_rule(StyleRule::new(Selector::Type(TypeId::of::<T>()), style))
}
pub fn style_name(self, name: &'static str, style: Style) -> Self {
self.add_rule(StyleRule::new(Selector::Name(name), style))
}
pub fn style_class(self, class: &'static str, style: Style) -> Self {
self.add_rule(StyleRule::new(Selector::Class(class), style))
}
pub fn style_id(self, id: &'static str, style: Style) -> Self {
self.add_rule(StyleRule::new(Selector::Id(id), style))
}
pub fn get_styles(&self, selector: &Selector) -> Vec<&Style> {
let mut matching: Vec<_> = self
.rules
.iter()
.filter(|rule| &rule.selector == selector)
.collect();
matching.sort_by(|a, b| b.priority.cmp(&a.priority));
matching.iter().map(|rule| &rule.style).collect()
}
pub fn compute_style(&self, selectors: &[Selector]) -> Style {
let mut final_style = Style::new();
let mut all_rules: Vec<_> = selectors
.iter()
.flat_map(|selector| {
self.rules
.iter()
.filter(move |rule| &rule.selector == selector)
})
.collect();
all_rules.sort_by_key(|rule| rule.priority);
for rule in all_rules {
final_style = final_style.merge(&rule.style);
}
final_style
}
}
pub trait Styleable: 'static {
fn type_selector(&self) -> Selector {
Selector::Type(TypeId::of::<Self>())
}
fn name_selector(&self) -> Option<Selector> {
None
}
fn class_selectors(&self) -> Vec<Selector> {
Vec::new()
}
fn id_selector(&self) -> Option<Selector> {
None
}
fn selectors(&self) -> Vec<Selector> {
let mut selectors = vec![self.type_selector()];
if let Some(name) = self.name_selector() {
selectors.push(name);
}
selectors.extend(self.class_selectors());
if let Some(id) = self.id_selector() {
selectors.push(id);
}
selectors
}
fn compute_style(&self, stylesheet: &StyleSheet) -> Style {
stylesheet.compute_style(&self.selectors())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::components::text::Text;
#[test]
fn test_style_creation() {
let style = Style::new().bold(true).padding(2);
assert!(style.has("bold"));
assert!(style.has("padding"));
assert!(!style.has("color"));
}
#[test]
fn test_style_merge() {
let style1 = Style::new().bold(true).padding(2);
let style2 = Style::new().padding(4).dim(true);
let merged = style1.merge(&style2);
if let Some(StyleProperty::Padding(p)) = merged.get("padding") {
assert_eq!(*p, 4);
} else {
panic!("Expected padding property");
}
assert!(merged.has("bold"));
assert!(merged.has("dim"));
}
#[test]
fn test_stylesheet_type_selector() {
let stylesheet = StyleSheet::new().style_type::<Text>(Style::new().bold(true));
let selector = Selector::Type(TypeId::of::<Text>());
let styles = stylesheet.get_styles(&selector);
assert_eq!(styles.len(), 1);
assert!(styles[0].has("bold"));
}
#[test]
fn test_stylesheet_priority() {
let stylesheet = StyleSheet::new()
.add_rule(
StyleRule::new(Selector::Name("test"), Style::new().padding(2)).with_priority(1),
)
.add_rule(
StyleRule::new(Selector::Name("test"), Style::new().padding(4)).with_priority(10),
);
let final_style = stylesheet.compute_style(&[Selector::Name("test")]);
if let Some(StyleProperty::Padding(p)) = final_style.get("padding") {
assert_eq!(*p, 4);
} else {
panic!("Expected padding property");
}
}
#[test]
fn test_empty_stylesheet() {
let stylesheet = StyleSheet::new();
let style = stylesheet.compute_style(&[Selector::Name("test")]);
assert!(!style.has("padding"));
assert!(!style.has("color"));
}
#[test]
fn test_no_matching_selector() {
let stylesheet = StyleSheet::new().style_name("foo", Style::new().padding(2));
let style = stylesheet.compute_style(&[Selector::Name("bar")]);
assert!(!style.has("padding"));
}
#[test]
fn test_style_multiple_selectors() {
let stylesheet = StyleSheet::new()
.style_name("foo", Style::new().padding(2))
.style_class("bar", Style::new().bold(true));
let style = stylesheet.compute_style(&[Selector::Name("foo"), Selector::Class("bar")]);
assert!(style.has("padding"));
assert!(style.has("bold"));
}
#[test]
fn test_style_property_override() {
let stylesheet = StyleSheet::new()
.add_rule(
StyleRule::new(Selector::Name("test"), Style::new().padding(2)).with_priority(1),
)
.add_rule(
StyleRule::new(Selector::Class("test"), Style::new().padding(10)).with_priority(5),
);
let style = stylesheet.compute_style(&[Selector::Name("test"), Selector::Class("test")]);
if let Some(StyleProperty::Padding(p)) = style.get("padding") {
assert_eq!(*p, 10); } else {
panic!("Expected padding property");
}
}
}