Skip to main content

elegance/
pill.rs

1//! Status pill — a rounded capsule of small labelled status lights.
2
3use egui::{
4    vec2, CornerRadius, Margin, Response, Stroke, Ui, Widget, WidgetInfo, WidgetText, WidgetType,
5};
6
7use crate::{
8    indicator::{Indicator, IndicatorState},
9    theme::Theme,
10};
11
12/// A capsule-shaped row of `(label, state)` status items.
13///
14/// ```no_run
15/// # use elegance::{StatusPill, IndicatorState};
16/// # egui::__run_test_ui(|ui| {
17/// ui.add(
18///     StatusPill::new()
19///         .item("UI", IndicatorState::On)
20///         .item("API", IndicatorState::Connecting)
21///         .item("DB", IndicatorState::Off),
22/// );
23/// # });
24/// ```
25#[derive(Default)]
26#[must_use = "Add with `ui.add(...)`."]
27pub struct StatusPill {
28    items: Vec<(WidgetText, IndicatorState)>,
29}
30
31impl std::fmt::Debug for StatusPill {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        f.debug_struct("StatusPill")
34            .field(
35                "items",
36                &self
37                    .items
38                    .iter()
39                    .map(|(l, s)| (l.text().to_string(), *s))
40                    .collect::<Vec<_>>(),
41            )
42            .finish()
43    }
44}
45
46impl StatusPill {
47    /// Create an empty status pill. Add rows with [`StatusPill::item`].
48    pub fn new() -> Self {
49        Self { items: Vec::new() }
50    }
51
52    /// Append a `(label, state)` row to the pill.
53    pub fn item(mut self, label: impl Into<WidgetText>, state: IndicatorState) -> Self {
54        self.items.push((label.into(), state));
55        self
56    }
57}
58
59impl Widget for StatusPill {
60    fn ui(self, ui: &mut Ui) -> Response {
61        let theme = Theme::current(ui.ctx());
62        let p = &theme.palette;
63        let t = &theme.typography;
64
65        let frame = egui::Frame::new()
66            .fill(p.card)
67            .stroke(Stroke::new(1.0, p.border))
68            .corner_radius(CornerRadius::same(99))
69            .inner_margin(Margin::symmetric(12, 4));
70
71        let response = frame
72            .show(ui, |ui| {
73                ui.horizontal(|ui| {
74                    ui.spacing_mut().item_spacing = vec2(10.0, 0.0);
75                    for (i, (label, state)) in self.items.iter().enumerate() {
76                        if i > 0 {
77                            ui.add_space(4.0);
78                        }
79                        ui.add(Indicator::new(*state));
80                        let rt = egui::RichText::new(label.text())
81                            .color(p.text_faint)
82                            .size(t.small);
83                        ui.add(egui::Label::new(rt).wrap_mode(egui::TextWrapMode::Extend));
84                    }
85                });
86            })
87            .response;
88
89        response.widget_info(|| WidgetInfo::labeled(WidgetType::Other, true, "status pill"));
90        response
91    }
92}