mod blink;
mod cursor_vfx;
use std::{collections::HashMap, sync::Arc};
use approx::AbsDiffEq;
use itertools::Itertools;
use skia_safe::{Canvas, Paint, Path, PathBuilder, op};
use winit::event::WindowEvent;
use crate::{
bridge::EditorMode,
editor::{Cursor, CursorShape, Word},
profiling::{tracy_plot, tracy_zone},
renderer::{GridRenderer, RenderedWindow, animation_utils::*},
settings::{ParseFromValue, Settings},
units::{
GridPos, GridScale, GridSize, PixelPos, PixelRect, PixelSize, PixelVec, to_skia_point,
},
window::ShouldRender,
};
use blink::*;
const DEFAULT_CELL_PERCENTAGE: f32 = 1.0 / 8.0;
const STANDARD_CORNERS: &[(f32, f32); 4] = &[(-0.5, -0.5), (0.5, -0.5), (0.5, 0.5), (-0.5, 0.5)];
#[cfg(feature = "profiling")]
use std::ffi::{CStr, c_char};
#[cfg(feature = "profiling")]
static PLOT_NAMES_X: [&CStr; 4] = [
unsafe { CStr::from_ptr(b"Cursor top left x\0".as_ptr() as *const c_char) },
unsafe { CStr::from_ptr(b"Cursor top right x\0".as_ptr() as *const c_char) },
unsafe { CStr::from_ptr(b"Cursor bottom right x\0".as_ptr() as *const c_char) },
unsafe { CStr::from_ptr(b"Cursor bottom left x\0".as_ptr() as *const c_char) },
];
#[cfg(feature = "profiling")]
static PLOT_NAMES_Y: [&CStr; 4] = [
unsafe { CStr::from_ptr(b"Cursor top left y\0".as_ptr() as *const c_char) },
unsafe { CStr::from_ptr(b"Cursor top right y\0".as_ptr() as *const c_char) },
unsafe { CStr::from_ptr(b"Cursor bottom right y\0".as_ptr() as *const c_char) },
unsafe { CStr::from_ptr(b"Cursor bottom left y\0".as_ptr() as *const c_char) },
];
#[derive(SettingGroup)]
#[setting_prefix = "cursor"]
#[derive(Clone)]
pub struct CursorSettings {
antialiasing: bool,
animation_length: f32,
short_animation_length: f32,
animate_in_insert_mode: bool,
animate_command_line: bool,
trail_size: f32,
unfocused_outline_width: f32,
smooth_blink: bool,
cell_color_fallback: bool,
vfx_mode: cursor_vfx::VfxModeList,
vfx_opacity: f32,
vfx_particle_lifetime: f32,
vfx_particle_highlight_lifetime: f32,
vfx_particle_density: f32,
vfx_particle_speed: f32,
vfx_particle_phase: f32,
vfx_particle_curl: f32,
}
impl Default for CursorSettings {
fn default() -> Self {
CursorSettings {
antialiasing: true,
animation_length: 0.150,
short_animation_length: 0.04,
animate_in_insert_mode: true,
animate_command_line: true,
trail_size: 1.0,
unfocused_outline_width: 1.0 / 8.0,
smooth_blink: false,
cell_color_fallback: false,
vfx_mode: cursor_vfx::VfxModeList::default(),
vfx_opacity: 200.0,
vfx_particle_lifetime: 0.5,
vfx_particle_highlight_lifetime: 0.2,
vfx_particle_density: 0.7,
vfx_particle_speed: 10.0,
vfx_particle_phase: 1.5,
vfx_particle_curl: 1.0,
}
}
}
#[derive(Clone)]
pub struct Corner {
current_position: PixelPos<f32>,
relative_position: GridPos<f32>,
previous_destination: PixelPos<f32>,
animation_x: CriticallyDampedSpringAnimation,
animation_y: CriticallyDampedSpringAnimation,
animation_length: f32,
#[cfg(feature = "profiling")]
id: usize,
}
impl Corner {
pub fn new() -> Corner {
Corner {
current_position: PixelPos::default(),
relative_position: GridPos::<f32>::default(),
previous_destination: PixelPos::new(-1000.0, -1000.0),
animation_x: CriticallyDampedSpringAnimation::new(),
animation_y: CriticallyDampedSpringAnimation::new(),
animation_length: 0.0,
#[cfg(feature = "profiling")]
id: 0,
}
}
pub fn update(
&mut self,
cursor_dimensions: GridScale,
destination: PixelPos<f32>,
dt: f32,
immediate_movement: bool,
) -> bool {
let corner_destination = self.get_destination(destination, cursor_dimensions);
if corner_destination != self.previous_destination {
let delta = corner_destination - self.current_position;
self.animation_x.position = delta.x;
self.animation_y.position = delta.y;
self.previous_destination = corner_destination;
}
if immediate_movement {
self.current_position = corner_destination;
return false;
}
let mut animating = self.animation_x.update(dt, self.animation_length);
animating |= self.animation_y.update(dt, self.animation_length);
self.current_position.x = corner_destination.x - self.animation_x.position;
self.current_position.y = corner_destination.y - self.animation_y.position;
#[cfg(feature = "profiling")]
{
tracy_plot!(PLOT_NAMES_X[self.id], self.current_position.x.into());
tracy_plot!(PLOT_NAMES_Y[self.id], self.current_position.y.into());
}
animating
}
fn jump(
&mut self,
settings: &CursorSettings,
destination: PixelPos<f32>,
cursor_dimensions: GridScale,
rank: usize,
) {
let corner_destination = self.get_destination(destination, cursor_dimensions);
let jump_vec = (corner_destination - self.previous_destination) / cursor_dimensions;
self.animation_length = if jump_vec.x.abs() <= 2.001 && jump_vec.y.abs_diff_eq(&0.0, 0.001)
{
settings.animation_length.min(settings.short_animation_length)
} else {
let leading = settings.animation_length * (1.0 - settings.trail_size).clamp(0.0, 1.0);
let trailing = settings.animation_length;
match rank {
2..=3 => leading,
1 => (leading + trailing) / 2.0,
0 => trailing,
_ => panic!("Invalid rank"),
}
}
}
fn get_destination(
&self,
destination: PixelPos<f32>,
cursor_dimensions: GridScale,
) -> PixelPos<f32> {
let relative_scaled_position = self.relative_position * cursor_dimensions;
destination + relative_scaled_position.to_vector()
}
fn calculate_direction_alignment(
&self,
cursor_dimensions: GridScale,
destination: PixelPos<f32>,
) -> f32 {
let relative_scaled_position = self.relative_position * cursor_dimensions;
let corner_destination = destination + relative_scaled_position.to_vector();
let corner_direction = self.relative_position.as_vector().normalize().cast();
let travel_direction = {
let d = corner_destination - self.current_position;
d.normalize()
};
travel_direction.dot(corner_direction)
}
}
pub struct CursorRenderer {
pub corners: Vec<Corner>,
cursor: Cursor,
destination: PixelPos<f32>,
blink_status: BlinkStatus,
previous_cursor_position: Option<(u64, GridPos<u64>)>,
previous_cursor_shape: Option<CursorShape>,
previous_editor_mode: EditorMode,
cursor_vfxs: Vec<Box<dyn cursor_vfx::CursorVfx>>,
previous_vfx_mode: cursor_vfx::VfxModeList,
window_has_focus: bool,
jumped: bool,
settings: Arc<Settings>,
}
impl CursorRenderer {
pub fn new(settings: Arc<Settings>) -> CursorRenderer {
let mut renderer = CursorRenderer {
corners: vec![Corner::new(); 4],
cursor: Cursor::new(),
destination: (0.0, 0.0).into(),
blink_status: BlinkStatus::new(),
previous_cursor_position: None,
previous_cursor_shape: None,
previous_editor_mode: EditorMode::Normal,
cursor_vfxs: vec![],
previous_vfx_mode: cursor_vfx::VfxModeList::default(),
window_has_focus: true,
jumped: false,
settings,
};
renderer.set_cursor_shape(&CursorShape::Block, DEFAULT_CELL_PERCENTAGE);
renderer
}
pub fn handle_event(&mut self, event: &WindowEvent) {
if let WindowEvent::Focused(is_focused) = event {
self.window_has_focus = *is_focused;
}
}
pub fn update_cursor(&mut self, new_cursor: Cursor) {
self.cursor = new_cursor;
}
fn set_cursor_shape(&mut self, cursor_shape: &CursorShape, cell_percentage: f32) {
self.corners = self
.corners
.clone()
.into_iter()
.enumerate()
.map(|(i, corner)| {
let (x, y) = STANDARD_CORNERS[i];
Corner {
relative_position: match cursor_shape {
CursorShape::Block => (x, y).into(),
CursorShape::Vertical => ((x + 0.5) * cell_percentage - 0.5, y).into(),
CursorShape::Horizontal => {
(x, -((-y + 0.5) * cell_percentage - 0.5)).into()
}
},
#[cfg(feature = "profiling")]
id: i,
..corner
}
})
.collect::<Vec<Corner>>();
}
pub fn update_cursor_destination(
&mut self,
grid_scale: GridScale,
windows: &HashMap<u64, RenderedWindow>,
) {
let cursor_grid_position = GridPos::<u64>::from(self.cursor.grid_position);
let cursor_grid_position_f = cursor_grid_position.try_cast().unwrap();
let new_cursor_pos = if let Some(window) = windows.get(&self.cursor.parent_window_id) {
let mut grid = cursor_grid_position_f + window.grid_current_position.to_vector();
grid.y -= window.scroll_animation.position;
let top_border = window.viewport_margins.top as f32;
let bottom_border = window.viewport_margins.bottom as f32;
grid.y = grid.y.max(window.grid_current_position.y + top_border).min(
window.grid_current_position.y + window.grid_size.height as f32
- 1.0
- bottom_border,
);
self.destination = grid * grid_scale;
Some((window.id, cursor_grid_position))
} else {
self.destination = cursor_grid_position_f * grid_scale;
Some((0, cursor_grid_position))
};
if new_cursor_pos != self.previous_cursor_position {
self.previous_cursor_position = new_cursor_pos;
self.jumped = true;
for vfx in self.cursor_vfxs.iter_mut() {
vfx.cursor_jumped(self.destination);
}
}
}
pub fn prepare_frame(&mut self) -> ShouldRender {
self.blink_status.update_status(&self.cursor)
}
pub fn draw(&mut self, grid_renderer: &mut GridRenderer, canvas: &Canvas) {
tracy_zone!("cursor_draw");
let settings = self.settings.get::<CursorSettings>();
let render = self.blink_status.should_render() || settings.smooth_blink;
let opacity = match settings.smooth_blink {
true => self.blink_status.opacity(),
false => 1.0,
};
let alpha = self.cursor.alpha() as f32;
let mut paint = Paint::new(skia_safe::colors::WHITE, None);
paint.set_anti_alias(settings.antialiasing);
let character = self.cursor.grid_cell.0.clone();
if !(self.cursor.enabled && render) {
return;
}
let background_color = self
.cursor
.background(&grid_renderer.default_style.colors, settings.cell_color_fallback)
.to_color()
.with_a((opacity * alpha) as u8);
paint.set_color(background_color);
let path = if self.window_has_focus || self.cursor.shape != CursorShape::Block {
self.draw_rectangle(canvas, &paint)
} else {
let outline_width = settings.unfocused_outline_width * grid_renderer.em_size;
self.draw_rectangular_outline(canvas, &paint, outline_width)
};
let foreground_color = self
.cursor
.foreground(&grid_renderer.default_style.colors, settings.cell_color_fallback)
.to_color()
.with_a((opacity * alpha) as u8);
paint.set_color(foreground_color);
canvas.save();
canvas.clip_path(&path, None, Some(false));
let baseline_offset = grid_renderer.shaper.baseline_offset();
let style = &self.cursor.grid_cell.1;
let coarse_style = style.as_ref().map(|style| style.into()).unwrap_or_default();
let box_char_drawn = grid_renderer.box_char_renderer.draw_glyph(
&character,
canvas,
PixelRect::from_origin_and_size(
self.destination,
GridSize::new(1, 1) * grid_renderer.grid_scale,
),
foreground_color,
PixelPos::default(),
);
if !box_char_drawn {
let pos = (self.destination.x, self.destination.y + baseline_offset);
let blobs = &grid_renderer
.shaper
.shape_cached(Word::new(&character, &[character.len() as u8]), coarse_style);
for blob in blobs.iter() {
canvas.draw_text_blob(blob, pos, &paint);
}
}
canvas.restore();
for vfx in self.cursor_vfxs.iter() {
vfx.render(&settings, canvas, grid_renderer, &self.cursor);
}
}
pub fn animate(
&mut self,
current_mode: &EditorMode,
grid_renderer: &GridRenderer,
dt: f32,
) -> bool {
tracy_zone!("cursor_animate");
if !self.cursor.enabled {
return false;
}
let settings = self.settings.get::<CursorSettings>();
if settings.vfx_mode != self.previous_vfx_mode {
self.cursor_vfxs = cursor_vfx::new_cursor_vfxs(&settings.vfx_mode);
self.previous_vfx_mode = settings.vfx_mode.clone();
}
let mut cursor_width = grid_renderer.grid_scale.width();
if self.cursor.double_width && self.cursor.shape == CursorShape::Block {
cursor_width *= 2.0;
}
let cursor_dimensions = PixelSize::new(cursor_width, grid_renderer.grid_scale.height());
let in_insert_mode = matches!(current_mode, EditorMode::Insert);
let changed_to_from_cmdline = !matches!(self.previous_editor_mode, EditorMode::CmdLine)
^ matches!(current_mode, EditorMode::CmdLine);
let center_destination = self.destination + cursor_dimensions.to_vector() * 0.5;
if self.previous_cursor_shape.as_ref() != Some(&self.cursor.shape) {
self.previous_cursor_shape = Some(self.cursor.shape.clone());
self.set_cursor_shape(
&self.cursor.shape.clone(),
self.cursor.cell_percentage.unwrap_or(DEFAULT_CELL_PERCENTAGE),
);
for vfx in self.cursor_vfxs.iter_mut() {
vfx.restart(center_destination);
}
}
let mut animating = false;
if center_destination != PixelPos::ZERO {
let immediate_movement = !settings.animate_in_insert_mode && in_insert_mode
|| !settings.animate_command_line && !changed_to_from_cmdline;
if self.jumped {
let corner_ranks = self
.corners
.iter()
.map(|corner| {
corner.calculate_direction_alignment(
cursor_dimensions.into(),
center_destination,
)
})
.enumerate()
.sorted_by(|a, b| {
a.1.partial_cmp(&b.1)
.unwrap_or(std::cmp::Ordering::Equal)
.then(a.0.cmp(&b.0))
})
.enumerate()
.sorted_by_key(|(_, (id, _))| *id)
.map(|(rank, (_, _))| rank)
.collect_array::<4>()
.unwrap();
for (id, corner) in self.corners.iter_mut().enumerate() {
corner.jump(
&settings,
center_destination,
cursor_dimensions.into(),
corner_ranks[id],
)
}
}
for corner in self.corners.iter_mut() {
let corner_animating = corner.update(
cursor_dimensions.into(),
center_destination,
dt,
immediate_movement,
);
animating |= corner_animating;
}
let mut vfx_animating = false;
let vfx_base_color = self
.cursor
.background(&grid_renderer.default_style.colors, settings.cell_color_fallback)
.to_color();
for vfx in self.cursor_vfxs.iter_mut() {
let ret = vfx.update(
&settings,
vfx_base_color,
center_destination,
cursor_dimensions,
immediate_movement,
dt,
);
if !vfx_animating {
vfx_animating = ret;
}
}
animating |= vfx_animating;
}
self.jumped = false;
let blink_animating = settings.smooth_blink && self.blink_status.should_animate();
animating |= blink_animating;
if !animating {
self.previous_editor_mode = current_mode.clone();
}
tracy_plot!("cursor animating", animating as u8 as f64);
animating
}
fn draw_rectangle(&self, canvas: &Canvas, paint: &Paint) -> Path {
let mut builder = PathBuilder::new();
builder
.move_to(to_skia_point(self.corners[0].current_position.round()))
.line_to(to_skia_point(self.corners[1].current_position.round()))
.line_to(to_skia_point(self.corners[2].current_position.round()))
.line_to(to_skia_point(self.corners[3].current_position.round()))
.close();
let path = builder.detach();
canvas.draw_path(&path, paint);
path
}
fn draw_rectangular_outline(&self, canvas: &Canvas, paint: &Paint, outline_width: f32) -> Path {
let mut rectangle_builder = PathBuilder::new();
rectangle_builder
.move_to(to_skia_point(self.corners[0].current_position.round()))
.line_to(to_skia_point(self.corners[1].current_position.round()))
.line_to(to_skia_point(self.corners[2].current_position.round()))
.line_to(to_skia_point(self.corners[3].current_position.round()))
.close();
let rectangle = rectangle_builder.detach();
let offsets: [PixelVec<f32>; 4] = [
(outline_width, outline_width).into(),
(-outline_width, outline_width).into(),
(-outline_width, -outline_width).into(),
(outline_width, -outline_width).into(),
];
let mut subtract_builder = PathBuilder::new();
subtract_builder
.move_to(to_skia_point(self.corners[0].current_position + offsets[0]))
.line_to(to_skia_point(self.corners[1].current_position + offsets[1]))
.line_to(to_skia_point(self.corners[2].current_position + offsets[2]))
.line_to(to_skia_point(self.corners[3].current_position + offsets[3]))
.close();
let subtract = subtract_builder.detach();
let path = op(&rectangle, &subtract, skia_safe::PathOp::Difference).unwrap_or(rectangle);
canvas.draw_path(&path, paint);
path
}
pub fn get_destination(&self) -> PixelPos<f32> {
self.destination
}
}