1use crate::gpui_compat::element_id;
2use crate::motion::pop_in;
3use gpui::{App, Context, FocusHandle, Focusable, MouseButton, Render, Window, prelude::*, px};
4use liora_core::Config;
5use liora_icons::Icon;
6use liora_icons_lucide::IconName;
7
8pub struct Rate {
9 value: f32,
10 max: usize,
11 hover_value: Option<f32>,
12 disabled: bool,
13 focus_handle: FocusHandle,
14 on_change: Option<Box<dyn Fn(f32, &mut Window, &mut App) + 'static>>,
15}
16
17impl Rate {
18 pub fn new(value: f32, cx: &mut Context<Self>) -> Self {
19 Self {
20 value,
21 max: 5,
22 hover_value: None,
23 disabled: false,
24 focus_handle: cx.focus_handle(),
25 on_change: None,
26 }
27 }
28
29 pub fn max(mut self, max: usize) -> Self {
30 self.max = max;
31 self
32 }
33 pub fn disabled(mut self, d: bool) -> Self {
34 self.disabled = d;
35 self
36 }
37
38 pub fn on_change(mut self, cb: impl Fn(f32, &mut Window, &mut App) + 'static) -> Self {
39 self.on_change = Some(Box::new(cb));
40 self
41 }
42
43 fn set_value(&mut self, val: f32, window: &mut Window, cx: &mut Context<Self>) {
44 if (val - self.value).abs() > f32::EPSILON {
45 self.value = val;
46 if let Some(ref cb) = self.on_change {
47 cb(self.value, window, cx);
48 }
49 cx.notify();
50 }
51 }
52}
53
54impl Focusable for Rate {
55 fn focus_handle(&self, _cx: &App) -> FocusHandle {
56 self.focus_handle.clone()
57 }
58}
59
60impl Render for Rate {
61 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
62 let theme = &cx.global::<Config>().theme;
63 let icon_sz = 20.0;
64
65 let view_id = cx.entity().entity_id().as_u64();
66
67 let mut row = gpui::div()
68 .id(element_id(format!("rate-container-{view_id}")))
69 .relative()
70 .flex()
71 .flex_row()
72 .items_center()
73 .gap_1();
74
75 if !self.disabled {
76 row = row.track_focus(&self.focus_handle).on_hover(cx.listener(
77 |this, hovered, _, cx| {
78 if !hovered && this.hover_value.is_some() {
79 this.hover_value = None;
80 cx.notify();
81 }
82 },
83 ));
84 }
85
86 for i in 1..=self.max {
87 let active_val = self.hover_value.unwrap_or(self.value);
88 let is_active = i as f32 <= active_val;
89
90 let color = if is_active {
91 theme.warning.base
92 } else {
93 theme.neutral.border
94 };
95
96 let mut star = gpui::div()
97 .id(element_id(format!("rate-star-{view_id}-{i}")))
98 .flex()
99 .items_center()
100 .justify_center()
101 .child({
102 let icon = Icon::new(IconName::Star).size(px(icon_sz)).color(color);
103 let icon_shell = gpui::div()
104 .flex()
105 .items_center()
106 .justify_center()
107 .child(icon);
108 if is_active {
109 pop_in(
110 element_id(format!("rate-star-motion-{view_id}-{i}")),
111 icon_shell,
112 )
113 .into_any_element()
114 } else {
115 icon_shell.into_any_element()
116 }
117 });
118
119 if !self.disabled {
120 star = star
121 .cursor_pointer()
122 .on_hover(cx.listener(move |this, hovered, _, cx| {
123 let hover_value = Some(i as f32);
124 match (*hovered, this.hover_value == hover_value) {
125 (true, false) => {
126 this.hover_value = hover_value;
127 cx.notify();
128 }
129 (false, true) => {
130 this.hover_value = None;
131 cx.notify();
132 }
133 _ => {}
134 }
135 }))
136 .on_mouse_down(
137 MouseButton::Left,
138 cx.listener(move |this, _, window, cx| {
139 this.set_value(i as f32, window, cx);
140 }),
141 );
142 } else {
143 star = star.cursor_not_allowed();
144 }
145
146 row = row.child(star);
147 }
148
149 row
150 }
151}