use std::sync::{Arc, Mutex};
use std::time::Instant;
use blinc_core::{Brush, Color, CornerRadius, DrawContext, Rect};
use crate::canvas::{canvas, Canvas, CanvasBounds};
#[derive(Clone, Copy, Debug, Default)]
pub enum CursorAnimation {
#[default]
SmoothFade,
Blink,
Solid,
}
#[derive(Clone, Debug)]
pub struct CursorState {
pub visible: bool,
pub color: Color,
pub width: f32,
pub x: f32,
pub animation: CursorAnimation,
pub blink_period_ms: u64,
pub reset_time: Instant,
}
impl Default for CursorState {
fn default() -> Self {
Self {
visible: false,
color: Color::rgba(0.4, 0.6, 1.0, 1.0),
width: 2.0,
x: 0.0,
animation: CursorAnimation::default(),
blink_period_ms: 530,
reset_time: Instant::now(),
}
}
}
impl CursorState {
pub fn new() -> Self {
Self::default()
}
pub fn with_color(mut self, color: Color) -> Self {
self.color = color;
self
}
pub fn with_width(mut self, width: f32) -> Self {
self.width = width;
self
}
pub fn with_animation(mut self, animation: CursorAnimation) -> Self {
self.animation = animation;
self
}
pub fn with_blink_period(mut self, period_ms: u64) -> Self {
self.blink_period_ms = period_ms;
self
}
pub fn reset_blink(&mut self) {
self.reset_time = Instant::now();
}
pub fn set_visible(&mut self, visible: bool) {
self.visible = visible;
if visible {
self.reset_blink();
}
}
pub fn set_x(&mut self, x: f32) {
self.x = x;
}
pub fn current_opacity(&self) -> f32 {
if !self.visible {
return 0.0;
}
let elapsed = self.reset_time.elapsed().as_millis() as f64;
let period = self.blink_period_ms as f64;
match self.animation {
CursorAnimation::Solid => 1.0,
CursorAnimation::Blink => {
let phase = (elapsed / period) as u64 % 2;
if phase == 0 {
1.0
} else {
0.0
}
}
CursorAnimation::SmoothFade => {
let t = (elapsed / period) * std::f64::consts::PI;
let sine = (t.sin() + 1.0) / 2.0; 0.3 + (sine as f32 * 0.7)
}
}
}
}
pub type SharedCursorState = Arc<Mutex<CursorState>>;
pub fn cursor_state() -> SharedCursorState {
Arc::new(Mutex::new(CursorState::new()))
}
pub fn cursor_canvas(state: &SharedCursorState, height: f32) -> Canvas {
let state = Arc::clone(state);
canvas(move |ctx: &mut dyn DrawContext, bounds: CanvasBounds| {
let s = state.lock().unwrap();
if !s.visible {
return;
}
let opacity = s.current_opacity();
if opacity < 0.01 {
return;
}
let color = Color::rgba(s.color.r, s.color.g, s.color.b, s.color.a * opacity);
ctx.fill_rect(
Rect::new(0.0, 0.0, s.width, bounds.height),
CornerRadius::default(),
Brush::Solid(color),
);
})
.w(2.0) .h(height)
}
pub fn cursor_canvas_absolute(state: &SharedCursorState, height: f32, top: f32) -> Canvas {
let state = Arc::clone(state);
canvas(move |ctx: &mut dyn DrawContext, bounds: CanvasBounds| {
let s = state.lock().unwrap();
if !s.visible {
return;
}
let opacity = s.current_opacity();
if opacity < 0.01 {
return;
}
let color = Color::rgba(s.color.r, s.color.g, s.color.b, s.color.a * opacity);
ctx.fill_rect(
Rect::new(s.x, 0.0, s.width, bounds.height),
CornerRadius::default(),
Brush::Solid(color),
);
})
.absolute()
.left(0.0)
.top(top)
.w_full()
.h(height)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cursor_state_defaults() {
let state = CursorState::new();
assert!(!state.visible);
assert_eq!(state.width, 2.0);
assert_eq!(state.blink_period_ms, 530);
}
#[test]
fn test_cursor_opacity_when_not_visible() {
let state = CursorState::new();
assert_eq!(state.current_opacity(), 0.0);
}
#[test]
fn test_cursor_opacity_solid() {
let mut state = CursorState::new();
state.visible = true;
state.animation = CursorAnimation::Solid;
assert_eq!(state.current_opacity(), 1.0);
}
#[test]
fn test_cursor_opacity_smooth_fade_range() {
let mut state = CursorState::new();
state.visible = true;
state.animation = CursorAnimation::SmoothFade;
let opacity = state.current_opacity();
assert!(opacity >= 0.3);
assert!(opacity <= 1.0);
}
}