use anyhow::Result;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use skia_safe::Canvas;
use crate::engine::renderer::paint_from_hex;
use crate::layout::{Constraints, LayoutNode};
use crate::schema::LayerStyle;
use crate::traits::{RenderContext, TimingConfig, Widget};
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct CursorWaypoint {
pub time: f64,
pub x: f32,
pub y: f32,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct Cursor {
#[serde(default = "default_cursor_width")]
pub width: f32,
#[serde(default = "default_cursor_height")]
pub height: f32,
#[serde(default = "default_cursor_color")]
pub color: String,
#[serde(default = "default_blink_interval")]
pub blink: f32,
#[serde(default = "default_cursor_radius")]
pub radius: f32,
#[serde(default)]
pub click_at: Vec<f64>,
#[serde(default)]
pub auto_path: Vec<CursorWaypoint>,
#[serde(default = "default_click_duration")]
pub click_duration: f32,
#[serde(default = "default_cursor_style")]
pub cursor_style: String,
#[serde(default = "default_path_easing")]
pub path_easing: String,
#[serde(flatten)]
pub timing: TimingConfig,
#[serde(default)]
pub style: LayerStyle,
}
fn default_cursor_width() -> f32 {
3.0
}
fn default_cursor_height() -> f32 {
40.0
}
fn default_cursor_color() -> String {
"#FFFFFF".to_string()
}
fn default_blink_interval() -> f32 {
0.5
}
fn default_cursor_radius() -> f32 {
1.5
}
fn default_click_duration() -> f32 {
0.3
}
fn default_cursor_style() -> String {
"default".to_string()
}
fn default_path_easing() -> String {
"ease_in_out".to_string()
}
crate::impl_traits!(Cursor {
Animatable => style,
Timed => timing,
Styled => style,
});
impl Cursor {
fn click_times(&self) -> Vec<f64> {
if !self.auto_path.is_empty() {
self.auto_path.iter().map(|w| w.time).collect()
} else {
self.click_at.clone()
}
}
fn auto_path_offset(&self, time: f64) -> (f32, f32) {
if self.auto_path.len() < 2 {
if let Some(wp) = self.auto_path.first() {
return (wp.x, wp.y);
}
return (0.0, 0.0);
}
let waypoints = &self.auto_path;
if time <= waypoints[0].time {
return (waypoints[0].x, waypoints[0].y);
}
if time >= waypoints[waypoints.len() - 1].time {
let last = &waypoints[waypoints.len() - 1];
return (last.x, last.y);
}
let mut seg_idx = 0;
for i in 0..waypoints.len() - 1 {
if time >= waypoints[i].time && time < waypoints[i + 1].time {
seg_idx = i;
break;
}
}
let wp0 = &waypoints[seg_idx];
let wp1 = &waypoints[seg_idx + 1];
let seg_duration = wp1.time - wp0.time;
if seg_duration <= 0.0 {
return (wp1.x, wp1.y);
}
let click_end = wp0.time + self.click_duration as f64;
let move_start = if seg_idx > 0 { click_end } else { wp0.time };
let move_duration = wp1.time - move_start;
if time < move_start || move_duration <= 0.0 {
return (wp0.x, wp0.y);
}
let raw_t = ((time - move_start) / move_duration).clamp(0.0, 1.0);
let t = match self.path_easing.as_str() {
"linear" => raw_t,
"ease_out" => 1.0 - (1.0 - raw_t).powi(3),
_ => { if raw_t < 0.5 {
4.0 * raw_t * raw_t * raw_t
} else {
1.0 - (-2.0 * raw_t + 2.0).powi(3) / 2.0
}
}
} as f32;
let p_prev = if seg_idx > 0 { &waypoints[seg_idx - 1] } else { wp0 };
let p_next = if seg_idx + 2 < waypoints.len() { &waypoints[seg_idx + 2] } else { wp1 };
let x = catmull_rom(t, p_prev.x, wp0.x, wp1.x, p_next.x);
let y = catmull_rom(t, p_prev.y, wp0.y, wp1.y, p_next.y);
(x, y)
}
}
fn catmull_rom(t: f32, p0: f32, p1: f32, p2: f32, p3: f32) -> f32 {
let t2 = t * t;
let t3 = t2 * t;
0.5 * ((2.0 * p1)
+ (-p0 + p2) * t
+ (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2
+ (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3)
}
impl Widget for Cursor {
fn render(
&self,
canvas: &Canvas,
_layout: &LayoutNode,
ctx: &RenderContext,
_props: &crate::engine::animator::AnimatedProperties,
) -> Result<()> {
let click_times = self.click_times();
let in_click = click_times.iter().any(|&t| {
let dt = ctx.time - t;
dt >= 0.0 && dt < self.click_duration as f64
});
if self.blink > 0.0 && !in_click {
let cycle = (ctx.time as f32 % (self.blink * 2.0)) / self.blink;
if cycle >= 1.0 {
return Ok(()); }
}
let (path_dx, path_dy) = if !self.auto_path.is_empty() {
self.auto_path_offset(ctx.time)
} else {
(0.0, 0.0)
};
let click_scale = if in_click {
let closest_click = click_times.iter()
.filter(|&&t| ctx.time >= t && ctx.time < t + self.click_duration as f64)
.copied()
.last()
.unwrap_or(0.0);
let progress = ((ctx.time - closest_click) / self.click_duration as f64) as f32;
if progress < 0.3 {
1.0 + 0.5 * (progress / 0.3)
} else {
1.5 - 0.5 * ((progress - 0.3) / 0.7)
}
} else {
1.0
};
if path_dx.abs() > 0.001 || path_dy.abs() > 0.001 {
canvas.save();
canvas.translate((path_dx, path_dy));
}
if (click_scale - 1.0).abs() > 0.001 {
let cx = self.width / 2.0;
let cy = self.height / 2.0;
canvas.save();
canvas.translate((cx, cy));
canvas.scale((click_scale, click_scale));
canvas.translate((-cx, -cy));
}
let paint = paint_from_hex(&self.color);
let rect = skia_safe::Rect::from_xywh(0.0, 0.0, self.width, self.height);
let rrect = skia_safe::RRect::new_rect_xy(rect, self.radius, self.radius);
canvas.draw_rrect(rrect, &paint);
if (click_scale - 1.0).abs() > 0.001 {
canvas.restore();
}
if path_dx.abs() > 0.001 || path_dy.abs() > 0.001 {
canvas.restore();
}
Ok(())
}
fn measure(&self, _constraints: &Constraints) -> (f32, f32) {
(self.width, self.height)
}
}