Skip to main content

elegance/
indicator.rs

1//! Status indicator light — a small circle with three visual states.
2
3use egui::{CornerRadius, Response, Sense, Stroke, Ui, Vec2, Widget, WidgetInfo, WidgetType};
4
5use crate::theme::{with_alpha, Theme};
6
7/// The three visual states of an [`Indicator`].
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
9pub enum IndicatorState {
10    /// Connected / healthy — solid green dot with a subtle glow.
11    On,
12    /// Disconnected / error — red horizontal bar inside a ring.
13    Off,
14    /// Transient / in-progress — amber ring only.
15    Connecting,
16}
17
18/// A small status light.
19#[derive(Debug, Clone, Copy)]
20#[must_use = "Add with `ui.add(...)`."]
21pub struct Indicator {
22    state: IndicatorState,
23    size: f32,
24}
25
26impl Indicator {
27    /// Create an indicator in the given state. Default size: 10 points.
28    pub fn new(state: IndicatorState) -> Self {
29        Self { state, size: 10.0 }
30    }
31
32    /// Override the indicator diameter in points.
33    pub fn size(mut self, size: f32) -> Self {
34        self.size = size;
35        self
36    }
37}
38
39impl Widget for Indicator {
40    fn ui(self, ui: &mut Ui) -> Response {
41        let theme = Theme::current(ui.ctx());
42        let p = &theme.palette;
43
44        let size = Vec2::splat(self.size);
45        let (rect, response) = ui.allocate_exact_size(size, Sense::hover());
46
47        if ui.is_rect_visible(rect) {
48            let painter = ui.painter();
49            let c = rect.center();
50            let r = self.size * 0.5;
51
52            match self.state {
53                IndicatorState::On => {
54                    // Soft outer glow.
55                    painter.circle_filled(c, r + 1.5, with_alpha(p.success, 70));
56                    painter.circle_filled(c, r, p.success);
57                }
58                IndicatorState::Off => {
59                    painter.circle_stroke(c, r - 0.5, Stroke::new(1.0, p.danger));
60                    // Horizontal bar inside.
61                    let bar_w = self.size * 0.7;
62                    let bar_h = 2.0;
63                    let bar = egui::Rect::from_center_size(c, Vec2::new(bar_w, bar_h));
64                    painter.rect_filled(bar, CornerRadius::same(1), p.danger);
65                }
66                IndicatorState::Connecting => {
67                    painter.circle_stroke(c, r - 0.5, Stroke::new(1.8, p.warning));
68                }
69            }
70        }
71
72        response.widget_info(|| {
73            WidgetInfo::labeled(
74                WidgetType::Other,
75                true,
76                match self.state {
77                    IndicatorState::On => "status: on",
78                    IndicatorState::Off => "status: off",
79                    IndicatorState::Connecting => "status: connecting",
80                },
81            )
82        });
83        response
84    }
85}