Skip to main content

liora_components/
rate.rs

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}