Skip to main content

zest_widget/widget/
led.rs

1//! Passive status indicator. Draws a filled circle via
2//! [`Renderer::fill_circle`] sized to fit its arranged rect.
3//!
4//! Two construction styles:
5//!
6//! * [`LED::new`] — an always-on LED of an explicit color.
7//! * [`LED::from_state`] — an on/off LED whose color is chosen from the
8//!   `on`/`off` colors (the off color defaults to the theme divider).
9
10use super::Widget;
11use core::marker::PhantomData;
12use embedded_graphics::{pixelcolor::PixelColor, prelude::*, primitives::Rectangle};
13use zest_core::{Constraints, Length, RenderError, Renderer, TouchPhase};
14use zest_theme::Theme;
15
16/// Default LED diameter when sizing intent is `Shrink`.
17const DEFAULT_DIAMETER: u32 = 16;
18
19/// A filled-circle status light.
20pub struct LED<C: PixelColor, M: Clone> {
21    rect: Rectangle,
22    /// Color drawn when `on`.
23    on_color: C,
24    /// Color drawn when not `on`. `None` falls back to the theme divider.
25    off_color: Option<C>,
26    /// Whether the LED is lit.
27    on: bool,
28    width: Length,
29    height: Length,
30    _phantom: PhantomData<M>,
31}
32
33impl<C: PixelColor, M: Clone> LED<C, M> {
34    /// New always-on LED of `color`. Defaults to a fixed square slot;
35    /// position is assigned by the parent via `arrange`.
36    pub fn new(color: C) -> Self {
37        Self {
38            rect: Rectangle::zero(),
39            on_color: color,
40            off_color: None,
41            on: true,
42            width: Length::Fixed(DEFAULT_DIAMETER),
43            height: Length::Fixed(DEFAULT_DIAMETER),
44            _phantom: PhantomData,
45        }
46    }
47
48    /// New on/off LED. `on_color` is drawn when lit; the off color
49    /// defaults to `theme.background.divider` unless set via
50    /// [`Self::off_color`].
51    pub fn from_state(on: bool, on_color: C) -> Self {
52        Self {
53            rect: Rectangle::zero(),
54            on_color,
55            off_color: None,
56            on,
57            width: Length::Fixed(DEFAULT_DIAMETER),
58            height: Length::Fixed(DEFAULT_DIAMETER),
59            _phantom: PhantomData,
60        }
61    }
62
63    /// Set the lit state.
64    #[must_use]
65    pub fn on(mut self, on: bool) -> Self {
66        self.on = on;
67        self
68    }
69
70    /// Override the lit color.
71    #[must_use]
72    pub fn on_color(mut self, color: C) -> Self {
73        self.on_color = color;
74        self
75    }
76
77    /// Set the unlit color (default: `theme.background.divider`).
78    #[must_use]
79    pub fn off_color(mut self, color: C) -> Self {
80        self.off_color = Some(color);
81        self
82    }
83
84    /// Width sizing intent.
85    #[must_use]
86    pub fn width(mut self, width: impl Into<Length>) -> Self {
87        self.width = width.into();
88        self
89    }
90
91    /// Height sizing intent.
92    #[must_use]
93    pub fn height(mut self, height: impl Into<Length>) -> Self {
94        self.height = height.into();
95        self
96    }
97}
98
99impl<C: PixelColor, M: Clone> Widget<C, M> for LED<C, M> {
100    fn measure(&mut self, constraints: Constraints) -> Size {
101        let w = self.width.resolve(DEFAULT_DIAMETER, constraints.max.width);
102        let h = self
103            .height
104            .resolve(DEFAULT_DIAMETER, constraints.max.height);
105        constraints.clamp(Size::new(w, h))
106    }
107
108    fn preferred_size(&self) -> (Length, Length) {
109        (self.width, self.height)
110    }
111
112    fn arrange(&mut self, rect: Rectangle) {
113        self.rect = rect;
114    }
115
116    fn rect(&self) -> Rectangle {
117        self.rect
118    }
119
120    fn handle_touch(&mut self, _point: Point, _phase: TouchPhase) -> Option<M> {
121        None
122    }
123
124    fn draw<'t>(
125        &self,
126        renderer: &mut dyn Renderer<C>,
127        theme: &Theme<'t, C>,
128    ) -> Result<(), RenderError> {
129        let color = if self.on {
130            self.on_color
131        } else {
132            self.off_color.unwrap_or(theme.background.divider)
133        };
134        // Largest circle centered in the arranged rect.
135        let radius = self.rect.size.width.min(self.rect.size.height) / 2;
136        if radius == 0 {
137            return Ok(());
138        }
139        let center = self.rect.top_left
140            + Point::new(
141                self.rect.size.width as i32 / 2,
142                self.rect.size.height as i32 / 2,
143            );
144        renderer.fill_circle(center, radius, color)
145    }
146}