use iced::widget::canvas::{self, event, Cache, Frame, Geometry, Path, Program, Stroke};
use iced::{Color, Point, Rectangle};
use iced::mouse;
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeadzoneShape {
Circular,
Square,
}
pub struct AnalogVisualizer {
pub stick_x: f32,
pub stick_y: f32,
pub deadzone: f32,
pub deadzone_shape: DeadzoneShape,
pub range_min: i32,
pub range_max: i32,
pub cache: Arc<Cache>,
}
impl Default for AnalogVisualizer {
fn default() -> Self {
Self {
stick_x: 0.0,
stick_y: 0.0,
deadzone: 0.15,
deadzone_shape: DeadzoneShape::Circular,
range_min: -32768,
range_max: 32767,
cache: Arc::new(Cache::default()),
}
}
}
impl<Message> Program<Message> for AnalogVisualizer {
type State = ();
fn update(
&self,
_state: &mut Self::State,
_event: canvas::Event,
_bounds: Rectangle,
_cursor: mouse::Cursor,
) -> (event::Status, Option<Message>) {
(event::Status::Ignored, None)
}
fn draw(
&self,
_state: &Self::State,
renderer: &iced::Renderer,
_theme: &iced::Theme,
bounds: Rectangle,
_cursor: mouse::Cursor,
) -> Vec<Geometry> {
let center = Point::new(bounds.width / 2.0, bounds.height / 2.0);
let size = bounds.width.min(bounds.height);
let outer_radius = size * 0.45;
let background = self.cache.draw(renderer, bounds.size(), |frame| {
let outer_circle = Path::circle(center, outer_radius);
frame.fill(&outer_circle, Color::from_rgb(0.15, 0.15, 0.15));
frame.stroke(
&outer_circle,
Stroke::default()
.with_color(Color::from_rgb(0.4, 0.4, 0.4))
.with_width(2.0),
);
let deadzone_radius = (outer_radius * self.deadzone.clamp(0.0, 1.0)).max(0.0);
let deadzone_color = Color::from_rgba(0.2, 0.5, 0.2, 0.4);
if self.deadzone_shape == DeadzoneShape::Circular && deadzone_radius > 0.5 {
let deadzone_circle = Path::circle(center, deadzone_radius);
frame.fill(&deadzone_circle, deadzone_color);
frame.stroke(
&deadzone_circle,
Stroke::default()
.with_color(Color::from_rgb(0.3, 0.7, 0.3))
.with_width(1.0),
);
} else if deadzone_radius > 0.5 {
let dz_size = deadzone_radius * 2.0;
let deadzone_rect = Path::rectangle(
Point::new(center.x - deadzone_radius, center.y - deadzone_radius),
iced::Size::new(dz_size, dz_size),
);
frame.fill(&deadzone_rect, deadzone_color);
frame.stroke(
&deadzone_rect,
Stroke::default()
.with_color(Color::from_rgb(0.3, 0.7, 0.3))
.with_width(1.0),
);
}
let h_line = Path::line(
Point::new(center.x - outer_radius, center.y),
Point::new(center.x + outer_radius, center.y),
);
let v_line = Path::line(
Point::new(center.x, center.y - outer_radius),
Point::new(center.x, center.y + outer_radius),
);
frame.stroke(
&h_line,
Stroke::default()
.with_color(Color::from_rgba(0.5, 0.5, 0.5, 0.3))
.with_width(1.0),
);
frame.stroke(
&v_line,
Stroke::default()
.with_color(Color::from_rgba(0.5, 0.5, 0.5, 0.3))
.with_width(1.0),
);
let center_dot = Path::circle(center, 3.0);
frame.fill(¢er_dot, Color::from_rgb(0.6, 0.6, 0.6));
});
let mut frame = Frame::new(renderer, bounds.size());
let stick_x_clamped = self.stick_x.clamp(-1.0, 1.0);
let stick_y_clamped = self.stick_y.clamp(-1.0, 1.0);
let stick_offset_x = stick_x_clamped * outer_radius;
let stick_offset_y = -stick_y_clamped * outer_radius;
let stick_pos = Point::new(center.x + stick_offset_x, center.y + stick_offset_y);
let stick_dot = Path::circle(stick_pos, 6.0);
frame.fill(&stick_dot, Color::from_rgb(0.9, 0.3, 0.3));
frame.stroke(
&stick_dot,
Stroke::default()
.with_color(Color::from_rgb(1.0, 1.0, 1.0))
.with_width(1.0),
);
vec![background, frame.into_geometry()]
}
}
impl AnalogVisualizer {
pub fn clear_cache(&self) {
self.cache.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_analog_visualizer_default() {
let viz = AnalogVisualizer::default();
assert_eq!(viz.stick_x, 0.0);
assert_eq!(viz.stick_y, 0.0);
assert_eq!(viz.deadzone, 0.15);
assert_eq!(viz.deadzone_shape, DeadzoneShape::Circular);
}
#[test]
fn test_analog_visualizer_with_values() {
let viz = AnalogVisualizer {
stick_x: 0.5,
stick_y: -0.3,
deadzone: 0.2,
deadzone_shape: DeadzoneShape::Square,
range_min: -32768,
range_max: 32767,
cache: Arc::new(Cache::default()),
};
assert_eq!(viz.stick_x, 0.5);
assert_eq!(viz.stick_y, -0.3);
assert_eq!(viz.deadzone, 0.2);
assert_eq!(viz.deadzone_shape, DeadzoneShape::Square);
}
#[test]
fn test_deadzone_shapes() {
let circular = AnalogVisualizer {
deadzone_shape: DeadzoneShape::Circular,
..Default::default()
};
assert_eq!(circular.deadzone_shape, DeadzoneShape::Circular);
let square = AnalogVisualizer {
deadzone_shape: DeadzoneShape::Square,
..Default::default()
};
assert_eq!(square.deadzone_shape, DeadzoneShape::Square);
}
#[test]
fn test_range_values() {
let viz = AnalogVisualizer {
range_min: -16384,
range_max: 16383,
..Default::default()
};
assert_eq!(viz.range_min, -16384);
assert_eq!(viz.range_max, 16383);
}
#[test]
fn test_stick_position_clamping_bounds() {
let viz = AnalogVisualizer {
stick_x: 1.0,
stick_y: 1.0,
..Default::default()
};
assert_eq!(viz.stick_x, 1.0);
assert_eq!(viz.stick_y, 1.0);
let viz_negative = AnalogVisualizer {
stick_x: -1.0,
stick_y: -1.0,
..Default::default()
};
assert_eq!(viz_negative.stick_x, -1.0);
assert_eq!(viz_negative.stick_y, -1.0);
}
#[test]
fn test_clear_cache_exists() {
let viz = AnalogVisualizer::default();
viz.clear_cache();
}
}