use embedded_graphics::{pixelcolor::Rgb565, prelude::*, primitives::Rectangle};
use zest_core::{Constraints, Length, RenderError, Renderer, TouchPhase, Widget};
use zest_theme::Theme;
mod cloudy;
mod fog;
mod rain;
mod showers;
mod snow;
mod sunny;
mod thunderstorm;
mod unknown;
mod windy;
fn anchor(rect: Rectangle) -> (i32, i32, i32) {
let size = rect.size.width.min(rect.size.height) as i32;
let cx = rect.top_left.x + rect.size.width as i32 / 2;
let cy = rect.top_left.y + rect.size.height as i32 / 2;
(cx, cy, size)
}
#[cfg(test)]
mod tests {
use super::*;
use embedded_graphics::{mono_font::MonoFont, text::Alignment};
#[derive(Default)]
struct Bbox {
minx: i32,
miny: i32,
maxx: i32,
maxy: i32,
any: bool,
}
impl Bbox {
fn add(&mut self, x0: i32, y0: i32, x1: i32, y1: i32) {
if !self.any {
(self.minx, self.miny, self.maxx, self.maxy, self.any) = (x0, y0, x1, y1, true);
} else {
self.minx = self.minx.min(x0);
self.miny = self.miny.min(y0);
self.maxx = self.maxx.max(x1);
self.maxy = self.maxy.max(y1);
}
}
fn cx(&self) -> i32 {
(self.minx + self.maxx) / 2
}
fn cy(&self) -> i32 {
(self.miny + self.maxy) / 2
}
}
struct Rec(Bbox);
impl Renderer<Rgb565> for Rec {
fn fill_rect(&mut self, r: Rectangle, _: Rgb565) -> Result<(), RenderError> {
self.0.add(
r.top_left.x,
r.top_left.y,
r.top_left.x + r.size.width as i32,
r.top_left.y + r.size.height as i32,
);
Ok(())
}
fn stroke_rect(&mut self, r: Rectangle, c: Rgb565) -> Result<(), RenderError> {
self.fill_rect(r, c)
}
fn fill_circle(&mut self, ctr: Point, radius: u32, _: Rgb565) -> Result<(), RenderError> {
let r = radius as i32;
self.0.add(ctr.x - r, ctr.y - r, ctr.x + r, ctr.y + r);
Ok(())
}
fn stroke_line(
&mut self,
a: Point,
b: Point,
_: Rgb565,
w: u32,
) -> Result<(), RenderError> {
let w = w as i32;
self.0.add(
a.x.min(b.x) - w,
a.y.min(b.y) - w,
a.x.max(b.x) + w,
a.y.max(b.y) + w,
);
Ok(())
}
fn draw_text(
&mut self,
text: &str,
pos: Point,
font: &MonoFont,
_: Rgb565,
_: Alignment,
) -> Result<(), RenderError> {
let half_w = text.chars().count() as i32 * font.character_size.width as i32 / 2;
self.0.add(
pos.x - half_w,
pos.y - font.baseline as i32,
pos.x + half_w,
pos.y,
);
Ok(())
}
}
fn bb(f: impl FnOnce(&mut Rec)) -> Bbox {
let mut r = Rec(Bbox::default());
f(&mut r);
r.0
}
#[test]
fn glyphs_centered() {
for rect in [
Rectangle::new(Point::new(0, 0), Size::new(200, 80)),
Rectangle::new(Point::new(0, 0), Size::new(80, 80)),
] {
let (cx, cy, size) = anchor(rect);
let half = size / 2;
let check = |name: &str, b: Bbox, tol: i32| {
assert!((b.cx() - cx).abs() <= 2, "{name}: cx={} want {cx}", b.cx());
assert!(
(b.cy() - cy).abs() <= tol,
"{name}: cy={} want {cy}±{tol}",
b.cy()
);
assert!(
b.minx >= cx - half - 2
&& b.maxx <= cx + half + 2
&& b.miny >= cy - half - 2
&& b.maxy <= cy + half + 2,
"{name}: bbox x[{}..{}] y[{}..{}] spills the centered square",
b.minx,
b.maxx,
b.miny,
b.maxy
);
};
check("sunny", bb(|r| sunny::draw(r, rect).unwrap()), 4);
check("cloudy", bb(|r| cloudy::draw(r, rect).unwrap()), 4);
check("rain", bb(|r| rain::draw(r, rect).unwrap()), 4);
check("snow", bb(|r| snow::draw(r, rect).unwrap()), 4);
check("thunder", bb(|r| thunderstorm::draw(r, rect).unwrap()), 4);
check("fog", bb(|r| fog::draw(r, rect).unwrap()), 4);
check("windy", bb(|r| windy::draw(r, rect).unwrap()), 4);
check("unknown", bb(|r| unknown::draw(r, rect).unwrap()), 4);
check(
"partly",
bb(|r| {
sunny::draw_small(r, rect).unwrap();
let (cx, cy, size) = anchor(rect);
cloudy::draw_at(r, Point::new(cx, cy + size / 12), size).unwrap();
}),
8,
);
check("showers", bb(|r| showers::draw(r, rect).unwrap()), 8);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum WeatherCondition {
Sunny,
Cloudy,
Rain,
PartlyCloudy,
Showers,
Thunderstorm,
Snow,
Fog,
Windy,
Unknown,
}
pub struct WeatherIcon {
rect: Rectangle,
condition: WeatherCondition,
width: Length,
height: Length,
}
impl WeatherIcon {
pub fn new(condition: WeatherCondition) -> Self {
Self {
rect: Rectangle::zero(),
condition,
width: Length::Fill,
height: Length::Fill,
}
}
#[must_use]
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
#[must_use]
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
}
impl<M: Clone> Widget<Rgb565, M> for WeatherIcon {
fn draw<'t>(
&self,
renderer: &mut dyn Renderer<Rgb565>,
_theme: &Theme<'t, Rgb565>,
) -> Result<(), RenderError> {
match self.condition {
WeatherCondition::Sunny => sunny::draw(renderer, self.rect),
WeatherCondition::Cloudy => cloudy::draw(renderer, self.rect),
WeatherCondition::Rain => rain::draw(renderer, self.rect),
WeatherCondition::PartlyCloudy => {
sunny::draw_small(renderer, self.rect)?;
let (cx, cy, size) = anchor(self.rect);
cloudy::draw_at(renderer, Point::new(cx, cy + size / 12), size)
}
WeatherCondition::Showers => showers::draw(renderer, self.rect),
WeatherCondition::Thunderstorm => thunderstorm::draw(renderer, self.rect),
WeatherCondition::Snow => snow::draw(renderer, self.rect),
WeatherCondition::Fog => fog::draw(renderer, self.rect),
WeatherCondition::Windy => windy::draw(renderer, self.rect),
WeatherCondition::Unknown => unknown::draw(renderer, self.rect),
}
}
fn measure(&mut self, constraints: Constraints) -> Size {
let w = self
.width
.resolve(constraints.max.width, constraints.max.width);
let h = self
.height
.resolve(constraints.max.height, constraints.max.height);
constraints.clamp(Size::new(w, h))
}
fn preferred_size(&self) -> (Length, Length) {
(self.width, self.height)
}
fn arrange(&mut self, rect: Rectangle) {
self.rect = rect;
}
fn rect(&self) -> Rectangle {
self.rect
}
fn handle_touch(&mut self, _: Point, _: TouchPhase) -> Option<M> {
None
}
}