use crate::components::Text;
use crate::core::{Color, Element};
#[derive(Debug, Clone)]
pub struct RatingSymbols {
pub filled: String,
pub half: String,
pub empty: String,
}
impl Default for RatingSymbols {
fn default() -> Self {
Self {
filled: "★".to_string(),
half: "☆".to_string(),
empty: "☆".to_string(),
}
}
}
impl RatingSymbols {
pub fn new(
filled: impl Into<String>,
half: impl Into<String>,
empty: impl Into<String>,
) -> Self {
Self {
filled: filled.into(),
half: half.into(),
empty: empty.into(),
}
}
pub fn stars() -> Self {
Self::default()
}
pub fn hearts() -> Self {
Self::new("♥", "♡", "♡")
}
pub fn circles() -> Self {
Self::new("●", "◐", "○")
}
pub fn squares() -> Self {
Self::new("■", "◧", "□")
}
pub fn emoji() -> Self {
Self::new("⭐", "✨", "☆")
}
pub fn ascii() -> Self {
Self::new("*", "+", "-")
}
}
#[derive(Debug, Clone)]
pub struct RatingStyle {
pub symbols: RatingSymbols,
pub filled_color: Color,
pub half_color: Color,
pub empty_color: Color,
pub show_value: bool,
pub value_format: String,
pub spacing: usize,
}
impl Default for RatingStyle {
fn default() -> Self {
Self {
symbols: RatingSymbols::default(),
filled_color: Color::Yellow,
half_color: Color::Yellow,
empty_color: Color::BrightBlack,
show_value: false,
value_format: "{:.1}".to_string(),
spacing: 0,
}
}
}
impl RatingStyle {
pub fn new() -> Self {
Self::default()
}
pub fn symbols(mut self, symbols: RatingSymbols) -> Self {
self.symbols = symbols;
self
}
pub fn filled_color(mut self, color: Color) -> Self {
self.filled_color = color;
self
}
pub fn empty_color(mut self, color: Color) -> Self {
self.empty_color = color;
self
}
pub fn show_value(mut self, show: bool) -> Self {
self.show_value = show;
self
}
pub fn spacing(mut self, spacing: usize) -> Self {
self.spacing = spacing;
self
}
pub fn colorful() -> Self {
Self::new()
.filled_color(Color::BrightYellow)
.empty_color(Color::BrightBlack)
}
pub fn minimal() -> Self {
Self::new()
.symbols(RatingSymbols::ascii())
.filled_color(Color::White)
.empty_color(Color::BrightBlack)
}
}
#[derive(Debug, Clone)]
pub struct Rating {
value: f32,
max: u8,
style: RatingStyle,
label: Option<String>,
}
impl Rating {
pub fn new(value: f32) -> Self {
Self {
value,
max: 5,
style: RatingStyle::default(),
label: None,
}
}
pub fn max(mut self, max: u8) -> Self {
self.max = max;
self
}
pub fn style(mut self, style: RatingStyle) -> Self {
self.style = style;
self
}
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
pub fn get_value(&self) -> f32 {
self.value
}
pub fn get_max(&self) -> u8 {
self.max
}
pub fn percentage(&self) -> f32 {
(self.value / self.max as f32) * 100.0
}
pub fn render(&self) -> String {
let mut result = String::new();
if let Some(label) = &self.label {
result.push_str(label);
result.push(' ');
}
let spacing = " ".repeat(self.style.spacing);
let full_stars = self.value.floor() as u8;
let has_half = (self.value - self.value.floor()) >= 0.5;
for i in 0..self.max {
if i > 0 && self.style.spacing > 0 {
result.push_str(&spacing);
}
if i < full_stars {
result.push_str(&format!(
"{}{}{}",
self.style.filled_color.to_ansi_fg(),
self.style.symbols.filled,
"\x1b[0m"
));
} else if i == full_stars && has_half {
result.push_str(&format!(
"{}{}{}",
self.style.half_color.to_ansi_fg(),
self.style.symbols.half,
"\x1b[0m"
));
} else {
result.push_str(&format!(
"{}{}{}",
self.style.empty_color.to_ansi_fg(),
self.style.symbols.empty,
"\x1b[0m"
));
}
}
if self.style.show_value {
result.push_str(&format!(" ({:.1}/{})", self.value, self.max));
}
result
}
pub fn into_element(self) -> Element {
Text::new(self.render()).into_element()
}
}
impl Default for Rating {
fn default() -> Self {
Self::new(0.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rating_creation() {
let r = Rating::new(4.5);
assert_eq!(r.value, 4.5);
assert_eq!(r.max, 5);
}
#[test]
fn test_rating_max() {
let r = Rating::new(8.0).max(10);
assert_eq!(r.max, 10);
}
#[test]
fn test_rating_percentage() {
let r = Rating::new(2.5).max(5);
assert_eq!(r.percentage(), 50.0);
}
#[test]
fn test_rating_render() {
let r = Rating::new(3.0).max(5);
let rendered = r.render();
assert!(rendered.contains("★"));
}
#[test]
fn test_rating_with_label() {
let r = Rating::new(4.0).label("Quality:");
let rendered = r.render();
assert!(rendered.contains("Quality:"));
}
#[test]
fn test_rating_symbols() {
let symbols = RatingSymbols::hearts();
assert_eq!(symbols.filled, "♥");
assert_eq!(symbols.empty, "♡");
}
#[test]
fn test_rating_style() {
let style = RatingStyle::new()
.symbols(RatingSymbols::circles())
.show_value(true);
assert!(style.show_value);
assert_eq!(style.symbols.filled, "●");
}
#[test]
fn test_rating_into_element() {
let r = Rating::new(4.0);
let _ = r.into_element();
}
#[test]
fn test_rating_symbols_presets() {
let _ = RatingSymbols::stars();
let _ = RatingSymbols::hearts();
let _ = RatingSymbols::circles();
let _ = RatingSymbols::squares();
let _ = RatingSymbols::emoji();
let _ = RatingSymbols::ascii();
}
#[test]
fn test_rating_style_presets() {
let _ = RatingStyle::colorful();
let _ = RatingStyle::minimal();
}
}