use crate::core::{PlottingError, Result};
use std::time::Duration;
#[derive(Debug, Clone, PartialEq)]
pub enum InteractionEvent {
Zoom { factor: f64, center: Point2D },
Pan { delta: Vector2D },
Reset,
Select { region: Rectangle },
SelectPoint { point: Point2D },
ClearSelection,
Brush { start: Point2D, end: Point2D },
LinkPlots { plot_ids: Vec<PlotId> },
Hover { point: Point2D },
ShowTooltip { content: String, position: Point2D },
HideTooltip,
AddAnnotation { annotation: Annotation },
ModifyStyle { element: StyleElement, style: Style },
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Point2D {
pub x: f64,
pub y: f64,
}
impl Point2D {
pub fn new(x: f64, y: f64) -> Self {
Self { x, y }
}
pub fn zero() -> Self {
Self { x: 0.0, y: 0.0 }
}
pub fn distance_to(&self, other: Point2D) -> f64 {
((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Vector2D {
pub x: f64,
pub y: f64,
}
impl Vector2D {
pub fn new(x: f64, y: f64) -> Self {
Self { x, y }
}
pub fn zero() -> Self {
Self { x: 0.0, y: 0.0 }
}
pub fn magnitude(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Rectangle {
pub min: Point2D,
pub max: Point2D,
}
impl Rectangle {
pub fn new(min_x: f64, min_y: f64, max_x: f64, max_y: f64) -> Self {
Self {
min: Point2D::new(min_x, min_y),
max: Point2D::new(max_x, max_y),
}
}
pub fn from_points(p1: Point2D, p2: Point2D) -> Self {
Self {
min: Point2D::new(p1.x.min(p2.x), p1.y.min(p2.y)),
max: Point2D::new(p1.x.max(p2.x), p1.y.max(p2.y)),
}
}
pub fn contains(&self, point: Point2D) -> bool {
point.x >= self.min.x
&& point.x <= self.max.x
&& point.y >= self.min.y
&& point.y <= self.max.y
}
pub fn width(&self) -> f64 {
self.max.x - self.min.x
}
pub fn height(&self) -> f64 {
self.max.y - self.min.y
}
pub fn center(&self) -> Point2D {
Point2D::new(
(self.min.x + self.max.x) * 0.5,
(self.min.y + self.max.y) * 0.5,
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct PlotId(pub u64);
impl Default for PlotId {
fn default() -> Self {
Self::new()
}
}
impl PlotId {
pub fn new() -> Self {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(1);
Self(COUNTER.fetch_add(1, Ordering::Relaxed))
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Annotation {
Text {
content: String,
position: Point2D,
style: TextStyle,
},
Arrow {
start: Point2D,
end: Point2D,
style: ArrowStyle,
},
Shape {
geometry: ShapeGeometry,
style: ShapeStyle,
},
Equation {
latex: String,
position: Point2D,
style: EquationStyle,
},
}
#[derive(Debug, Clone, PartialEq)]
pub struct TextStyle {
pub font_size: f32,
pub color: [u8; 4], pub font_family: String,
pub bold: bool,
pub italic: bool,
}
impl Default for TextStyle {
fn default() -> Self {
Self {
font_size: 12.0,
color: [0, 0, 0, 255], font_family: "Arial".to_string(),
bold: false,
italic: false,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ArrowStyle {
pub color: [u8; 4],
pub thickness: f32,
pub head_size: f32,
}
impl Default for ArrowStyle {
fn default() -> Self {
Self {
color: [0, 0, 0, 255], thickness: 2.0,
head_size: 8.0,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ShapeGeometry {
Circle { center: Point2D, radius: f64 },
Rectangle(Rectangle),
Polygon { points: Vec<Point2D> },
}
#[derive(Debug, Clone, PartialEq)]
pub struct ShapeStyle {
pub fill_color: Option<[u8; 4]>,
pub stroke_color: [u8; 4],
pub stroke_width: f32,
}
impl Default for ShapeStyle {
fn default() -> Self {
Self {
fill_color: None,
stroke_color: [0, 0, 0, 255], stroke_width: 1.0,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct EquationStyle {
pub font_size: f32,
pub color: [u8; 4],
}
impl Default for EquationStyle {
fn default() -> Self {
Self {
font_size: 14.0,
color: [0, 0, 0, 255], }
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum StyleElement {
Axis,
Grid,
Legend,
Title,
DataSeries(usize),
}
#[derive(Debug, Clone, PartialEq)]
pub struct Style {
pub color: Option<[u8; 4]>,
pub thickness: Option<f32>,
pub font_size: Option<f32>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum EventResponse {
Handled,
NeedsRedraw,
NeedsRecompute,
PropagateTo(Vec<PlotId>),
Error(String),
}
pub trait EventHandler {
fn handle_event(&mut self, event: InteractionEvent) -> Result<EventResponse>;
fn update(&mut self, dt: Duration) -> Result<()>;
fn needs_redraw(&self) -> bool;
fn reset(&mut self);
}
pub struct EventProcessor;
impl EventProcessor {
pub fn process_raw_event(
raw_event: RawInputEvent,
current_state: &InteractionState,
) -> Option<InteractionEvent> {
match raw_event {
RawInputEvent::MouseWheel { delta, position } => {
let factor = if delta > 0.0 { 1.2 } else { 1.0 / 1.2 };
Some(InteractionEvent::Zoom {
factor,
center: Point2D::new(position.0, position.1),
})
}
RawInputEvent::MouseMove {
position,
button_pressed,
} => {
if button_pressed {
let delta = Vector2D::new(
position.0 - current_state.last_mouse_pos.x,
position.1 - current_state.last_mouse_pos.y,
);
Some(InteractionEvent::Pan { delta })
} else {
Some(InteractionEvent::Hover {
point: Point2D::new(position.0, position.1),
})
}
}
RawInputEvent::MouseClick { position, button } => match button {
MouseButton::Left => Some(InteractionEvent::SelectPoint {
point: Point2D::new(position.0, position.1),
}),
MouseButton::Right => Some(InteractionEvent::ShowTooltip {
content: "Right-click menu".to_string(),
position: Point2D::new(position.0, position.1),
}),
_ => None,
},
RawInputEvent::KeyPress { key } => match key.as_str() {
"Escape" => Some(InteractionEvent::Reset),
"Delete" => Some(InteractionEvent::ClearSelection),
_ => None,
},
}
}
pub fn validate_event(event: &InteractionEvent) -> Result<()> {
match event {
InteractionEvent::Zoom { factor, .. } => {
if *factor <= 0.0 || factor.is_infinite() || factor.is_nan() {
return Err(PlottingError::InvalidInput(format!(
"Invalid zoom factor: {}",
factor
)));
}
}
InteractionEvent::Pan { delta } => {
if delta.x.is_infinite()
|| delta.x.is_nan()
|| delta.y.is_infinite()
|| delta.y.is_nan()
{
return Err(PlottingError::InvalidInput("Invalid pan delta".to_string()));
}
}
_ => {} }
Ok(())
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum RawInputEvent {
MouseWheel {
delta: f64,
position: (f64, f64),
},
MouseMove {
position: (f64, f64),
button_pressed: bool,
},
MouseClick {
position: (f64, f64),
button: MouseButton,
},
KeyPress {
key: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum MouseButton {
Left,
Right,
Middle,
}
use super::state::InteractionState;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_point2d_operations() {
let p1 = Point2D::new(3.0, 4.0);
let p2 = Point2D::new(0.0, 0.0);
assert_eq!(p1.distance_to(p2), 5.0); }
#[test]
fn test_rectangle_operations() {
let rect = Rectangle::new(0.0, 0.0, 10.0, 10.0);
assert!(rect.contains(Point2D::new(5.0, 5.0)));
assert!(!rect.contains(Point2D::new(15.0, 5.0)));
assert_eq!(rect.width(), 10.0);
assert_eq!(rect.height(), 10.0);
assert_eq!(rect.center(), Point2D::new(5.0, 5.0));
}
#[test]
fn test_event_validation() {
let valid_zoom = InteractionEvent::Zoom {
factor: 1.5,
center: Point2D::new(100.0, 100.0),
};
assert!(EventProcessor::validate_event(&valid_zoom).is_ok());
let invalid_zoom = InteractionEvent::Zoom {
factor: -1.0,
center: Point2D::new(100.0, 100.0),
};
assert!(EventProcessor::validate_event(&invalid_zoom).is_err());
let valid_pan = InteractionEvent::Pan {
delta: Vector2D::new(10.0, -5.0),
};
assert!(EventProcessor::validate_event(&valid_pan).is_ok());
}
#[test]
fn test_plot_id_generation() {
let id1 = PlotId::new();
let id2 = PlotId::new();
assert_ne!(id1, id2);
assert!(id1.0 < id2.0); }
#[test]
fn test_raw_event_processing() {
let state = InteractionState::default();
let mouse_wheel = RawInputEvent::MouseWheel {
delta: 1.0,
position: (50.0, 50.0),
};
let processed = EventProcessor::process_raw_event(mouse_wheel, &state);
match processed {
Some(InteractionEvent::Zoom { factor, center }) => {
assert_eq!(factor, 1.2);
assert_eq!(center, Point2D::new(50.0, 50.0));
}
_ => panic!("Expected zoom event"),
}
}
}