#[cfg(not(feature = "std"))]
use crate::math::F32Ext as _;
use crate::{
geometry::Rect,
render::{PixelRead, RenderCtx},
};
use embedded_graphics_core::{
draw_target::DrawTarget,
pixelcolor::{Rgb565, RgbColor, WebColors},
};
#[derive(Debug, Clone, Copy)]
pub struct BusyWheel {
pub center_x: i32,
pub center_y: i32,
pub radius: u32,
pub dot_count: u8,
pub dot_radius: u32,
pub phase: f32,
pub color: Rgb565,
pub opacity: u8,
}
impl BusyWheel {
pub fn new(center_x: i32, center_y: i32, radius: u32) -> Self {
Self {
center_x,
center_y,
radius,
dot_count: 8,
dot_radius: 3,
phase: 0.0,
color: Rgb565::CSS_CYAN,
opacity: 255,
}
}
pub fn draw<D, C>(&self, ctx: &mut RenderCtx<D, C>) -> Result<(), D::Error>
where
D: DrawTarget<Color = Rgb565> + PixelRead,
C: crate::render::Compositor<D>,
{
if self.opacity == 0 || self.dot_count == 0 {
return Ok(());
}
let step_angle = 2.0 * core::f32::consts::PI / self.dot_count as f32;
let dr = self.dot_radius as i32;
for i in 0..self.dot_count {
let angle = self.phase + (i as f32 * step_angle);
let dx = (angle.cos() * self.radius as f32) as i32;
let dy = (angle.sin() * self.radius as f32) as i32;
let px = self.center_x + dx;
let py = self.center_y + dy;
let dot_rect = Rect::new(px - dr, py - dr, (dr * 2 + 1) as u32, (dr * 2 + 1) as u32);
ctx.fill_rounded_rect(dot_rect, dr as u8, self.color)?;
}
Ok(())
}
}
#[derive(Debug, Clone, Copy)]
pub struct GaugeWidget {
pub bounds: Rect,
pub min_val: f32,
pub max_val: f32,
pub current_val: f32,
pub needle_color: Rgb565,
pub dial_color: Rgb565,
pub arc_color: Rgb565,
}
impl GaugeWidget {
pub fn new(bounds: Rect, min_val: f32, max_val: f32) -> Self {
Self {
bounds,
min_val,
max_val,
current_val: min_val,
needle_color: Rgb565::RED,
dial_color: Rgb565::new(4, 8, 4),
arc_color: Rgb565::GREEN,
}
}
pub fn draw<D, C>(&self, ctx: &mut RenderCtx<D, C>) -> Result<(), D::Error>
where
D: DrawTarget<Color = Rgb565> + PixelRead,
C: crate::render::Compositor<D>,
{
ctx.fill_rounded_rect(self.bounds, 6, self.dial_color)?;
let center_x = self.bounds.x + (self.bounds.w as i32 / 2);
let center_y = self.bounds.y + (self.bounds.h as i32 / 2);
let radius = (self.bounds.w.min(self.bounds.h) as f32 * 0.4) as i32;
let range = (self.max_val - self.min_val).max(0.001);
let norm_val = ((self.current_val - self.min_val) / range).clamp(0.0, 1.0);
let angle_deg = -135.0 + norm_val * 270.0;
let angle_rad = angle_deg * core::f32::consts::PI / 180.0;
let nx = center_x + (angle_rad.cos() * radius as f32) as i32;
let ny = center_y + (angle_rad.sin() * radius as f32) as i32;
ctx.draw_line(center_x, center_y, nx, ny, self.needle_color)?;
let pivot_rect = Rect::new(center_x - 2, center_y - 2, 5, 5);
ctx.fill_rounded_rect(pivot_rect, 2, Rgb565::WHITE)?;
Ok(())
}
}
#[cfg(feature = "embedded-dsp")]
pub struct TouchInputFilter {
coeffs: [f32; 5],
state_x: [f32; 4],
state_y: [f32; 4],
initialized: bool,
}
#[cfg(feature = "embedded-dsp")]
impl TouchInputFilter {
pub fn new(_cutoff_freq_ratio: f32) -> Self {
let coeffs = [0.0675, 0.1349, 0.0675, 1.1430, -0.4128];
Self {
coeffs,
state_x: [0.0; 4],
state_y: [0.0; 4],
initialized: false,
}
}
pub fn filter(&mut self, raw_x: f32, raw_y: f32) -> (f32, f32) {
if !self.initialized {
self.state_x.fill(raw_x);
self.state_y.fill(raw_y);
self.initialized = true;
}
let mut inst_x = embedded_dsp::filtering::BiquadCascadeInstanceF32 {
num_stages: 1,
coeffs: &self.coeffs,
state: &mut self.state_x,
};
let mut out_x = 0.0f32;
embedded_dsp::filtering::biquad_cascade_df1_f32(
&mut inst_x,
&[raw_x],
core::slice::from_mut(&mut out_x),
);
let mut inst_y = embedded_dsp::filtering::BiquadCascadeInstanceF32 {
num_stages: 1,
coeffs: &self.coeffs,
state: &mut self.state_y,
};
let mut out_y = 0.0f32;
embedded_dsp::filtering::biquad_cascade_df1_f32(
&mut inst_y,
&[raw_y],
core::slice::from_mut(&mut out_y),
);
(out_x, out_y)
}
pub fn reset(&mut self) {
self.state_x.fill(0.0);
self.state_y.fill(0.0);
self.initialized = false;
}
}
#[cfg(feature = "embedded-dsp")]
pub struct SpectrumAnalyzerWidget<'a> {
pub bounds: Rect,
pub signal_samples: &'a [f32],
pub bar_color: Rgb565,
pub bg_color: Rgb565,
}
#[cfg(feature = "embedded-dsp")]
impl<'a> SpectrumAnalyzerWidget<'a> {
pub fn new(bounds: Rect, signal_samples: &'a [f32]) -> Self {
Self {
bounds,
signal_samples,
bar_color: Rgb565::CSS_LIME_GREEN,
bg_color: Rgb565::new(2, 4, 2),
}
}
pub fn draw<D, C>(&self, ctx: &mut RenderCtx<D, C>) -> Result<(), D::Error>
where
D: DrawTarget<Color = Rgb565> + PixelRead,
C: crate::render::Compositor<D>,
{
ctx.fill_rounded_rect(self.bounds, 4, self.bg_color)?;
if self.signal_samples.is_empty() {
return Ok(());
}
let mut rms = 0.0f32;
let _ = embedded_dsp::statistics::rms_f32(self.signal_samples, &mut rms);
let mut max_val = 0.0f32;
let mut idx = 0;
let _ = embedded_dsp::statistics::max_f32(self.signal_samples, &mut max_val, &mut idx);
let bar_count = (self.bounds.w / 6).max(1) as usize;
let chunk_size = (self.signal_samples.len() / bar_count).max(1);
for i in 0..bar_count {
let start = i * chunk_size;
let end = (start + chunk_size).min(self.signal_samples.len());
let chunk = &self.signal_samples[start..end];
let mut chunk_rms = 0.0f32;
if !chunk.is_empty() {
let _ = embedded_dsp::statistics::rms_f32(chunk, &mut chunk_rms);
}
let norm_h = (chunk_rms / (max_val.max(rms).max(0.001))).clamp(0.05, 1.0);
let bar_h = (self.bounds.h as f32 * norm_h) as u32;
let bx = self.bounds.x + (i as i32 * 6);
let by = self.bounds.bottom() - bar_h as i32;
let bar_rect = Rect::new(bx + 1, by, 4, bar_h);
ctx.fill_rect(bar_rect, self.bar_color)?;
}
Ok(())
}
}