egui_components/
rating.rs1use egui::{pos2, vec2, Color32, Pos2, Response, Sense, Stroke, Ui, Widget};
9use egui_components_theme::{mix, Theme};
10
11pub struct Rating<'a> {
12 value: &'a mut u32,
13 max: u32,
14 star_size: f32,
15 gap: f32,
16 read_only: bool,
17 color: Option<Color32>,
18}
19
20impl<'a> Rating<'a> {
21 pub fn new(value: &'a mut u32) -> Self {
22 Self {
23 value,
24 max: 5,
25 star_size: 20.0,
26 gap: 4.0,
27 read_only: false,
28 color: None,
29 }
30 }
31 pub fn max(mut self, m: u32) -> Self {
32 self.max = m.max(1);
33 self
34 }
35 pub fn star_size(mut self, s: f32) -> Self {
36 self.star_size = s;
37 self
38 }
39 pub fn read_only(mut self) -> Self {
40 self.read_only = true;
41 self
42 }
43 pub fn color(mut self, c: Color32) -> Self {
44 self.color = Some(c);
45 self
46 }
47}
48
49impl<'a> Widget for Rating<'a> {
50 fn ui(self, ui: &mut Ui) -> Response {
51 let theme = Theme::get(ui.ctx());
52 let c = theme.colors;
53 let fill = self.color.unwrap_or(c.warning_background);
54 let empty = mix(c.muted_foreground, c.background, 0.4);
55
56 let n = self.max as f32;
57 let total = vec2(n * self.star_size + (n - 1.0) * self.gap, self.star_size);
58 let sense = if self.read_only {
59 Sense::hover()
60 } else {
61 Sense::click()
62 };
63 let (rect, mut response) = ui.allocate_exact_size(total, sense);
64
65 let hover_index = if !self.read_only && response.hovered() {
67 response.hover_pos().map(|p| {
68 let rel = (p.x - rect.left()) / (self.star_size + self.gap);
69 (rel.floor() as i64 + 1).clamp(1, self.max as i64) as u32
70 })
71 } else {
72 None
73 };
74
75 if let Some(h) = hover_index {
76 if response.clicked() {
77 *self.value = if *self.value == h { 0 } else { h };
79 response.mark_changed();
80 }
81 }
82
83 let shown = hover_index.unwrap_or(*self.value);
84
85 if ui.is_rect_visible(rect) {
86 for i in 0..self.max {
87 let cx = rect.left() + self.star_size * 0.5 + i as f32 * (self.star_size + self.gap);
88 let center = pos2(cx, rect.center().y);
89 let filled = i < shown;
90 draw_star(
91 ui.painter(),
92 center,
93 self.star_size * 0.5,
94 if filled { fill } else { Color32::TRANSPARENT },
95 Stroke::new(1.4, if filled { fill } else { empty }),
96 );
97 }
98 if !self.read_only && response.hovered() {
99 ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
100 }
101 }
102
103 response
104 }
105}
106
107fn draw_star(painter: &egui::Painter, center: Pos2, radius: f32, fill: Color32, stroke: Stroke) {
108 let inner = radius * 0.4;
109 let mut pts = Vec::with_capacity(10);
110 let start = -std::f32::consts::FRAC_PI_2;
111 for k in 0..10 {
112 let r = if k % 2 == 0 { radius } else { inner };
113 let a = start + std::f32::consts::PI * (k as f32) / 5.0;
114 let (s, co) = a.sin_cos();
115 pts.push(center + vec2(co, s) * r);
116 }
117 painter.add(egui::Shape::convex_polygon(pts, fill, stroke));
118}