1use embedded_graphics::{pixelcolor::Rgb565, prelude::*, primitives::Rectangle};
8use zest_core::{Constraints, Length, RenderError, Renderer, TouchPhase, Widget};
9use zest_theme::Theme;
10
11mod cloudy;
12mod fog;
13mod rain;
14mod showers;
15mod snow;
16mod sunny;
17mod thunderstorm;
18mod unknown;
19mod windy;
20
21fn anchor(rect: Rectangle) -> (i32, i32, i32) {
29 let size = rect.size.width.min(rect.size.height) as i32;
30 let cx = rect.top_left.x + rect.size.width as i32 / 2;
31 let cy = rect.top_left.y + rect.size.height as i32 / 2;
32 (cx, cy, size)
33}
34
35#[cfg(test)]
36mod tests {
37 use super::*;
38 use embedded_graphics::{mono_font::MonoFont, text::Alignment};
39
40 #[derive(Default)]
41 struct Bbox {
42 minx: i32,
43 miny: i32,
44 maxx: i32,
45 maxy: i32,
46 any: bool,
47 }
48 impl Bbox {
49 fn add(&mut self, x0: i32, y0: i32, x1: i32, y1: i32) {
50 if !self.any {
51 (self.minx, self.miny, self.maxx, self.maxy, self.any) = (x0, y0, x1, y1, true);
52 } else {
53 self.minx = self.minx.min(x0);
54 self.miny = self.miny.min(y0);
55 self.maxx = self.maxx.max(x1);
56 self.maxy = self.maxy.max(y1);
57 }
58 }
59 fn cx(&self) -> i32 {
60 (self.minx + self.maxx) / 2
61 }
62 fn cy(&self) -> i32 {
63 (self.miny + self.maxy) / 2
64 }
65 }
66 struct Rec(Bbox);
67 impl Renderer<Rgb565> for Rec {
68 fn fill_rect(&mut self, r: Rectangle, _: Rgb565) -> Result<(), RenderError> {
69 self.0.add(
70 r.top_left.x,
71 r.top_left.y,
72 r.top_left.x + r.size.width as i32,
73 r.top_left.y + r.size.height as i32,
74 );
75 Ok(())
76 }
77 fn stroke_rect(&mut self, r: Rectangle, c: Rgb565) -> Result<(), RenderError> {
78 self.fill_rect(r, c)
79 }
80 fn fill_circle(&mut self, ctr: Point, radius: u32, _: Rgb565) -> Result<(), RenderError> {
81 let r = radius as i32;
82 self.0.add(ctr.x - r, ctr.y - r, ctr.x + r, ctr.y + r);
83 Ok(())
84 }
85 fn stroke_line(
86 &mut self,
87 a: Point,
88 b: Point,
89 _: Rgb565,
90 w: u32,
91 ) -> Result<(), RenderError> {
92 let w = w as i32;
93 self.0.add(
94 a.x.min(b.x) - w,
95 a.y.min(b.y) - w,
96 a.x.max(b.x) + w,
97 a.y.max(b.y) + w,
98 );
99 Ok(())
100 }
101 fn draw_text(
102 &mut self,
103 text: &str,
104 pos: Point,
105 font: &MonoFont,
106 _: Rgb565,
107 _: Alignment,
108 ) -> Result<(), RenderError> {
109 let half_w = text.chars().count() as i32 * font.character_size.width as i32 / 2;
110 self.0.add(
111 pos.x - half_w,
112 pos.y - font.baseline as i32,
113 pos.x + half_w,
114 pos.y,
115 );
116 Ok(())
117 }
118 }
119
120 fn bb(f: impl FnOnce(&mut Rec)) -> Bbox {
121 let mut r = Rec(Bbox::default());
122 f(&mut r);
123 r.0
124 }
125
126 #[test]
130 fn glyphs_centered() {
131 for rect in [
132 Rectangle::new(Point::new(0, 0), Size::new(200, 80)),
133 Rectangle::new(Point::new(0, 0), Size::new(80, 80)),
134 ] {
135 let (cx, cy, size) = anchor(rect);
136 let half = size / 2;
137 let check = |name: &str, b: Bbox, tol: i32| {
138 assert!((b.cx() - cx).abs() <= 2, "{name}: cx={} want {cx}", b.cx());
139 assert!(
140 (b.cy() - cy).abs() <= tol,
141 "{name}: cy={} want {cy}±{tol}",
142 b.cy()
143 );
144 assert!(
145 b.minx >= cx - half - 2
146 && b.maxx <= cx + half + 2
147 && b.miny >= cy - half - 2
148 && b.maxy <= cy + half + 2,
149 "{name}: bbox x[{}..{}] y[{}..{}] spills the centered square",
150 b.minx,
151 b.maxx,
152 b.miny,
153 b.maxy
154 );
155 };
156 check("sunny", bb(|r| sunny::draw(r, rect).unwrap()), 4);
158 check("cloudy", bb(|r| cloudy::draw(r, rect).unwrap()), 4);
159 check("rain", bb(|r| rain::draw(r, rect).unwrap()), 4);
160 check("snow", bb(|r| snow::draw(r, rect).unwrap()), 4);
161 check("thunder", bb(|r| thunderstorm::draw(r, rect).unwrap()), 4);
162 check("fog", bb(|r| fog::draw(r, rect).unwrap()), 4);
163 check("windy", bb(|r| windy::draw(r, rect).unwrap()), 4);
164 check("unknown", bb(|r| unknown::draw(r, rect).unwrap()), 4);
165 check(
168 "partly",
169 bb(|r| {
170 sunny::draw_small(r, rect).unwrap();
171 let (cx, cy, size) = anchor(rect);
172 cloudy::draw_at(r, Point::new(cx, cy + size / 12), size).unwrap();
173 }),
174 8,
175 );
176 check("showers", bb(|r| showers::draw(r, rect).unwrap()), 8);
177 }
178 }
179}
180
181#[derive(Debug, Clone, Copy, PartialEq)]
183pub enum WeatherCondition {
184 Sunny,
186 Cloudy,
188 Rain,
190 PartlyCloudy,
192 Showers,
194 Thunderstorm,
196 Snow,
198 Fog,
200 Windy,
202 Unknown,
204}
205
206pub struct WeatherIcon {
208 rect: Rectangle,
209 condition: WeatherCondition,
210 width: Length,
211 height: Length,
212}
213
214impl WeatherIcon {
215 pub fn new(condition: WeatherCondition) -> Self {
219 Self {
220 rect: Rectangle::zero(),
221 condition,
222 width: Length::Fill,
223 height: Length::Fill,
224 }
225 }
226
227 #[must_use]
229 pub fn width(mut self, width: impl Into<Length>) -> Self {
230 self.width = width.into();
231 self
232 }
233
234 #[must_use]
236 pub fn height(mut self, height: impl Into<Length>) -> Self {
237 self.height = height.into();
238 self
239 }
240}
241
242impl<M: Clone> Widget<Rgb565, M> for WeatherIcon {
243 fn draw<'t>(
244 &self,
245 renderer: &mut dyn Renderer<Rgb565>,
246 _theme: &Theme<'t, Rgb565>,
247 ) -> Result<(), RenderError> {
248 match self.condition {
249 WeatherCondition::Sunny => sunny::draw(renderer, self.rect),
250 WeatherCondition::Cloudy => cloudy::draw(renderer, self.rect),
251 WeatherCondition::Rain => rain::draw(renderer, self.rect),
252 WeatherCondition::PartlyCloudy => {
253 sunny::draw_small(renderer, self.rect)?;
254 let (cx, cy, size) = anchor(self.rect);
257 cloudy::draw_at(renderer, Point::new(cx, cy + size / 12), size)
258 }
259 WeatherCondition::Showers => showers::draw(renderer, self.rect),
260 WeatherCondition::Thunderstorm => thunderstorm::draw(renderer, self.rect),
261 WeatherCondition::Snow => snow::draw(renderer, self.rect),
262 WeatherCondition::Fog => fog::draw(renderer, self.rect),
263 WeatherCondition::Windy => windy::draw(renderer, self.rect),
264 WeatherCondition::Unknown => unknown::draw(renderer, self.rect),
265 }
266 }
267
268 fn measure(&mut self, constraints: Constraints) -> Size {
269 let w = self
270 .width
271 .resolve(constraints.max.width, constraints.max.width);
272 let h = self
273 .height
274 .resolve(constraints.max.height, constraints.max.height);
275 constraints.clamp(Size::new(w, h))
276 }
277
278 fn preferred_size(&self) -> (Length, Length) {
279 (self.width, self.height)
280 }
281
282 fn arrange(&mut self, rect: Rectangle) {
283 self.rect = rect;
284 }
285
286 fn rect(&self) -> Rectangle {
287 self.rect
288 }
289
290 fn handle_touch(&mut self, _: Point, _: TouchPhase) -> Option<M> {
291 None
292 }
293}