use std::rc::Rc;
use gpui::prelude::*;
use gpui::{div, px, App, ElementId, IntoElement, SharedString, Window};
use crate::reactive::Binding;
use crate::style::ColorValue;
use crate::theme::{theme, ColorName, Size};
type ChangeHandler = Rc<dyn Fn(&f32, &mut Window, &mut App) + 'static>;
#[derive(IntoElement)]
pub struct Rating {
id: ElementId,
value: f32,
count: usize,
color: ColorValue,
size: Size,
readonly: bool,
binding: Option<Binding<f32>>,
on_change: Option<ChangeHandler>,
}
impl Rating {
pub fn new(id: impl Into<ElementId>) -> Self {
Rating {
id: id.into(),
value: 0.0,
count: 5,
color: ColorValue::Named(ColorName::Yellow),
size: Size::Md,
readonly: false,
binding: None,
on_change: None,
}
}
pub fn value(mut self, value: f32) -> Self {
self.value = value;
self
}
pub fn count(mut self, count: usize) -> Self {
self.count = count.max(1);
self
}
pub fn color(mut self, color: impl Into<ColorValue>) -> Self {
self.color = color.into();
self
}
pub fn size(mut self, size: Size) -> Self {
self.size = size;
self
}
pub fn readonly(mut self, readonly: bool) -> Self {
self.readonly = readonly;
self
}
pub fn bind(mut self, binding: Binding<f32>) -> Self {
self.binding = Some(binding);
self
}
pub fn on_change(mut self, handler: impl Fn(&f32, &mut Window, &mut App) + 'static) -> Self {
self.on_change = Some(Rc::new(handler));
self
}
fn glyph_px(&self) -> f32 {
match self.size {
Size::Xs => 14.0,
Size::Sm => 18.0,
Size::Md => 22.0,
Size::Lg => 28.0,
Size::Xl => 36.0,
}
}
}
fn filled_stars(value: f32, count: usize) -> usize {
if value <= 0.0 {
0
} else {
(value.round() as usize).min(count)
}
}
impl RenderOnce for Rating {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let t = theme(cx);
let accent = self.color.accent(t);
let empty = if t.scheme.is_dark() {
t.color(ColorName::Dark, 3)
} else {
t.color(ColorName::Gray, 4)
}
.hsla();
let glyph = self.glyph_px();
let value = self.binding.as_ref().map_or(self.value, |b| b.get(cx));
let filled = filled_stars(value, self.count);
let mut row = div()
.id(self.id)
.flex()
.flex_row()
.items_center()
.gap(px(2.0));
for i in 1..=self.count {
let is_filled = i <= filled;
let mut star = div()
.id(("guise-rating-star", i))
.text_size(px(glyph))
.text_color(if is_filled { accent } else { empty })
.child(SharedString::new_static(if is_filled {
"\u{2605}"
} else {
"\u{2606}"
}));
if !self.readonly {
star = star.cursor_pointer().hover(move |s| s.text_color(accent));
let binding = self.binding.clone();
let handler = self.on_change.clone();
let next = i as f32;
star = star.on_click(move |_ev, window, cx| {
if let Some(binding) = &binding {
binding.set(cx, next);
}
if let Some(handler) = &handler {
handler(&next, window, cx);
}
});
}
row = row.child(star);
}
row
}
}
#[cfg(test)]
mod tests {
use super::filled_stars;
#[test]
fn rounds_to_the_nearest_star() {
assert_eq!(filled_stars(2.4, 5), 2);
assert_eq!(filled_stars(2.5, 5), 3);
assert_eq!(filled_stars(3.0, 5), 3);
}
#[test]
fn clamps_into_the_star_range() {
assert_eq!(filled_stars(0.0, 5), 0);
assert_eq!(filled_stars(-1.0, 5), 0);
assert_eq!(filled_stars(9.0, 5), 5);
}
}