use super::breakpoints::Breakpoint;
use super::responsive_config::ResponsiveConfig;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Default)]
pub struct ResponsiveBuilder {
classes: HashMap<Breakpoint, Vec<String>>,
config: ResponsiveConfig,
}
impl ResponsiveBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn with_config(config: ResponsiveConfig) -> Self {
Self {
classes: HashMap::new(),
config,
}
}
pub fn add_class(&mut self, breakpoint: Breakpoint, class: impl Into<String>) -> &mut Self {
self.classes
.entry(breakpoint)
.or_default()
.push(class.into());
self
}
pub fn add_classes(&mut self, breakpoint: Breakpoint, classes: Vec<String>) -> &mut Self {
self.classes
.entry(breakpoint)
.or_default()
.extend(classes);
self
}
pub fn base(&mut self, class: impl Into<String>) -> &mut Self {
self.add_class(Breakpoint::Base, class)
}
pub fn sm(&mut self, class: impl Into<String>) -> &mut Self {
self.add_class(Breakpoint::Sm, class)
}
pub fn md(&mut self, class: impl Into<String>) -> &mut Self {
self.add_class(Breakpoint::Md, class)
}
pub fn lg(&mut self, class: impl Into<String>) -> &mut Self {
self.add_class(Breakpoint::Lg, class)
}
pub fn xl(&mut self, class: impl Into<String>) -> &mut Self {
self.add_class(Breakpoint::Xl, class)
}
pub fn xl2(&mut self, class: impl Into<String>) -> &mut Self {
self.add_class(Breakpoint::Xl2, class)
}
pub fn responsive(
&mut self,
base: impl Into<String>,
sm: Option<String>,
md: Option<String>,
lg: Option<String>,
xl: Option<String>,
xl2: Option<String>,
) -> &mut Self {
self.base(base);
if let Some(sm_class) = sm {
self.sm(sm_class);
}
if let Some(md_class) = md {
self.md(md_class);
}
if let Some(lg_class) = lg {
self.lg(lg_class);
}
if let Some(xl_class) = xl {
self.xl(xl_class);
}
if let Some(xl2_class) = xl2 {
self.xl2(xl2_class);
}
self
}
pub fn get_classes(&self, breakpoint: Breakpoint) -> Vec<String> {
self.classes.get(&breakpoint).cloned().unwrap_or_default()
}
pub fn get_all_classes(&self) -> HashMap<Breakpoint, Vec<String>> {
self.classes.clone()
}
pub fn is_empty(&self) -> bool {
self.classes.is_empty() || self.classes.values().all(|classes| classes.is_empty())
}
pub fn len(&self) -> usize {
self.classes.len()
}
pub fn clear(&mut self) {
self.classes.clear();
}
pub fn remove_breakpoint(&mut self, breakpoint: Breakpoint) -> Vec<String> {
self.classes.remove(&breakpoint).unwrap_or_default()
}
pub fn build(&self) -> String {
let mut classes = Vec::new();
for breakpoint in Breakpoint::all() {
if let Some(breakpoint_classes) = self.classes.get(&breakpoint) {
if !breakpoint_classes.is_empty() {
let breakpoint_classes_str = breakpoint_classes.join(" ");
if breakpoint == Breakpoint::Base {
classes.push(breakpoint_classes_str);
} else {
classes.push(format!("{}{}", breakpoint.prefix(), breakpoint_classes_str));
}
}
}
}
classes.join(" ")
}
pub fn build_for_width(&self, screen_width: u32) -> String {
let mut classes = Vec::new();
let target_breakpoint = self.config.get_breakpoint_for_width(screen_width);
for breakpoint in Breakpoint::all() {
if breakpoint.min_width() <= target_breakpoint.min_width() {
if let Some(breakpoint_classes) = self.classes.get(&breakpoint) {
if !breakpoint_classes.is_empty() {
let breakpoint_classes_str = breakpoint_classes.join(" ");
if breakpoint == Breakpoint::Base {
classes.push(breakpoint_classes_str);
} else {
classes.push(format!(
"{}{}",
breakpoint.prefix(),
breakpoint_classes_str
));
}
}
}
}
}
classes.join(" ")
}
pub fn get_config(&self) -> &ResponsiveConfig {
&self.config
}
pub fn update_config(&mut self, config: ResponsiveConfig) {
self.config = config;
}
}
impl std::fmt::Display for ResponsiveBuilder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.build())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_responsive_builder_new() {
let builder = ResponsiveBuilder::new();
assert!(builder.is_empty());
assert_eq!(builder.len(), 0);
}
#[test]
fn test_responsive_builder_add_class() {
let mut builder = ResponsiveBuilder::new();
builder.add_class(Breakpoint::Base, "text-sm");
builder.add_class(Breakpoint::Sm, "text-base");
assert!(!builder.is_empty());
assert_eq!(builder.len(), 2);
assert_eq!(builder.get_classes(Breakpoint::Base), vec!["text-sm"]);
assert_eq!(builder.get_classes(Breakpoint::Sm), vec!["text-base"]);
}
#[test]
fn test_responsive_builder_add_classes() {
let mut builder = ResponsiveBuilder::new();
builder.add_classes(
Breakpoint::Base,
vec!["text-sm".to_string(), "font-medium".to_string()],
);
assert_eq!(
builder.get_classes(Breakpoint::Base),
vec!["text-sm", "font-medium"]
);
}
#[test]
fn test_responsive_builder_breakpoint_methods() {
let mut builder = ResponsiveBuilder::new();
builder.base("text-sm");
builder.sm("text-base");
builder.md("text-lg");
builder.lg("text-xl");
builder.xl("text-2xl");
builder.xl2("text-3xl");
assert_eq!(builder.get_classes(Breakpoint::Base), vec!["text-sm"]);
assert_eq!(builder.get_classes(Breakpoint::Sm), vec!["text-base"]);
assert_eq!(builder.get_classes(Breakpoint::Md), vec!["text-lg"]);
assert_eq!(builder.get_classes(Breakpoint::Lg), vec!["text-xl"]);
assert_eq!(builder.get_classes(Breakpoint::Xl), vec!["text-2xl"]);
assert_eq!(builder.get_classes(Breakpoint::Xl2), vec!["text-3xl"]);
}
#[test]
fn test_responsive_builder_responsive() {
let mut builder = ResponsiveBuilder::new();
builder.responsive(
"text-sm",
Some("text-base".to_string()),
Some("text-lg".to_string()),
None,
None,
None,
);
assert_eq!(builder.get_classes(Breakpoint::Base), vec!["text-sm"]);
assert_eq!(builder.get_classes(Breakpoint::Sm), vec!["text-base"]);
assert_eq!(builder.get_classes(Breakpoint::Md), vec!["text-lg"]);
assert_eq!(builder.get_classes(Breakpoint::Lg), Vec::<String>::new());
}
#[test]
fn test_responsive_builder_build() {
let mut builder = ResponsiveBuilder::new();
builder.base("text-sm");
builder.sm("text-base");
builder.md("text-lg");
let result = builder.build();
assert!(result.contains("text-sm"));
assert!(result.contains("sm:text-base"));
assert!(result.contains("md:text-lg"));
}
#[test]
fn test_responsive_builder_build_for_width() {
let mut builder = ResponsiveBuilder::new();
builder.base("text-sm");
builder.sm("text-base");
builder.md("text-lg");
let result_0 = builder.build_for_width(0);
assert!(result_0.contains("text-sm"));
assert!(!result_0.contains("sm:"));
assert!(!result_0.contains("md:"));
let result_640 = builder.build_for_width(640);
assert!(result_640.contains("text-sm"));
assert!(result_640.contains("sm:text-base"));
assert!(!result_640.contains("md:"));
let result_768 = builder.build_for_width(768);
assert!(result_768.contains("text-sm"));
assert!(result_768.contains("sm:text-base"));
assert!(result_768.contains("md:text-lg"));
}
#[test]
fn test_responsive_builder_clear() {
let mut builder = ResponsiveBuilder::new();
builder.base("text-sm");
builder.sm("text-base");
assert!(!builder.is_empty());
builder.clear();
assert!(builder.is_empty());
}
#[test]
fn test_responsive_builder_remove_breakpoint() {
let mut builder = ResponsiveBuilder::new();
builder.base("text-sm");
builder.sm("text-base");
assert_eq!(builder.len(), 2);
let removed = builder.remove_breakpoint(Breakpoint::Sm);
assert_eq!(removed, vec!["text-base"]);
assert_eq!(builder.len(), 1);
assert_eq!(builder.get_classes(Breakpoint::Sm), Vec::<String>::new());
}
#[test]
fn test_responsive_builder_display() {
let mut builder = ResponsiveBuilder::new();
builder.base("text-sm");
builder.sm("text-base");
let result = format!("{}", builder);
assert!(result.contains("text-sm"));
assert!(result.contains("sm:text-base"));
}
}