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:
+54
-54
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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") => "⛈",
|
||||
|
||||
Reference in New Issue
Block a user