1use crate::theme::ActiveTheme;
2use crate::{Disableable, Icon, IconName, Sizable, Size, StyledExt, h_flex};
3use std::rc::Rc;
4
5use gpui::{
6 App, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce, StyleRefinement,
7 Styled, Window, div, prelude::FluentBuilder as _,
8};
9use gpui::{ClickEvent, Hsla, StatefulInteractiveElement};
10
11#[derive(IntoElement)]
13pub struct Rating {
14 id: ElementId,
15 style: StyleRefinement,
16 size: Size,
17 disabled: bool,
18 value: usize,
19 max: usize,
20 color: Option<Hsla>,
21 on_click: Option<Rc<dyn Fn(&usize, &mut Window, &mut App) + 'static>>,
22}
23
24impl Rating {
25 pub fn new(id: impl Into<ElementId>) -> Self {
27 Self {
28 id: id.into(),
29 style: StyleRefinement::default(),
30 size: Size::Medium,
31 disabled: false,
32 value: 0,
33 max: 5,
34 color: None,
35 on_click: None,
36 }
37 }
38
39 pub fn with_size(mut self, size: impl Into<Size>) -> Self {
41 self.size = size.into();
42 self
43 }
44
45 pub fn disabled(mut self, disabled: bool) -> Self {
47 self.disabled = disabled;
48 self
49 }
50
51 pub fn color(mut self, color: impl Into<Hsla>) -> Self {
53 self.color = Some(color.into());
54 self
55 }
56
57 pub fn value(mut self, value: usize) -> Self {
59 self.value = value;
60 if self.value > self.max {
61 self.value = self.max;
62 }
63 self
64 }
65
66 pub fn max(mut self, max: usize) -> Self {
68 self.max = max;
69 if self.value > self.max {
70 self.value = self.max;
71 }
72 self
73 }
74
75 pub fn on_click(mut self, handler: impl Fn(&usize, &mut Window, &mut App) + 'static) -> Self {
79 self.on_click = Some(Rc::new(handler));
80 self
81 }
82}
83
84impl Styled for Rating {
85 fn style(&mut self) -> &mut gpui::StyleRefinement {
86 &mut self.style
87 }
88}
89
90impl Sizable for Rating {
91 fn with_size(mut self, size: impl Into<Size>) -> Self {
92 self.size = size.into();
93 self
94 }
95}
96
97impl Disableable for Rating {
98 fn disabled(mut self, disabled: bool) -> Self {
99 self.disabled = disabled;
100 self
101 }
102}
103
104struct RaitingState {
105 default_value: usize,
107 value: usize,
109 hovered_value: usize,
111}
112
113impl RenderOnce for Rating {
114 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
115 let id = self.id;
116 let size = self.size;
117 let disabled = self.disabled;
118 let max = self.max;
119 let default_value = self.value;
120 let active_color = self.color.unwrap_or(cx.theme().yellow);
121 let on_click = self.on_click.clone();
122
123 let state = window.use_keyed_state(id.clone(), cx, |_, _| RaitingState {
124 default_value,
125 value: default_value,
126 hovered_value: 0,
127 });
128
129 if state.read(cx).default_value != default_value {
131 state.update(cx, |state, _| {
132 state.default_value = default_value;
133 state.value = default_value;
134 });
135 }
136 let value = state.read(cx).value;
137
138 h_flex()
139 .id(id)
140 .flex_nowrap()
141 .refine_style(&self.style)
142 .on_hover(window.listener_for(&state, move |state, hovered, _, cx| {
143 if !hovered {
144 state.hovered_value = 0;
145 cx.notify();
146 }
147 }))
148 .map(|mut this| {
149 for ix in 1..=max {
150 let filled = ix <= value;
151 let hovered = state.read(cx).hovered_value >= ix;
152
153 this = this.child(
154 div()
155 .id(ix)
156 .p_0p5()
157 .flex_none()
158 .flex_shrink_0()
159 .when(filled || hovered, |this| this.text_color(active_color))
160 .child(
161 Icon::new(if filled {
162 IconName::StarFill
163 } else {
164 IconName::Star
165 })
166 .with_size(size),
167 )
168 .when(!disabled, |this| {
169 this.on_mouse_move(window.listener_for(
170 &state,
171 move |state, _, _, cx| {
172 state.hovered_value = ix;
173 cx.notify();
174 },
175 ))
176 .on_click({
177 let state = state.clone();
178 let on_click = on_click.clone();
179 move |_: &ClickEvent, window, cx| {
180 let new = if value >= ix {
181 ix.saturating_sub(1)
182 } else {
183 ix
184 };
185
186 state.update(cx, |state, cx| {
187 state.value = new;
188 cx.notify();
189 });
190
191 if let Some(on_click) = &on_click {
192 on_click(&new, window, cx);
193 }
194 }
195 })
196 }),
197 );
198 }
199
200 this
201 })
202 }
203}