fix: resolve all clippy warnings breaking CI

- introduce Color struct to reduce argument count in canvas drawing functions
- remove unused write_to method
- use is_multiple_of() per clippy suggestion
This commit is contained in:
kilyabin
2026-06-18 00:45:31 +04:00
parent 1978c14a68
commit 17f8a13188
5 changed files with 144 additions and 216 deletions
+54 -54
View File
@@ -1,6 +1,25 @@
use fontdue::{Font, FontSettings};
use image::RgbaImage;
// ── Color ─────────────────────────────────────────────────────────────────────
#[derive(Clone, Copy, Debug)]
pub struct Color {
pub r: u8,
pub g: u8,
pub b: u8,
pub a: u8,
}
impl Color {
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b, a: 255 }
}
pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
Self { r, g, b, a }
}
}
// ── Canvas ────────────────────────────────────────────────────────────────────
// Pixel buffer: each u32 = 0x00RRGGBB (softbuffer format)
@@ -21,62 +40,59 @@ impl Canvas {
self.buf.resize((width * height) as usize, 0);
}
/// Clear to black (OLED off)
pub fn clear(&mut self) {
self.buf.fill(0);
}
#[inline]
pub fn set_pixel(&mut self, x: i32, y: i32, r: u8, g: u8, b: u8, a: u8) {
pub fn set_pixel(&mut self, x: i32, y: i32, c: Color) {
if x < 0 || y < 0 || x >= self.width as i32 || y >= self.height as i32 { return; }
let idx = y as usize * self.width as usize + x as usize;
if a == 255 {
self.buf[idx] = rgb(r, g, b);
} else if a > 0 {
// alpha-blend over current pixel
if c.a == 255 {
self.buf[idx] = pack(c.r, c.g, c.b);
} else if c.a > 0 {
let dst = self.buf[idx];
let dr = ((dst >> 16) & 0xff) as u8;
let dg = ((dst >> 8) & 0xff) as u8;
let db = (dst & 0xff) as u8;
let af = a as u32;
let or_ = ((r as u32 * af + dr as u32 * (255 - af)) / 255) as u8;
let og = ((g as u32 * af + dg as u32 * (255 - af)) / 255) as u8;
let ob = ((b as u32 * af + db as u32 * (255 - af)) / 255) as u8;
self.buf[idx] = rgb(or_, og, ob);
let af = c.a as u32;
let or_ = ((c.r as u32 * af + dr as u32 * (255 - af)) / 255) as u8;
let og = ((c.g as u32 * af + dg as u32 * (255 - af)) / 255) as u8;
let ob = ((c.b as u32 * af + db as u32 * (255 - af)) / 255) as u8;
self.buf[idx] = pack(or_, og, ob);
}
}
pub fn fill_rect(&mut self, x: i32, y: i32, w: u32, h: u32, r: u8, g: u8, b: u8, a: u8) {
pub fn fill_rect(&mut self, x: i32, y: i32, w: u32, h: u32, c: Color) {
for dy in 0..h as i32 {
for dx in 0..w as i32 {
self.set_pixel(x + dx, y + dy, r, g, b, a);
self.set_pixel(x + dx, y + dy, c);
}
}
}
pub fn draw_circle(&mut self, cx: i32, cy: i32, radius: i32, r: u8, g: u8, b: u8, a: u8) {
pub fn draw_circle(&mut self, cx: i32, cy: i32, radius: i32, c: Color) {
for dy in -radius..=radius {
for dx in -radius..=radius {
if dx * dx + dy * dy <= radius * radius {
self.set_pixel(cx + dx, cy + dy, r, g, b, a);
self.set_pixel(cx + dx, cy + dy, c);
}
}
}
}
pub fn draw_ring(&mut self, cx: i32, cy: i32, outer: i32, inner: i32, r: u8, g: u8, b: u8, a: u8) {
pub fn draw_ring(&mut self, cx: i32, cy: i32, outer: i32, inner: i32, c: Color) {
for dy in -outer..=outer {
for dx in -outer..=outer {
let d2 = dx * dx + dy * dy;
if d2 <= outer * outer && d2 >= inner * inner {
self.set_pixel(cx + dx, cy + dy, r, g, b, a);
self.set_pixel(cx + dx, cy + dy, c);
}
}
}
}
/// Draw a line using Bresenham's algorithm
pub fn draw_line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, thickness: u32, r: u8, g: u8, b: u8, a: u8) {
pub fn draw_line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, thickness: u32, c: Color) {
let dx = (x1 - x0).abs();
let dy = (y1 - y0).abs();
let sx = if x0 < x1 { 1 } else { -1 };
@@ -87,7 +103,7 @@ impl Canvas {
loop {
for ty in -(t / 2)..=(t / 2) {
for tx in -(t / 2)..=(t / 2) {
self.set_pixel(x + tx, y + ty, r, g, b, a);
self.set_pixel(x + tx, y + ty, c);
}
}
if x == x1 && y == y1 { break; }
@@ -97,35 +113,28 @@ impl Canvas {
}
}
/// Draw an RGBA image at (x, y)
pub fn draw_image(&mut self, x: i32, y: i32, img: &RgbaImage) {
for (px, py, pixel) in img.enumerate_pixels() {
self.set_pixel(
x + px as i32,
y + py as i32,
pixel[0], pixel[1], pixel[2], pixel[3],
);
for (px, py, p) in img.enumerate_pixels() {
self.set_pixel(x + px as i32, y + py as i32, Color::rgba(p[0], p[1], p[2], p[3]));
}
}
/// Draw an RGBA image scaled to fill (w, h) at (x, y)
pub fn draw_image_scaled(&mut self, x: i32, y: i32, w: u32, h: u32, img: &RgbaImage) {
let sw = img.width();
let sh = img.height();
for dy in 0..h {
for dx in 0..w {
let sx = (dx as f32 / w as f32 * sw as f32) as u32;
let sy = (dy as f32 / h as f32 * sh as f32) as u32;
let sx = sx.min(sw - 1);
let sy = sy.min(sh - 1);
let p = img.get_pixel(sx, sy);
self.set_pixel(x + dx as i32, y + dy as i32, p[0], p[1], p[2], p[3]);
let sx = ((dx as f32 / w as f32) * sw as f32) as u32;
let sy = ((dy as f32 / h as f32) * sh as f32) as u32;
let p = img.get_pixel(sx.min(sw - 1), sy.min(sh - 1));
self.set_pixel(x + dx as i32, y + dy as i32, Color::rgba(p[0], p[1], p[2], p[3]));
}
}
}
/// Render text, returns (pixel_width, pixel_height)
pub fn draw_text(&mut self, x: i32, y: i32, text: &str, font: &Font, size: f32, r: u8, g: u8, b: u8) -> (u32, u32) {
/// Render text; coverage from font bitmap is multiplied with `c.a`.
/// Returns (rendered_width, rendered_height).
pub fn draw_text(&mut self, x: i32, y: i32, text: &str, font: &Font, size: f32, c: Color) -> (u32, u32) {
let mut cursor = x;
let mut max_h = 0u32;
for ch in text.chars() {
@@ -136,7 +145,8 @@ impl Canvas {
for px in 0..metrics.width {
let cov = bitmap[py * metrics.width + px];
if cov > 0 {
self.set_pixel(bx + px as i32, by + py as i32, r, g, b, cov);
let a = ((cov as u32 * c.a as u32) / 255) as u8;
self.set_pixel(bx + px as i32, by + py as i32, Color::rgba(c.r, c.g, c.b, a));
}
}
}
@@ -146,7 +156,6 @@ impl Canvas {
((cursor - x) as u32, max_h)
}
/// Measure text without rendering
pub fn measure_text(text: &str, font: &Font, size: f32) -> (u32, u32) {
let mut w = 0u32;
let mut h = 0u32;
@@ -158,37 +167,29 @@ impl Canvas {
(w, h)
}
/// Draw rounded rectangle (Material card)
pub fn draw_rounded_rect(&mut self, x: i32, y: i32, w: u32, h: u32, radius: u32, r: u8, g: u8, b: u8, a: u8) {
pub fn draw_rounded_rect(&mut self, x: i32, y: i32, w: u32, h: u32, radius: u32, c: Color) {
let rad = radius as i32;
// fill center
self.fill_rect(x + rad, y, w - 2 * radius, h, r, g, b, a);
self.fill_rect(x, y + rad, w, h - 2 * radius, r, g, b, a);
// four corner circles
self.fill_rect(x + rad, y, w - 2 * radius, h, c);
self.fill_rect(x, y + rad, w, h - 2 * radius, c);
let corners = [
(x + rad, y + rad),
(x + rad, y + rad),
(x + w as i32 - rad - 1, y + rad),
(x + rad, y + h as i32 - rad - 1),
(x + rad, y + h as i32 - rad - 1),
(x + w as i32 - rad - 1, y + h as i32 - rad - 1),
];
for (cx, cy) in corners {
self.draw_circle(cx, cy, rad, r, g, b, a);
self.draw_circle(cx, cy, rad, c);
}
}
pub fn write_to(&self, target: &mut [u32]) {
target.copy_from_slice(&self.buf);
}
}
#[inline]
pub fn rgb(r: u8, g: u8, b: u8) -> u32 {
fn pack(r: u8, g: u8, b: u8) -> u32 {
((r as u32) << 16) | ((g as u32) << 8) | b as u32
}
// ── Font loading ─────────────────────────────────────────────────────────────
/// Try common system font paths, return the first that loads
pub fn load_system_font() -> Option<Font> {
let candidates = [
"/usr/share/fonts/TTF/DejaVuSans.ttf",
@@ -205,7 +206,6 @@ pub fn load_system_font() -> Option<Font> {
"/usr/share/fonts/truetype/ubuntu/Ubuntu-R.ttf",
];
// also check user-local fonts
let home_fonts = dirs::home_dir()
.map(|h| h.join(".local/share/fonts"))
.unwrap_or_default();
+25 -47
View File
@@ -1,28 +1,26 @@
use fontdue::Font;
use crate::{canvas::Canvas, config::{TextAnimation, parse_hex_color}};
use crate::{canvas::{Canvas, Color}, config::{TextAnimation, parse_hex_color}};
use super::Screensaver;
pub struct TextScreensaver {
content: String,
font: Font,
font_size: f32,
color: (u8, u8, u8),
color: Color,
animation: TextAnimation,
time: f64,
// Lissajous orbit for anti-burn-in floating
lx: f64,
ly: f64,
lx: f64,
ly: f64,
}
impl TextScreensaver {
pub fn new(content: String, font: Font, font_size: f32, color: &str, animation: TextAnimation) -> Self {
let color = parse_hex_color(color);
// random-ish starting phase so it doesn't start center every time
let (r, g, b) = parse_hex_color(color);
Self {
content,
font,
font_size,
color,
color: Color::rgb(r, g, b),
animation,
time: 0.0,
lx: 0.0,
@@ -38,66 +36,46 @@ impl Screensaver for TextScreensaver {
let (cw, ch) = (canvas.width as f32, canvas.height as f32);
let (tw, th) = Canvas::measure_text(&self.content, &self.font, self.font_size);
let c = self.color;
let (ox, oy) = match self.animation {
TextAnimation::Float => {
// Lissajous curve: a=3, b=2 gives a nice non-repeating pattern
let margin_x = (cw * 0.3) as f64;
let margin_y = (ch * 0.3) as f64;
let cx = cw as f64 / 2.0 - tw as f64 / 2.0;
let cy = ch as f64 / 2.0 - th as f64 / 2.0;
let x = cx + margin_x * (3.0 * self.time * 0.05 + self.lx).sin();
let y = cy + margin_y * (2.0 * self.time * 0.05 + self.ly).sin();
(x as i32, y as i32)
}
TextAnimation::Typewriter => {
let cx = (cw / 2.0 - tw as f32 / 2.0) as i32;
let cy = (ch / 2.0 - th as f32 / 2.0) as i32;
(cx, cy)
}
TextAnimation::Fade => {
let cx = (cw / 2.0 - tw as f32 / 2.0) as i32;
let cy = (ch / 2.0 - th as f32 / 2.0) as i32;
(cx, cy)
(
(cx + margin_x * (3.0 * self.time * 0.05 + self.lx).sin()) as i32,
(cy + margin_y * (2.0 * self.time * 0.05 + self.ly).sin()) as i32,
)
}
TextAnimation::Typewriter | TextAnimation::Fade => (
(cw / 2.0 - tw as f32 / 2.0) as i32,
(ch / 2.0 - th as f32 / 2.0) as i32,
),
};
let (r, g, b) = self.color;
match self.animation {
TextAnimation::Float => {
canvas.draw_text(ox, oy + th as i32, &self.content, &self.font, self.font_size, r, g, b);
canvas.draw_text(ox, oy + th as i32, &self.content, &self.font, self.font_size, c);
}
TextAnimation::Typewriter => {
// show chars one by one, cycling every 3 seconds per char
let chars: Vec<char> = self.content.chars().collect();
let visible = ((self.time * 2.0) as usize % (chars.len() * 2 + 4)).min(chars.len());
let partial: String = chars[..visible].iter().collect();
canvas.draw_text(ox, oy + th as i32, &partial, &self.font, self.font_size, r, g, b);
// blinking cursor
if (self.time * 2.0) as u32 % 2 == 0 && visible < chars.len() {
canvas.draw_text(ox, oy + th as i32, &partial, &self.font, self.font_size, c);
if ((self.time * 2.0) as u32).is_multiple_of(2) && visible < chars.len() {
let (pw, _) = Canvas::measure_text(&partial, &self.font, self.font_size);
canvas.fill_rect(ox + pw as i32, oy, 3, th, r, g, b, 200);
canvas.fill_rect(ox + pw as i32, oy, 3, th, Color::rgba(c.r, c.g, c.b, 200));
}
}
TextAnimation::Fade => {
// fade in/out cycle: 3s in, 1s hold, 3s out, 1s black
let cycle = 8.0_f64;
let t = self.time % cycle;
let alpha: u8 = if t < 3.0 {
(t / 3.0 * 255.0) as u8
} else if t < 4.0 {
255
} else if t < 7.0 {
((7.0 - t) / 3.0 * 255.0) as u8
} else {
0
};
// draw to temp layer
let ar = (r as u32 * alpha as u32 / 255) as u8;
let ag = (g as u32 * alpha as u32 / 255) as u8;
let ab = (b as u32 * alpha as u32 / 255) as u8;
canvas.draw_text(ox, oy + th as i32, &self.content, &self.font, self.font_size, ar, ag, ab);
let t = self.time % 8.0;
let alpha: u8 = if t < 3.0 { (t / 3.0 * 255.0) as u8 }
else if t < 4.0 { 255 }
else if t < 7.0 { ((7.0 - t) / 3.0 * 255.0) as u8 }
else { 0 };
canvas.draw_text(ox, oy + th as i32, &self.content, &self.font, self.font_size, Color::rgba(c.r, c.g, c.b, alpha));
}
}
}
+31 -52
View File
@@ -3,7 +3,7 @@ use fontdue::Font;
use std::f64::consts::PI;
use crate::{
canvas::Canvas,
canvas::{Canvas, Color},
config::{ClockStyle, Position, parse_hex_color},
};
use super::Widget;
@@ -11,7 +11,7 @@ use super::Widget;
pub struct ClockWidget {
font: Font,
font_size: f32,
color: (u8, u8, u8),
color: Color,
position: Position,
style: ClockStyle,
show_seconds: bool,
@@ -19,30 +19,22 @@ pub struct ClockWidget {
impl ClockWidget {
pub fn new(font: Font, font_size: f32, color: &str, position: Position, style: ClockStyle, show_seconds: bool) -> Self {
Self {
font,
font_size,
color: parse_hex_color(color),
position,
style,
show_seconds,
}
let (r, g, b) = parse_hex_color(color);
Self { font, font_size, color: Color::rgb(r, g, b), position, style, show_seconds }
}
}
impl Widget for ClockWidget {
fn render(&mut self, canvas: &mut Canvas, _dt: f64) {
let now = Local::now();
let (r, g, b) = self.color;
match self.style {
ClockStyle::Digital => render_digital(self, canvas, &now, r, g, b),
ClockStyle::Analog => render_analog(self, canvas, &now, r, g, b),
ClockStyle::Digital => render_digital(self, canvas, &now),
ClockStyle::Analog => render_analog(self, canvas, &now),
}
}
}
fn render_digital(w: &ClockWidget, canvas: &mut Canvas, now: &chrono::DateTime<chrono::Local>, r: u8, g: u8, b: u8) {
fn render_digital(w: &ClockWidget, canvas: &mut Canvas, now: &chrono::DateTime<chrono::Local>) {
let text = if w.show_seconds {
now.format("%H:%M:%S").to_string()
} else {
@@ -50,47 +42,46 @@ fn render_digital(w: &ClockWidget, canvas: &mut Canvas, now: &chrono::DateTime<c
};
let (tw, th) = Canvas::measure_text(&text, &w.font, w.font_size);
let x = w.position.x.resolve(canvas.width, tw);
let y = w.position.y.resolve(canvas.height, th) + th as i32;
// Material 3 card background — very slightly lifted from black
let x = w.position.x.resolve(canvas.width, tw);
let y = w.position.y.resolve(canvas.height, th) + th as i32;
let pad = (w.font_size * 0.4) as u32;
canvas.draw_rounded_rect(
x - pad as i32,
y - th as i32 - pad as i32,
tw + pad * 2,
th + pad * 2,
(w.font_size * 0.25) as u32,
10, 10, 10, 200,
Color::rgba(10, 10, 10, 200),
);
canvas.draw_text(x, y, &text, &w.font, w.font_size, r, g, b);
canvas.draw_text(x, y, &text, &w.font, w.font_size, w.color);
}
fn render_analog(w: &ClockWidget, canvas: &mut Canvas, now: &chrono::DateTime<chrono::Local>, r: u8, g: u8, b: u8) {
fn render_analog(w: &ClockWidget, canvas: &mut Canvas, now: &chrono::DateTime<chrono::Local>) {
use chrono::Timelike;
let size = (w.font_size * 3.0) as i32;
let cx = w.position.x.resolve(canvas.width, size as u32 * 2) + size;
let cy = w.position.y.resolve(canvas.height, size as u32 * 2) + size;
let radius = size;
let c = w.color;
// Face ring
canvas.draw_ring(cx, cy, radius, radius - 4, r / 3, g / 3, b / 3, 180);
canvas.draw_ring(cx, cy, radius, radius - 4, Color::rgba(c.r / 3, c.g / 3, c.b / 3, 180));
// Hour ticks
for i in 0..12 {
let angle = i as f64 * PI / 6.0 - PI / 2.0;
let inner = (radius as f64 * 0.85) as i32;
let outer = radius - 2;
let x0 = cx + (angle.cos() * inner as f64) as i32;
let y0 = cy + (angle.sin() * inner as f64) as i32;
let x1 = cx + (angle.cos() * outer as f64) as i32;
let y1 = cy + (angle.sin() * outer as f64) as i32;
canvas.draw_line(x0, y0, x1, y1, 2, r / 2, g / 2, b / 2, 255);
canvas.draw_line(
cx + (angle.cos() * inner as f64) as i32,
cy + (angle.sin() * inner as f64) as i32,
cx + (angle.cos() * outer as f64) as i32,
cy + (angle.sin() * outer as f64) as i32,
2,
Color::rgba(c.r / 2, c.g / 2, c.b / 2, 255),
);
}
// Hands
let h = now.hour() as f64 % 12.0;
let m = now.minute() as f64;
let s = now.second() as f64;
@@ -99,28 +90,16 @@ fn render_analog(w: &ClockWidget, canvas: &mut Canvas, now: &chrono::DateTime<ch
let minute_angle = (m / 60.0 + s / 3600.0) * 2.0 * PI - PI / 2.0;
let second_angle = (s / 60.0) * 2.0 * PI - PI / 2.0;
let hr = (radius as f64 * 0.55) as i32;
let mr = (radius as f64 * 0.78) as i32;
let sr = (radius as f64 * 0.88) as i32;
let hr = (radius as f64 * 0.55) as i32;
let mr = (radius as f64 * 0.78) as i32;
let sr = (radius as f64 * 0.88) as i32;
canvas.draw_line(cx, cy, cx + (hour_angle.cos() * hr as f64) as i32, cy + (hour_angle.sin() * hr as f64) as i32, 4, Color::rgba(c.r, c.g, c.b, 255));
canvas.draw_line(cx, cy, cx + (minute_angle.cos() * mr as f64) as i32, cy + (minute_angle.sin() * mr as f64) as i32, 3, Color::rgba(c.r, c.g, c.b, 220));
// hour hand
canvas.draw_line(cx, cy,
cx + (hour_angle.cos() * hr as f64) as i32,
cy + (hour_angle.sin() * hr as f64) as i32,
4, r, g, b, 255);
// minute hand
canvas.draw_line(cx, cy,
cx + (minute_angle.cos() * mr as f64) as i32,
cy + (minute_angle.sin() * mr as f64) as i32,
3, r, g, b, 220);
// second hand (accent color — Material 3 tertiary)
if w.show_seconds {
canvas.draw_line(cx, cy,
cx + (second_angle.cos() * sr as f64) as i32,
cy + (second_angle.sin() * sr as f64) as i32,
2, 255, 120, 80, 220);
canvas.draw_line(cx, cy, cx + (second_angle.cos() * sr as f64) as i32, cy + (second_angle.sin() * sr as f64) as i32, 2, Color::rgba(255, 120, 80, 220));
}
// center dot
canvas.draw_circle(cx, cy, 5, r, g, b, 255);
canvas.draw_circle(cx, cy, 5, Color::rgba(c.r, c.g, c.b, 255));
}
+9 -15
View File
@@ -2,7 +2,7 @@ use chrono::Local;
use fontdue::Font;
use crate::{
canvas::Canvas,
canvas::{Canvas, Color},
config::{Position, parse_hex_color},
};
use super::Widget;
@@ -10,30 +10,24 @@ use super::Widget;
pub struct DateWidget {
font: Font,
font_size: f32,
color: (u8, u8, u8),
color: Color,
position: Position,
format: String,
}
impl DateWidget {
pub fn new(font: Font, font_size: f32, color: &str, position: Position, format: String) -> Self {
Self {
font,
font_size,
color: parse_hex_color(color),
position,
format,
}
let (r, g, b) = parse_hex_color(color);
Self { font, font_size, color: Color::rgb(r, g, b), position, format }
}
}
impl Widget for DateWidget {
fn render(&mut self, canvas: &mut Canvas, _dt: f64) {
let text = Local::now().format(&self.format).to_string();
let (r, g, b) = self.color;
let (tw, th) = Canvas::measure_text(&text, &self.font, self.font_size);
let x = self.position.x.resolve(canvas.width, tw);
let y = self.position.y.resolve(canvas.height, th) + th as i32;
canvas.draw_text(x, y, &text, &self.font, self.font_size, r, g, b);
let text = Local::now().format(&self.format).to_string();
let (tw, th) = Canvas::measure_text(&text, &self.font, self.font_size);
let x = self.position.x.resolve(canvas.width, tw);
let y = self.position.y.resolve(canvas.height, th) + th as i32;
canvas.draw_text(x, y, &text, &self.font, self.font_size, self.color);
}
}
+25 -48
View File
@@ -3,13 +3,11 @@ use serde::Deserialize;
use std::sync::{Arc, Mutex};
use crate::{
canvas::Canvas,
canvas::{Canvas, Color},
config::{Position, WeatherUnits, parse_hex_color},
};
use super::Widget;
// ── OpenWeatherMap response structs ──────────────────────────────────────────
#[derive(Deserialize)]
struct OWMResponse {
main: OWMMain,
@@ -29,8 +27,6 @@ struct OWMWeather {
icon: String,
}
// ── Widget ───────────────────────────────────────────────────────────────────
struct WeatherData {
temp: f32,
humidity: u32,
@@ -40,31 +36,24 @@ struct WeatherData {
}
pub struct WeatherWidget {
font: Font,
font_size: f32,
color: (u8, u8, u8),
position: Position,
api_key: String,
location: String,
units: WeatherUnits,
data: Arc<Mutex<Option<WeatherData>>>,
font: Font,
font_size: f32,
color: Color,
position: Position,
api_key: String,
location: String,
units: WeatherUnits,
data: Arc<Mutex<Option<WeatherData>>>,
refresh_in: f64,
}
impl WeatherWidget {
pub fn new(
font: Font,
font_size: f32,
color: &str,
position: Position,
api_key: String,
location: String,
units: WeatherUnits,
) -> Self {
pub fn new(font: Font, font_size: f32, color: &str, position: Position, api_key: String, location: String, units: WeatherUnits) -> Self {
let (r, g, b) = parse_hex_color(color);
let widget = Self {
font,
font_size,
color: parse_hex_color(color),
color: Color::rgb(r, g, b),
position,
api_key,
location,
@@ -90,12 +79,11 @@ impl WeatherWidget {
Ok(resp) => match resp.json::<OWMResponse>().await {
Ok(json) => {
let w = json.weather.into_iter().next();
let mut lock = data.lock().unwrap();
*lock = Some(WeatherData {
*data.lock().unwrap() = Some(WeatherData {
temp: json.main.temp,
humidity: json.main.humidity,
description: w.as_ref().map(|w| w.description.clone()).unwrap_or_default(),
icon: w.as_ref().map(|w| w.icon.clone()).unwrap_or_default(),
description: w.as_ref().map(|x| x.description.clone()).unwrap_or_default(),
icon: w.as_ref().map(|x| x.icon.clone()).unwrap_or_default(),
city: json.name,
});
}
@@ -111,53 +99,42 @@ impl Widget for WeatherWidget {
fn render(&mut self, canvas: &mut Canvas, dt: f64) {
self.refresh_in -= dt;
if self.refresh_in <= 0.0 {
self.refresh_in = 1800.0; // refresh every 30 min
self.refresh_in = 1800.0;
self.fetch_async();
}
let lock = self.data.lock().unwrap();
let (r, g, b) = self.color;
let unit_sym = match self.units { WeatherUnits::Metric => "°C", WeatherUnits::Imperial => "°F" };
let text = match lock.as_ref() {
let lock = self.data.lock().unwrap();
let text = match lock.as_ref() {
None => "Loading weather…".to_string(),
Some(d) => format!(
"{} {} {:.0}{unit_sym} H:{:.0}% {}",
d.city,
icon_char(&d.icon),
d.temp,
d.humidity,
d.description,
d.city, icon_char(&d.icon), d.temp, d.humidity, d.description,
),
};
let (tw, th) = Canvas::measure_text(&text, &self.font, self.font_size);
let pad = (self.font_size * 0.4) as u32;
let x = self.position.x.resolve(canvas.width, tw);
let y = self.position.y.resolve(canvas.height, th) + th as i32;
let pad = (self.font_size * 0.4) as u32;
let x = self.position.x.resolve(canvas.width, tw);
let y = self.position.y.resolve(canvas.height, th) + th as i32;
// card background
canvas.draw_rounded_rect(
x - pad as i32,
y - th as i32 - pad as i32,
tw + pad * 2,
th + pad * 2,
(self.font_size * 0.25) as u32,
10, 10, 10, 200,
Color::rgba(10, 10, 10, 200),
);
canvas.draw_text(x, y, &text, &self.font, self.font_size, r, g, b);
canvas.draw_text(x, y, &text, &self.font, self.font_size, self.color);
}
}
/// Map OWM icon codes to Unicode weather symbols
fn icon_char(icon: &str) -> &'static str {
match icon {
s if s.starts_with("01") => "",
s if s.starts_with("02") => "",
s if s.starts_with("03") => "",
s if s.starts_with("04") => "",
s if s.starts_with("03") | s.starts_with("04") => "",
s if s.starts_with("09") => "🌧",
s if s.starts_with("10") => "🌦",
s if s.starts_with("11") => "",