diff --git a/core/resources/click_texture.png b/core/resources/click_texture.png new file mode 100644 index 0000000..07abb62 Binary files /dev/null and b/core/resources/click_texture.png differ diff --git a/core/src/graphics/click_animation.rs b/core/src/graphics/click_animation.rs new file mode 100644 index 0000000..aa660e0 --- /dev/null +++ b/core/src/graphics/click_animation.rs @@ -0,0 +1,716 @@ +//! Click animation rendering system for overlay graphics. +//! +//! This module provides a GPU-accelerated click animation rendering system using wgpu. +//! It supports multiple click animations with individual textures, transforms, and positions. +//! The system uses a shared transform buffer with dynamic offsets for efficient +//! rendering of multiple click animations. + +use crate::utils::geometry::{Extent, Position}; +use std::{collections::VecDeque, fs::File, io::Read}; +use wgpu::util::DeviceExt; + +use super::{ + create_texture, + point::{Point, TransformMatrix}, + OverlayError, Texture, Vertex, +}; + +/// Maximum number of click animations that can be rendered simultaneously +const MAX_ANIMATIONS: usize = 30; + +/// Base horizontal offset for click animation positioning (as a fraction of screen space) +const BASE_OFFSET_X: f32 = 0.007; +/// Base vertical offset for click animation positioning (as a fraction of screen space) +const BASE_OFFSET_Y: f32 = 0.015; + +/// Represents a single click animation with its texture, geometry, and position data. +/// +/// Each click animation maintains its own vertex and index buffers for geometry, +/// a texture for appearance, and position information for rendering. +/// The animation uses dynamic offsets into shared transform and radius buffers. +#[derive(Debug)] +pub struct ClickAnimation { + /// The click animation's texture (image) + texture: Texture, + /// GPU buffer containing vertex data for the animation quad + vertex_buffer: wgpu::Buffer, + /// GPU buffer containing index data for the animation quad + index_buffer: wgpu::Buffer, + /// Dynamic offset into the shared transform buffer + transform_offset: wgpu::DynamicOffset, + /// Position and transformation data + position: Point, + /// Dynamic offset into the shared radius buffer + radius_offset: wgpu::DynamicOffset, + /// Time when the animation was enabled, None if disabled + enabled_instant: Option, +} + +impl ClickAnimation { + /// Updates the GPU transform buffer with this animation's current position. + /// + /// # Arguments + /// * `queue` - wgpu queue for uploading data to GPU + /// * `transforms_buffer` - Shared transform buffer + /// + /// This method uploads the animation's transformation matrix to the GPU + /// at the appropriate offset in the shared buffer. + pub fn update_transform_buffer(&self, queue: &wgpu::Queue, transforms_buffer: &wgpu::Buffer) { + queue.write_buffer( + transforms_buffer, + self.transform_offset as wgpu::BufferAddress, + bytemuck::cast_slice(&[self.position.get_transform_matrix()]), + ); + } + + /// Updates the GPU radius buffer for this animation's current radius value. + /// + /// # Arguments + /// * `queue` - wgpu queue for uploading data to GPU + /// * `radius_buffer` - Shared radius buffer + /// * `radius` - Current radius value for the animation + pub fn update_radius(&self, queue: &wgpu::Queue, radius_buffer: &wgpu::Buffer, radius: f32) { + queue.write_buffer( + radius_buffer, + self.radius_offset as wgpu::BufferAddress, + bytemuck::cast_slice(&[radius]), + ); + } + + /// Enables the click animation at the specified position. + /// + /// # Arguments + /// * `position` - Screen position where the animation should appear + /// * `queue` - wgpu queue for uploading data to GPU + /// * `transforms_buffer` - Shared transform buffer + /// * `radius_buffer` - Shared radius buffer + /// + /// This method initializes the animation with a starting radius and position, + /// and records the current time for animation timing. + pub fn enable( + &mut self, + position: Position, + queue: &wgpu::Queue, + transforms_buffer: &wgpu::Buffer, + radius_buffer: &wgpu::Buffer, + ) { + self.position + .set_position(position.x as f32, position.y as f32); + self.update_transform_buffer(queue, transforms_buffer); + self.update_radius(queue, radius_buffer, 0.1); + self.enabled_instant = Some(std::time::Instant::now()); + } + + /// Disables the click animation by moving it off-screen. + /// + /// # Arguments + /// * `queue` - wgpu queue for uploading data to GPU + /// * `transforms_buffer` - Shared transform buffer + /// + /// This method hides the animation by positioning it off-screen and + /// clears the enabled timestamp. + pub fn disable(&mut self, queue: &wgpu::Queue, transforms_buffer: &wgpu::Buffer) { + self.position.set_position(-100.0, -100.0); + self.update_transform_buffer(queue, transforms_buffer); + self.enabled_instant = None; + } + + /// Renders this click animation using the provided render pass. + /// + /// # Arguments + /// * `render_pass` - Active wgpu render pass for drawing + /// * `queue` - wgpu queue for uploading data to GPU + /// * `radius_buffer` - Shared radius buffer + /// * `transforms_bind_group` - Bind group for transformation matrices + /// * `radius_bind_group` - Bind group for radius values + /// + /// This method handles the animation timing, updates the radius based on elapsed time, + /// and renders the animation to the current render target. The animation automatically + /// disables itself after 1s. + pub fn draw( + &mut self, + render_pass: &mut wgpu::RenderPass, + queue: &wgpu::Queue, + radius_buffer: &wgpu::Buffer, + transforms_bind_group: &wgpu::BindGroup, + radius_bind_group: &wgpu::BindGroup, + ) { + if self.enabled_instant.is_none() { + return; + } + let enabled_instant = self.enabled_instant.unwrap(); + let radius_start = 0.1; + let elapsed = enabled_instant.elapsed().as_millis(); + let time_offset = 300; + if elapsed > time_offset { + // We want the radius to reach up to 0.1 + 0.3. We got + // 2333 by (1000 - 300) / 0.3. + let radius = radius_start + (elapsed - time_offset) as f32 / 2333.0; + self.update_radius(queue, radius_buffer, radius); + } + if elapsed > 1000 { + self.disable(queue, radius_buffer); + } + render_pass.set_bind_group(0, &self.texture.bind_group, &[]); + render_pass.set_bind_group(1, transforms_bind_group, &[self.transform_offset]); + render_pass.set_bind_group(2, radius_bind_group, &[self.radius_offset]); + render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); + render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint32); + render_pass.draw_indexed(0..6, 0, 0..1); + } +} + +/// Main click animation rendering system that manages multiple animations. +/// +/// This renderer creates and manages the GPU resources needed for click animation rendering, +/// including shaders, pipelines, and shared buffers. It uses shared transform and radius +/// buffers with dynamic offsets to efficiently handle multiple animations. +/// +/// # Design Notes +/// +/// Due to compatibility issues with development Windows VMs, this implementation +/// uses shared buffers with dynamic offsets rather than separate buffers for each animation. +/// A channel is used to safely communicate animation enable requests from other threads +/// to the render thread. +#[derive(Debug)] +pub struct ClickAnimationRenderer { + /// GPU render pipeline for click animation rendering + pub render_pipeline: wgpu::RenderPipeline, + /// Bind group layout for animation textures + pub texture_bind_group_layout: wgpu::BindGroupLayout, + /// Bind group layout for transformation matrices + pub transform_bind_group_layout: wgpu::BindGroupLayout, + /// Shared buffer containing all animation transform matrices + pub transforms_buffer: wgpu::Buffer, + /// Size of each entry in the transform buffer (including alignment) + pub transforms_buffer_entry_offset: wgpu::BufferAddress, + /// Bind group for accessing the transform buffer + pub transforms_bind_group: wgpu::BindGroup, + /// Bind group layout for animation radius values + pub radius_bind_group_layout: wgpu::BindGroupLayout, + /// Shared buffer containing all animation radius values + pub radius_buffer: wgpu::Buffer, + /// Size of each entry in the radius buffer (including alignment) + pub radius_buffer_entry_offset: wgpu::BufferAddress, + /// Bind group for accessing the radius buffer + pub radius_bind_group: wgpu::BindGroup, + /// Sender for communicating animation enable requests to the render thread + pub click_animation_position_sender: std::sync::mpsc::Sender, + /// Receiver for animation enable requests (only accessed from render thread) + pub click_animation_position_receiver: std::sync::mpsc::Receiver, + /// Array of all click animation instances + pub click_animations: Vec, + /// Queue of available (inactive) animation slots + pub available_slots: VecDeque, + /// Queue of currently used (active) animation slots + pub used_slots: VecDeque, +} + +struct ClickAnimationCreateData<'a> { + texture_path: String, + scale: f64, + device: &'a wgpu::Device, + queue: &'a wgpu::Queue, + window_size: Extent, + texture_bind_group_layout: &'a wgpu::BindGroupLayout, + transforms_buffer_entry_offset: wgpu::BufferAddress, + transforms_buffer: &'a wgpu::Buffer, + radius_buffer_entry_offset: wgpu::BufferAddress, + radius_buffer: &'a wgpu::Buffer, + animations_created: u32, +} + +impl ClickAnimationRenderer { + /// Creates a new click animation renderer with all necessary GPU resources. + /// + /// # Arguments + /// * `device` - wgpu device for creating GPU resources + /// * `queue` - wgpu queue for uploading initial data + /// * `texture_format` - Format of the render target texture + /// * `texture_path` - Path to the texture resource directory + /// * `window_size` - Size of the rendering window + /// * `scale` - Display scale factor + /// + /// # Returns + /// A fully initialized animation renderer ready to render click animations, + /// or an error if initialization fails. + /// + /// This method sets up: + /// - Bind group layouts for textures, transforms, and radius values + /// - Shared transform and radius buffers with proper alignment + /// - Render pipeline with vertex and fragment shaders + /// - Pre-allocated pool of click animation instances + /// - Channel for thread-safe animation enable requests + pub fn create( + device: &wgpu::Device, + queue: &wgpu::Queue, + texture_format: wgpu::TextureFormat, + texture_path: &str, + window_size: Extent, + scale: f64, + ) -> Result { + // Create bind group layout for click animation textures + let texture_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Shared Click Animation Texture BGL"), + entries: &[ + // Texture + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + // Sampler + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); + + /* + * Because of an issue in our dev windows vm when using a separate transform + * buffer for each animation, we are using a single transform buffer for all animations + * with dynamic offsets. + */ + + // Calculate proper buffer alignment for transform matrices + let device_limits = device.limits(); + let buffer_uniform_alignment = + device_limits.min_uniform_buffer_offset_alignment as wgpu::BufferAddress; + let transform_buffer_size = std::mem::size_of::() as wgpu::BufferAddress; + let aligned_buffer_size = (transform_buffer_size + buffer_uniform_alignment - 1) + & !(buffer_uniform_alignment - 1); + + // Create bind group layout for transformation matrices + let transform_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Transform BGL"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: std::num::NonZero::new(transform_buffer_size), + }, + count: None, + }], + }); + + // Create shared transform buffer for all animations + let transforms_buffer_size = aligned_buffer_size * MAX_ANIMATIONS as wgpu::BufferAddress; + let transforms_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Transforms Buffer"), + size: transforms_buffer_size, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + // Create bind group for the transform buffer + let transform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("Transforms Buffer Bind Group"), + layout: &transform_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &transforms_buffer, + offset: 0, + size: std::num::NonZero::new(transform_buffer_size), + }), + }], + }); + + // Create radius uniform buffer for click animation, the radius will change over time + // for the animation + let radius_buffer_size = std::mem::size_of::() as wgpu::BufferAddress; + let aligned_radius_buffer_size = + (radius_buffer_size + buffer_uniform_alignment - 1) & !(buffer_uniform_alignment - 1); + + let radius_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Radius BGL"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: std::num::NonZero::new(radius_buffer_size), + }, + count: None, + }], + }); + let radius_whole_buffer_size = + aligned_radius_buffer_size * MAX_ANIMATIONS as wgpu::BufferAddress; + let radius_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Radius Buffer"), + size: radius_whole_buffer_size, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let radius_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("Radius Buffer Bind Group"), + layout: &radius_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &radius_buffer, + offset: 0, + size: std::num::NonZero::new(radius_buffer_size), + }), + }], + }); + + // Load shader and create render pipeline + let shader = device.create_shader_module(wgpu::include_wgsl!("shader.wgsl")); + let render_pipeline_layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Render Pipeline Click Animation"), + bind_group_layouts: &[ + &texture_bind_group_layout, + &transform_bind_group_layout, + &radius_bind_group_layout, + ], + push_constant_ranges: &[], + }); + + let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Render Pipeline Click Animation"), + layout: Some(&render_pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_click_animation_main"), + buffers: &[wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &[ + wgpu::VertexAttribute { + offset: 0, + shader_location: 0, + format: wgpu::VertexFormat::Float32x2, + }, + wgpu::VertexAttribute { + offset: std::mem::size_of::<[f32; 2]>() as wgpu::BufferAddress, + shader_location: 1, + format: wgpu::VertexFormat::Float32x2, + }, + ], + }], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("fs_click_animation_main"), + targets: &[Some(wgpu::ColorTargetState { + format: texture_format, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: Some(wgpu::Face::Back), + polygon_mode: wgpu::PolygonMode::Fill, + unclipped_depth: false, + conservative: false, + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview: None, + cache: None, + }); + + // Create the click animations + let mut click_animations = Vec::new(); + let mut available_slots = VecDeque::new(); + for i in 0..MAX_ANIMATIONS { + let click_animation = Self::create_click_animation(ClickAnimationCreateData { + texture_path: texture_path.to_owned(), + scale, + device, + queue, + window_size, + texture_bind_group_layout: &texture_bind_group_layout, + transforms_buffer_entry_offset: aligned_buffer_size, + transforms_buffer: &transforms_buffer, + radius_buffer_entry_offset: aligned_radius_buffer_size, + radius_buffer: &radius_buffer, + animations_created: i as u32, + })?; + + click_animations.push(click_animation); + available_slots.push_back(i); + } + + let (sender, receiver) = std::sync::mpsc::channel(); + Ok(Self { + render_pipeline, + texture_bind_group_layout, + transform_bind_group_layout, + transforms_buffer, + transforms_buffer_entry_offset: aligned_buffer_size, + transforms_bind_group: transform_bind_group, + radius_bind_group_layout, + radius_buffer, + radius_buffer_entry_offset: aligned_radius_buffer_size, + radius_bind_group, + click_animation_position_sender: sender, + click_animation_position_receiver: receiver, + click_animations, + available_slots, + used_slots: VecDeque::new(), + }) + } + + /// Creates a new click animation instance with the specified properties. + /// + /// # Arguments + /// * `data` - Configuration data containing all necessary parameters for animation creation + /// + /// # Returns + /// A new `ClickAnimation` instance ready for rendering, or an error if creation fails. + /// + /// # Errors + /// Returns `OverlayError::TextureCreationError` if: + /// - The texture file cannot be opened or read + /// - Texture creation fails + /// + /// The animation is automatically positioned off-screen and disabled by default. + /// Its transform matrix and radius are uploaded to the GPU at the appropriate offsets. + fn create_click_animation( + data: ClickAnimationCreateData, + ) -> Result { + let resource_path = format!("{}/click_texture.png", data.texture_path); + log::debug!("create_click_animation: resource path: {resource_path:?}"); + + let mut file = match File::open(&resource_path) { + Ok(file) => file, + Err(_) => { + log::error!("create_click_animation: failed to open file: click_texture.png"); + return Err(OverlayError::TextureCreationError); + } + }; + let mut image_data = Vec::new(); + let res = file.read_to_end(&mut image_data); + if res.is_err() { + log::error!("create_click_animation: failed to read file: click_texture.png"); + return Err(OverlayError::TextureCreationError); + } + + // Create texture from image file + let texture = create_texture( + data.device, + data.queue, + &image_data, + data.texture_bind_group_layout, + )?; + + // Create vertex and index buffers for animation geometry + let (vertex_buffer, index_buffer) = Self::create_animation_vertex_buffer( + data.device, + &texture, + data.scale, + data.window_size, + ); + + // Calculate offset into shared transform buffer + let transform_offset = + (data.animations_created as wgpu::BufferAddress) * data.transforms_buffer_entry_offset; + + // Initialize animation position with base offsets + let point = Point::new( + 0.0, + 0.0, + BASE_OFFSET_X * (data.scale as f32), + BASE_OFFSET_Y * (data.scale as f32), + ); + + // Upload initial transform matrix to GPU + data.queue.write_buffer( + data.transforms_buffer, + transform_offset, + bytemuck::cast_slice(&[point.get_transform_matrix()]), + ); + + let radius_offset = + (data.animations_created as wgpu::BufferAddress) * data.radius_buffer_entry_offset; + data.queue.write_buffer( + data.radius_buffer, + radius_offset, + bytemuck::cast_slice(&[0.0f32]), + ); + + Ok(ClickAnimation { + texture, + vertex_buffer, + index_buffer, + transform_offset: transform_offset as wgpu::DynamicOffset, + position: point, + radius_offset: radius_offset as wgpu::DynamicOffset, + enabled_instant: None, + }) + } + + /// Creates vertex and index buffers for a click animation quad. + /// + /// # Arguments + /// * `device` - wgpu device for creating buffers + /// * `texture` - Animation texture containing size information + /// * `scale` - Scale factor for animation size + /// * `window_size` - Window dimensions for proper aspect ratio + /// + /// # Returns + /// A tuple containing (vertex_buffer, index_buffer) for the animation quad. + /// + /// This method creates a quad that maintains the original texture aspect ratio + /// while scaling appropriately for the target window size. The quad is positioned + /// at the top-left of normalized device coordinates and sized according to the + /// texture dimensions and scale factor. + fn create_animation_vertex_buffer( + device: &wgpu::Device, + texture: &Texture, + scale: f64, + window_size: Extent, + ) -> (wgpu::Buffer, wgpu::Buffer) { + // Calculate animation size in clip space, maintaining aspect ratio + let clip_extent = Extent { + width: (texture.extent.width / window_size.width) * scale * 1.5, + height: (texture.extent.height / window_size.height) * scale * 1.5, + }; + + // Create quad vertices with texture coordinates + let vertices = vec![ + Vertex { + position: [-1.0, 1.0], + texture_coords: [0.0, 0.0], + }, + Vertex { + position: [-1.0, 1.0 - clip_extent.height as f32], + texture_coords: [0.0, 1.0], + }, + Vertex { + position: [ + -1.0 + clip_extent.width as f32, + 1.0 - clip_extent.height as f32, + ], + texture_coords: [1.0, 1.0], + }, + Vertex { + position: [-1.0 + clip_extent.width as f32, 1.0], + texture_coords: [1.0, 0.0], + }, + ]; + + // Define triangle indices for the quad (two triangles) + let indices = vec![0, 1, 2, 0, 2, 3]; + + // Create GPU buffers + let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Vertex Buffer"), + contents: bytemuck::cast_slice(&vertices), + usage: wgpu::BufferUsages::VERTEX, + }); + let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Index Buffer"), + contents: bytemuck::cast_slice(&indices), + usage: wgpu::BufferUsages::INDEX, + }); + + (vertex_buffer, index_buffer) + } + + /// Requests to enable a click animation at the specified position. + /// + /// # Arguments + /// * `position` - Screen position where the animation should appear + /// + /// This method sends the position through a channel to the render thread, + /// where it will be processed on the next draw call. This allows animations + /// to be triggered from any thread safely. + pub fn enable_click_animation(&mut self, position: Position) { + if let Err(e) = self.click_animation_position_sender.send(position) { + log::error!("enable_click_animation: error sending position: {e:?}"); + } + } + + /// Draws all active click animations to the provided render pass. + /// + /// # Arguments + /// * `render_pass` - Active wgpu render pass for drawing + /// * `queue` - wgpu queue for uploading data to GPU + /// + /// This method: + /// 1. Processes any pending animation enable requests from the channel + /// 2. Allocates slots for new animations from the available pool + /// 3. Renders all active animations + /// 4. Reclaims slots from completed animations back to the available pool + /// + /// The method automatically manages the lifecycle of animations, returning + /// them to the available pool once they complete. + pub fn draw(&mut self, render_pass: &mut wgpu::RenderPass, queue: &wgpu::Queue) { + // Drain click animation enable requests. + while let Ok(position) = self.click_animation_position_receiver.try_recv() { + if self.available_slots.is_empty() { + log::warn!("enable_click_animation: available_slots is empty"); + break; + } + + let slot = self.available_slots.pop_front().unwrap(); + self.used_slots.push_back(slot); + + self.click_animations[slot].enable( + position, + queue, + &self.transforms_buffer, + &self.radius_buffer, + ); + } + + if self.used_slots.is_empty() { + return; + } + + render_pass.set_pipeline(&self.render_pipeline); + + for slot in self.used_slots.iter() { + self.click_animations[*slot].draw( + render_pass, + queue, + &self.radius_buffer, + &self.transforms_bind_group, + &self.radius_bind_group, + ); + } + + loop { + let front = self.used_slots.front(); + if front.is_none() { + break; + } + + let slot = *front.unwrap(); + if self.click_animations[slot].enabled_instant.is_none() { + let front = self.used_slots.pop_front().unwrap(); + self.available_slots.push_back(front); + } else { + break; + } + } + } +} diff --git a/core/src/graphics/cursor.rs b/core/src/graphics/cursor.rs index b03e9fc..af4633c 100644 --- a/core/src/graphics/cursor.rs +++ b/core/src/graphics/cursor.rs @@ -8,7 +8,11 @@ use crate::utils::geometry::Extent; use wgpu::util::DeviceExt; -use super::{create_texture, GraphicsContext, OverlayError, Texture, Vertex}; +use super::{ + create_texture, + point::{Point, TransformMatrix}, + GraphicsContext, OverlayError, Texture, Vertex, +}; /// Maximum number of cursors that can be rendered simultaneously const MAX_CURSORS: u32 = 100; @@ -17,118 +21,6 @@ const BASE_OFFSET_X: f32 = 0.001; /// Base vertical offset for cursor positioning (as a fraction of screen space) const BASE_OFFSET_Y: f32 = 0.002; -/// A 4x4 transformation matrix for GPU vertex transformations. -/// -/// This matrix is used to transform cursor vertices in the shader, -/// primarily for positioning cursors at specific screen coordinates. -#[repr(C)] -#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -pub struct TransformMatrix { - pub matrix: [[f32; 4]; 4], -} - -/// Uniform buffer data structure containing a transformation matrix. -/// -/// This struct is uploaded to the GPU as a uniform buffer to provide -/// transformation data to the vertex shader for cursor positioning. -#[repr(C)] -#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -pub struct TranslationUniform { - transform: TransformMatrix, -} - -impl TranslationUniform { - /// Creates a new translation uniform with an identity transformation matrix. - /// - /// The identity matrix means no transformation is applied initially. - fn new() -> Self { - Self { - transform: TransformMatrix { - matrix: [ - [1.0, 0.0, 0.0, 0.0], - [0.0, 1.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0], - [0.0, 0.0, 0.0, 1.0], - ], - }, - } - } - - /// Sets the translation component of the transformation matrix. - /// - /// # Arguments - /// * `x` - Horizontal translation in normalized device coordinates (-1.0 to 1.0) - /// * `y` - Vertical translation in normalized device coordinates (-1.0 to 1.0) - /// - /// # Note - /// The coordinates are multiplied by 2.0 because the input is expected to be - /// in the range 0.0-1.0, but NDC space ranges from -1.0 to 1.0. - /// Y is negated to match screen coordinate conventions. - fn set_translation(&mut self, x: f32, y: f32) { - // We need to multiply by 2.0 because the cursor position is in the range of -1.0 to 1.0 - self.transform.matrix[3][0] = x * 2.0; - self.transform.matrix[3][1] = -y * 2.0; - } -} - -/// Represents a point in 2D space with position and offset information. -/// -/// This struct manages cursor positioning with both absolute coordinates -/// and rendering offsets. The transform matrix is automatically updated -/// when the position changes. -#[derive(Debug)] -struct Point { - /// Absolute X coordinate - x: f32, - /// Absolute Y coordinate - y: f32, - /// Horizontal rendering offset - offset_x: f32, - /// Vertical rendering offset - offset_y: f32, - /// GPU transformation matrix for this point - transform_matrix: TranslationUniform, -} - -impl Point { - /// Creates a new point with the specified position and offsets. - /// - /// # Arguments - /// * `x` - Initial X coordinate - /// * `y` - Initial Y coordinate - /// * `offset_x` - Horizontal rendering offset - /// * `offset_y` - Vertical rendering offset - fn new(x: f32, y: f32, offset_x: f32, offset_y: f32) -> Self { - Self { - x, - y, - offset_x, - offset_y, - transform_matrix: TranslationUniform::new(), - } - } - - /// Returns the current transformation matrix for GPU upload. - fn get_transform_matrix(&self) -> TransformMatrix { - self.transform_matrix.transform - } - - /// Updates the point's position and recalculates the transformation matrix. - /// - /// # Arguments - /// * `x` - New X coordinate - /// * `y` - New Y coordinate - /// - /// The transformation matrix is updated to position the cursor at the - /// specified coordinates, accounting for the configured offsets. - fn set_position(&mut self, x: f32, y: f32) { - self.x = x; - self.y = y; - self.transform_matrix - .set_translation(x - self.offset_x, y - self.offset_y); - } -} - /// Represents a single cursor with its texture, geometry, and position data. /// /// Each cursor maintains its own vertex and index buffers for geometry, diff --git a/core/src/graphics/graphics_context.rs b/core/src/graphics/graphics_context.rs index 2a09da9..b37f706 100644 --- a/core/src/graphics/graphics_context.rs +++ b/core/src/graphics/graphics_context.rs @@ -4,8 +4,8 @@ //! such as cursors and markers on top of shared screen content. It uses wgpu for //! hardware-accelerated rendering with proper alpha blending and transparent window support. -use crate::input::mouse::CursorController; use crate::utils::geometry::Extent; +use crate::{input::mouse::CursorController, utils::geometry::Position}; use image::GenericImageView; use log::error; use std::sync::Arc; @@ -23,6 +23,13 @@ use marker::MarkerRenderer; pub mod cursor; use cursor::{Cursor, CursorsRenderer}; +#[path = "click_animation.rs"] +pub mod click_animation; +use click_animation::ClickAnimationRenderer; + +#[path = "point.rs"] +pub mod point; + /// Errors that can occur during overlay graphics operations. #[derive(Error, Debug)] pub enum OverlayError { @@ -128,6 +135,9 @@ pub struct GraphicsContext<'a> { /// Renderer for corner markers indicating overlay boundaries marker_renderer: MarkerRenderer, + + /// Renderer for click animations + click_animation_renderer: ClickAnimationRenderer, } impl<'a> GraphicsContext<'a> { @@ -275,6 +285,18 @@ impl<'a> GraphicsContext<'a> { scale, )?; + let click_animation_renderer = ClickAnimationRenderer::create( + &device, + &queue, + surface_config.format, + &texture_path, + Extent { + width: size.width as f64, + height: size.height as f64, + }, + scale, + )?; + Ok(Self { surface, device, @@ -284,6 +306,7 @@ impl<'a> GraphicsContext<'a> { #[cfg(target_os = "windows")] _direct_composition: direct_composition, marker_renderer, + click_animation_renderer, }) } @@ -345,7 +368,7 @@ impl<'a> GraphicsContext<'a> { /// If frame acquisition fails (e.g., surface lost), the method logs the error /// and returns early without crashing. This provides resilience against /// temporary graphics driver issues or window state changes. - pub fn draw(&self, cursor_controller: &CursorController) { + pub fn draw(&mut self, cursor_controller: &CursorController) { let output = match self.surface.get_current_texture() { Ok(output) => output, Err(e) => { @@ -385,6 +408,8 @@ impl<'a> GraphicsContext<'a> { cursor_controller.draw(&mut render_pass, self); self.marker_renderer.draw(&mut render_pass); + self.click_animation_renderer + .draw(&mut render_pass, &self.queue); drop(render_pass); @@ -403,6 +428,16 @@ impl<'a> GraphicsContext<'a> { pub fn window(&self) -> &Window { &self.window } + + /// Requests to enable a click animation at the specified position. + /// + /// # Arguments + /// * `position` - Screen position where the animation should appear + pub fn enable_click_animation(&mut self, position: Position) { + log::debug!("GraphicsContext::enable_click_animation: {position:?}"); + self.click_animation_renderer + .enable_click_animation(position); + } } /// Creates a GPU texture from an image file for overlay rendering. diff --git a/core/src/graphics/point.rs b/core/src/graphics/point.rs new file mode 100644 index 0000000..8cc45e2 --- /dev/null +++ b/core/src/graphics/point.rs @@ -0,0 +1,111 @@ +/// A 4x4 transformation matrix for GPU vertex transformations. +/// +/// This matrix is used to transform cursor vertices in the shader, +/// primarily for positioning cursors at specific screen coordinates. +#[repr(C)] +#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub struct TransformMatrix { + pub matrix: [[f32; 4]; 4], +} + +/// Uniform buffer data structure containing a transformation matrix. +/// +/// This struct is uploaded to the GPU as a uniform buffer to provide +/// transformation data to the vertex shader for cursor positioning. +#[repr(C)] +#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub struct TranslationUniform { + transform: TransformMatrix, +} + +impl TranslationUniform { + /// Creates a new translation uniform with an identity transformation matrix. + /// + /// The identity matrix means no transformation is applied initially. + fn new() -> Self { + Self { + transform: TransformMatrix { + matrix: [ + [1.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0], + ], + }, + } + } + + /// Sets the translation component of the transformation matrix. + /// + /// # Arguments + /// * `x` - Horizontal translation in normalized device coordinates (-1.0 to 1.0) + /// * `y` - Vertical translation in normalized device coordinates (-1.0 to 1.0) + /// + /// # Note + /// The coordinates are multiplied by 2.0 because the input is expected to be + /// in the range 0.0-1.0, but NDC space ranges from -1.0 to 1.0. + /// Y is negated to match screen coordinate conventions. + fn set_translation(&mut self, x: f32, y: f32) { + // We need to multiply by 2.0 because the cursor position is in the range of -1.0 to 1.0 + self.transform.matrix[3][0] = x * 2.0; + self.transform.matrix[3][1] = -y * 2.0; + } +} + +/// Represents a point in 2D space with position and offset information. +/// +/// This struct manages cursor positioning with both absolute coordinates +/// and rendering offsets. The transform matrix is automatically updated +/// when the position changes. +#[derive(Debug)] +pub struct Point { + /// Absolute X coordinate + x: f32, + /// Absolute Y coordinate + y: f32, + /// Horizontal rendering offset + offset_x: f32, + /// Vertical rendering offset + offset_y: f32, + /// GPU transformation matrix for this point + transform_matrix: TranslationUniform, +} + +impl Point { + /// Creates a new point with the specified position and offsets. + /// + /// # Arguments + /// * `x` - Initial X coordinate + /// * `y` - Initial Y coordinate + /// * `offset_x` - Horizontal rendering offset + /// * `offset_y` - Vertical rendering offset + pub fn new(x: f32, y: f32, offset_x: f32, offset_y: f32) -> Self { + Self { + x, + y, + offset_x, + offset_y, + transform_matrix: TranslationUniform::new(), + } + } + + /// Returns the current transformation matrix for GPU upload. + pub fn get_transform_matrix(&self) -> TransformMatrix { + self.transform_matrix.transform + } + + /// Updates the point's position and recalculates the transformation matrix. + /// + /// # Arguments + /// * `x` - New X coordinate + /// * `y` - New Y coordinate + /// + /// The transformation matrix is updated to position the cursor at the + /// specified coordinates, accounting for the configured offsets. + pub fn set_position(&mut self, x: f32, y: f32) { + self.x = x; + self.y = y; + self.transform_matrix + .set_translation(x - self.offset_x, y - self.offset_y); + } +} diff --git a/core/src/graphics/shader.wgsl b/core/src/graphics/shader.wgsl index 8794e1d..faccbb6 100644 --- a/core/src/graphics/shader.wgsl +++ b/core/src/graphics/shader.wgsl @@ -44,4 +44,37 @@ fn vs_lines_main( out.texture_coords = model.texture_coords; out.clip_position = vec4(model.position, 0.0, 1.0); return out; +} + +@vertex +fn vs_click_animation_main( + model: VertexInput, +) -> VertexOutput { + var out: VertexOutput; + out.texture_coords = model.texture_coords; + out.clip_position = coords.transform * vec4(model.position, 0.0, 1.0); + return out; +} + +@group(2) @binding(0) +var radius: f32; + +@fragment +fn fs_click_animation_main(in: VertexOutput) -> @location(0) vec4 { + let color = textureSample(t_diffuse, s_diffuse, in.texture_coords); + let centered_coords = in.texture_coords - vec2(0.5, 0.5); + let dist = length(centered_coords); + + let radius_start = 0.1; + if radius == radius_start { + let alpha = 1.0 - smoothstep(radius, radius + 0.1, dist); + return vec4(color.rgb, color.a * alpha); + } else { + let ring_width = 0.01; + let edge = 0.02; + let outer = smoothstep(radius - ring_width - edge, radius - ring_width + edge, dist); + let inner = 1.0 - smoothstep(radius + ring_width - edge, radius + ring_width + edge, dist); + let alpha = inner * outer; + return vec4(color.rgb, color.a * alpha); + } } \ No newline at end of file diff --git a/core/src/input/mouse.rs b/core/src/input/mouse.rs index d33d3a0..91153f9 100644 --- a/core/src/input/mouse.rs +++ b/core/src/input/mouse.rs @@ -322,6 +322,7 @@ struct ControllerCursor { */ clicked: bool, enabled: bool, + pointer_enabled: bool, has_control: bool, visible_name: String, sid: String, @@ -340,6 +341,7 @@ impl ControllerCursor { pointer_cursor, clicked: false, enabled, + pointer_enabled: false, has_control: false, visible_name, sid, @@ -352,23 +354,23 @@ impl ControllerCursor { global_position, local_position, self.has_control, - self.enabled + self.enabled_control_cursor() ); self.control_cursor.set_position( global_position, local_position, - !self.has_control && self.enabled, + !self.has_control && self.enabled_control_cursor(), ); self.pointer_cursor.set_position( global_position, local_position, - !self.has_control && !self.enabled, + !self.has_control && !self.enabled_control_cursor(), ); } fn show(&mut self) { self.has_control = false; - if self.enabled { + if self.enabled_control_cursor() { self.control_cursor.show(); } else { self.pointer_cursor.show(); @@ -376,7 +378,7 @@ impl ControllerCursor { } fn hide(&mut self) { - if self.enabled { + if self.enabled_control_cursor() { self.has_control = true; self.control_cursor.hide(); } else { @@ -384,6 +386,10 @@ impl ControllerCursor { } } + fn enabled_control_cursor(&self) -> bool { + self.enabled && !self.pointer_enabled + } + fn enabled(&self) -> bool { self.enabled } @@ -391,7 +397,7 @@ impl ControllerCursor { fn set_enabled(&mut self, enabled: bool) { self.enabled = enabled; - if enabled { + if enabled && !self.pointer_enabled { self.control_cursor.show(); self.pointer_cursor.hide(); } else { @@ -417,7 +423,7 @@ impl ControllerCursor { return; } - if self.enabled { + if self.enabled && !self.pointer_enabled { self.control_cursor.draw(render_pass, gfx); } else { self.pointer_cursor.draw(render_pass, gfx); @@ -427,6 +433,22 @@ impl ControllerCursor { fn has_control(&self) -> bool { self.has_control } + + fn set_pointer_enabled(&mut self, pointer_enabled: bool) { + self.pointer_enabled = pointer_enabled; + + if pointer_enabled { + self.pointer_cursor.show(); + self.control_cursor.hide(); + } else if self.enabled { + self.pointer_cursor.hide(); + self.control_cursor.show(); + } + } + + fn pointer_enabled(&self) -> bool { + self.pointer_enabled + } } pub struct SharerCursor { @@ -488,8 +510,7 @@ impl SharerCursor { return; } - self.has_control = true; - self.cursor.hide(); + self.hide(); let mut controllers_cursors = self.controllers_cursors.lock().unwrap(); for controller in controllers_cursors.iter_mut() { if controller.has_control() { @@ -504,7 +525,6 @@ impl SharerCursor { */ let mut cursor_simulator = self.cursor_simulator.lock().unwrap(); let global_position = self.global_position(); - cursor_simulator.simulate_cursor_movement(global_position, false); cursor_simulator.simulate_click(MouseClickData { x: global_position.x as f32, y: global_position.y as f32, @@ -516,13 +536,6 @@ impl SharerCursor { ctrl: false, meta: false, }); - - let res = self - .event_loop_proxy - .send_event(UserEvent::ParticipantInControl("sharer".to_string())); - if let Err(e) = res { - error!("sharer_cursor: click: error sending participant in control: {e:?}"); - } } fn scroll(&mut self) { @@ -532,32 +545,13 @@ impl SharerCursor { return; } - { - /* - * This is the same as the click, we need to move the system cursor to the - * position of the scroll, because the system cursor was were the controlling - * controller was. - */ - let mut cursor_simulator = self.cursor_simulator.lock().unwrap(); - let global_position = self.global_position(); - cursor_simulator.simulate_cursor_movement(global_position, false); - } - - self.has_control = true; - self.cursor.hide(); + self.hide(); let mut controllers_cursors = self.controllers_cursors.lock().unwrap(); for controller in controllers_cursors.iter_mut() { if controller.has_control() { controller.show(); } } - - let res = self - .event_loop_proxy - .send_event(UserEvent::ParticipantInControl("sharer".to_string())); - if let Err(e) = res { - error!("sharer_cursor: scroll: error sending participant in control: {e:?}"); - } } fn has_control(&self) -> bool { @@ -579,6 +573,22 @@ impl SharerCursor { self.cursor.show(); } + fn hide(&mut self) { + self.has_control = true; + self.cursor.hide(); + + let mut cursor_simulator = self.cursor_simulator.lock().unwrap(); + let global_position = self.global_position(); + cursor_simulator.simulate_cursor_movement(global_position, false); + + let res = self + .event_loop_proxy + .send_event(UserEvent::ParticipantInControl("sharer".to_string())); + if let Err(e) = res { + error!("sharer_cursor: click: error sending participant in control: {e:?}"); + } + } + #[allow(dead_code)] fn set_last_event_position(&mut self, position: Position) { self.last_event_position = position; @@ -603,7 +613,7 @@ fn redraw_thread( receiver: Receiver, ) { loop { - match receiver.recv_timeout(std::time::Duration::from_millis(16)) { + match receiver.recv_timeout(std::time::Duration::from_millis(12)) { Ok(command) => match command { RedrawThreadCommands::Stop => break, }, @@ -921,8 +931,21 @@ impl CursorController { continue; } - if !controller.enabled() { + if !controller.enabled() || controller.pointer_enabled() { log::info!("mouse_click_controller: controller is disabled."); + if click_data.down && controller.pointer_enabled() { + if let Err(e) = + self.event_loop_proxy + .send_event(UserEvent::EnableClickAnimation(Position { + x: click_data.x as f64, + y: click_data.y as f64, + })) + { + error!( + "mouse_click_controller: error sending enable click animation: {e:?}" + ); + } + } break; } @@ -1014,7 +1037,7 @@ impl CursorController { continue; } - if !controller.enabled() { + if !controller.enabled() || controller.pointer_enabled() { log::info!("scroll_controller: controller is disabled."); break; } @@ -1089,6 +1112,18 @@ impl CursorController { self.controllers_cursors_enabled = enabled; for controller in controllers_cursors.iter_mut() { controller.set_enabled(enabled); + + if controller.has_control() { + controller.show(); + let mut sharer_cursor = self + .remote_control + .as_ref() + .unwrap() + .sharer_cursor + .lock() + .unwrap(); + sharer_cursor.hide(); + } } } @@ -1098,17 +1133,10 @@ impl CursorController { /// /// # Parameters /// - /// * `visible` - Whether to show full cursor (true) or minimal pointer (false) + /// * `enabled` - Whether to show full cursor (true) or minimal pointer (false) /// * `sid` - Session ID identifying which controller to modify - pub fn set_controller_visible(&mut self, visible: bool, sid: &str) { - log::info!("set_controller_visible: {visible} {sid}"); - - if !self.controllers_cursors_enabled { - log::info!( - "set_controller_visible: sharer has disabled controllers' cursors this is a noop." - ); - return; - } + pub fn set_controller_pointer_enabled(&mut self, enabled: bool, sid: &str) { + log::info!("set_controller_pointer_enabled: {enabled} {sid}"); let mut controllers_cursors = self.controllers_cursors.lock().unwrap(); for controller in controllers_cursors.iter_mut() { @@ -1116,7 +1144,20 @@ impl CursorController { continue; } - controller.set_enabled(visible); + if controller.has_control() { + log::info!("set_controller_pointer_enabled: controller {sid} has control, give control back to sharer."); + controller.show(); + let mut sharer_cursor = self + .remote_control + .as_ref() + .unwrap() + .sharer_cursor + .lock() + .unwrap(); + sharer_cursor.hide(); + } + + controller.set_pointer_enabled(enabled); break; } } diff --git a/core/src/lib.rs b/core/src/lib.rs index d37f37d..d8a1b1d 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -55,6 +55,7 @@ use winit::platform::windows::WindowExtWindows; use winit::window::{WindowAttributes, WindowLevel}; use crate::overlay_window::DisplayInfo; +use crate::utils::geometry::Position; // Constants for magic numbers /// Initial size for the overlay window (width and height in logical pixels) @@ -642,7 +643,7 @@ impl<'a> ApplicationHandler for Application<'a> { } let remote_control = &mut self.remote_control.as_mut().unwrap(); let cursor_controller = &mut remote_control.cursor_controller; - cursor_controller.set_controller_visible(visible, sid.as_str()); + cursor_controller.set_controller_pointer_enabled(visible, sid.as_str()); } UserEvent::Keystroke(keystroke_data) => { log::debug!("user_event: keystroke: {keystroke_data:?}"); @@ -808,6 +809,15 @@ impl<'a> ApplicationHandler for Application<'a> { sentry_metadata.app_version, ); } + UserEvent::EnableClickAnimation(position) => { + log::debug!("user_event: Enable click animation: {position:?}"); + if self.remote_control.is_none() { + log::warn!("user_event: remote control is none enable click animation"); + return; + } + let gfx = &mut self.remote_control.as_mut().unwrap().gfx; + gfx.enable_click_animation(position); + } } } @@ -884,6 +894,7 @@ pub enum UserEvent { ScreenShare(ScreenShareMessage), StopScreenShare, RequestRedraw, + EnableClickAnimation(Position), SharerPosition(f64, f64), ResetState, Tick(u128), diff --git a/core/tests/src/main.rs b/core/tests/src/main.rs index e618e9e..f4e5e0b 100644 --- a/core/tests/src/main.rs +++ b/core/tests/src/main.rs @@ -56,6 +56,16 @@ enum CursorTest { WindowEdges, /// Test concurrent scrolling ConcurrentScrolling, + /// Click animation + ClickAnimation, + /// Test transitions: Remote enabled with animation toggling + TransitionsRemoteEnabledAnimation, + /// Test transitions: Remote enabled then disabled + TransitionsRemoteEnabledThenDisabled, + /// Test transitions: Remote disabled with animation + TransitionsRemoteDisabledAnimation, + /// Test transitions: Mixed remote control and animation + TransitionsMixed, } #[tokio::main] @@ -119,6 +129,26 @@ async fn main() -> io::Result<()> { println!("Running concurrent scrolling test..."); remote_cursor::test_concurrent_scrolling().await?; } + CursorTest::ClickAnimation => { + println!("Running click animation test..."); + remote_cursor::test_click_animation().await?; + } + CursorTest::TransitionsRemoteEnabledAnimation => { + println!("Running transitions test: Remote enabled with animation..."); + remote_cursor::test_transitions_remote_enabled_with_animation().await?; + } + CursorTest::TransitionsRemoteEnabledThenDisabled => { + println!("Running transitions test: Remote enabled then disabled..."); + remote_cursor::test_transitions_remote_enabled_then_disabled().await?; + } + CursorTest::TransitionsRemoteDisabledAnimation => { + println!("Running transitions test: Remote disabled with animation..."); + remote_cursor::test_transitions_remote_disabled_with_animation().await?; + } + CursorTest::TransitionsMixed => { + println!("Running transitions test: Mixed remote and animation..."); + remote_cursor::test_transitions_mixed_remote_and_animation().await?; + } } println!("Cursor test finished."); } diff --git a/core/tests/src/remote_cursor.rs b/core/tests/src/remote_cursor.rs index ede0cbc..e3f2430 100644 --- a/core/tests/src/remote_cursor.rs +++ b/core/tests/src/remote_cursor.rs @@ -1,8 +1,9 @@ -use crate::events::{ClientEvent, ClientPoint, MouseClickData, WheelDelta}; +use crate::events::{ClientEvent, ClientPoint, MouseClickData, MouseVisibleData, WheelDelta}; use crate::livekit_utils; use crate::screenshare_client; use livekit::prelude::*; use rand::{rngs::StdRng, Rng, SeedableRng}; +use socket_lib::Message; use std::{io, time::Duration}; use tokio::time::sleep; @@ -60,6 +61,45 @@ async fn send_mouse_click(room: &Room, x: f64, y: f64, button: u32) -> io::Resul Ok(()) } +/// Sends a ClickAnimation event via the LiveKit data channel. +async fn send_click_animation(room: &Room, enabled: bool) -> io::Result<()> { + let click_animation_data = MouseVisibleData { visible: enabled }; + let event = ClientEvent::MouseVisible(click_animation_data); + let payload = serde_json::to_vec(&event).map_err(io::Error::other)?; + room.local_participant() + .publish_data(DataPacket { + payload, + reliable: true, + ..Default::default() + }) + .await + .map_err(io::Error::other)?; + Ok(()) +} + +/// Smoothly moves the cursor from one position to another +async fn smooth_cursor_move( + room: &Room, + from_x: f64, + from_y: f64, + to_x: f64, + to_y: f64, +) -> io::Result<()> { + let steps = 30; // Number of steps for smooth movement + let delay = Duration::from_millis(16); // ~60fps + + for i in 0..=steps { + let t = i as f64 / steps as f64; + let x = from_x + (to_x - from_x) * t; + let y = from_y + (to_y - from_y) * t; + + send_mouse_move(room, x, y).await?; + sleep(delay).await; + } + + Ok(()) +} + /// Sends a mouse move event via the LiveKit data channel. async fn send_mouse_move(room: &Room, x: f64, y: f64) -> io::Result<()> { let point = ClientPoint { @@ -1210,3 +1250,492 @@ pub async fn test_concurrent_scrolling() -> io::Result<()> { Ok(()) } + +/// Test click animation with various remote control states +pub async fn test_click_animation() -> io::Result<()> { + // Start single screenshare session + println!("Starting screenshare session..."); + let (mut cursor_socket, _) = screenshare_client::start_screenshare_session()?; + + let url = std::env::var("LIVEKIT_URL").expect("LIVEKIT_URL environment variable not set"); + + // Create single participant connection + let token = livekit_utils::generate_token("TestUser"); + let (room, _rx) = Room::connect(&url, &token, RoomOptions::default()) + .await + .unwrap(); + + println!("Participant connected."); + + // We need to wait for the textures to load + tokio::time::sleep(std::time::Duration::from_secs(7)).await; + + // Starting position - middle of screen + let start_x = 0.5; + let start_y = 0.5; + let move_distance = 0.2; + + // Track current cursor position - start slightly to the left + let mut current_x = start_x - 0.1; + let mut current_y = start_y; + + println!("\n=== Phase 1: Click Animation Enabled with Remote Control ==="); + // Move to starting position before first click + println!("Moving to starting position ({start_x}, {current_y})"); + smooth_cursor_move(&room, current_x, current_y, start_x, current_y).await?; + current_x = start_x; + sleep(Duration::from_millis(500)).await; + + // Enable click animation + println!("Enabling click animation"); + send_click_animation(&room, true).await?; + sleep(Duration::from_millis(200)).await; + + // Perform a click + println!("Clicking at ({current_x}, {current_y})"); + send_mouse_click(&room, current_x, current_y, 0).await?; + sleep(Duration::from_millis(500)).await; + + // Move to the right + let new_x = current_x + move_distance; + println!("Moving to the right to ({new_x}, {current_y})"); + smooth_cursor_move(&room, current_x, current_y, new_x, current_y).await?; + current_x = new_x; + sleep(Duration::from_millis(500)).await; + + // Perform a click + println!("Clicking at ({current_x}, {current_y})"); + send_mouse_click(&room, current_x, current_y, 0).await?; + sleep(Duration::from_millis(500)).await; + + // Disable click animation + println!("Disabling click animation"); + send_click_animation(&room, false).await?; + sleep(Duration::from_millis(200)).await; + + println!("\n=== Phase 2: Remote Control Disabled ==="); + // Disable remote control + println!("Disabling remote control"); + cursor_socket.send_message(Message::ControllerCursorEnabled(false))?; + sleep(Duration::from_millis(500)).await; + + // Move down + let new_y = current_y + move_distance; + println!("Moving down to ({current_x}, {new_y})"); + smooth_cursor_move(&room, current_x, current_y, current_x, new_y).await?; + current_y = new_y; + sleep(Duration::from_millis(500)).await; + + // Enable click animation + println!("Enabling click animation"); + send_click_animation(&room, true).await?; + sleep(Duration::from_millis(200)).await; + + // Click - move right - click + println!("Clicking at ({current_x}, {current_y})"); + send_mouse_click(&room, current_x, current_y, 0).await?; + sleep(Duration::from_millis(500)).await; + + let new_x2 = current_x + move_distance; + println!("Moving right to ({new_x2}, {current_y})"); + smooth_cursor_move(&room, current_x, current_y, new_x2, current_y).await?; + current_x = new_x2; + sleep(Duration::from_millis(500)).await; + + println!("Clicking at ({current_x}, {current_y})"); + send_mouse_click(&room, current_x, current_y, 0).await?; + sleep(Duration::from_millis(500)).await; + + println!("\n=== Phase 3: Remote Control Re-enabled ==="); + // Enable remote control + println!("Enabling remote control"); + cursor_socket.send_message(Message::ControllerCursorEnabled(true))?; + sleep(Duration::from_millis(500)).await; + + // Disable click animation + println!("Disabling click animation"); + send_click_animation(&room, false).await?; + sleep(Duration::from_millis(200)).await; + + // Click + println!("Clicking at ({current_x}, {current_y})"); + send_mouse_click(&room, current_x, current_y, 0).await?; + sleep(Duration::from_millis(500)).await; + + // Move up + let new_y2 = current_y - move_distance; + println!("Moving up to ({current_x}, {new_y2})"); + smooth_cursor_move(&room, current_x, current_y, current_x, new_y2).await?; + current_y = new_y2; + sleep(Duration::from_millis(500)).await; + + println!("\n=== Phase 4: Final Click Animation Sequence ==="); + // Enable click animation + println!("Enabling click animation"); + send_click_animation(&room, true).await?; + sleep(Duration::from_millis(200)).await; + + // Click - move right - click + println!("Clicking at ({current_x}, {current_y})"); + send_mouse_click(&room, current_x, current_y, 0).await?; + sleep(Duration::from_millis(500)).await; + + let final_x = current_x + move_distance; + println!("Moving right to ({final_x}, {current_y})"); + smooth_cursor_move(&room, current_x, current_y, final_x, current_y).await?; + current_x = final_x; + sleep(Duration::from_millis(500)).await; + + println!("Clicking at ({current_x}, {current_y})"); + send_mouse_click(&room, current_x, current_y, 0).await?; + sleep(Duration::from_millis(500)).await; + + println!("\n=== TEST COMPLETED ==="); + println!("All click animation phases completed successfully."); + + // Stop the screenshare session + screenshare_client::stop_screenshare_session(&mut cursor_socket)?; + println!("Screenshare session stopped."); + + Ok(()) +} + +/// Test cursor transitions with remote control enabled - Animation toggling +/// Expected behavior: +/// - Move: normal cursor (no control) +/// - Click: take control +/// - Move: give control back -> pointy finger +/// - Enable animation: pointy finger still +/// - Move: pointy finger still +/// - Click: click animation plays +/// - Disable animation: back to pointy finger +/// - Move: pointy finger +/// - Click: take control again (no animation) +pub async fn test_transitions_remote_enabled_with_animation() -> io::Result<()> { + println!("\n=== TEST: Remote Control Enabled - Animation Toggling ==="); + let (mut cursor_socket, _) = screenshare_client::start_screenshare_session()?; + + let url = std::env::var("LIVEKIT_URL").expect("LIVEKIT_URL environment variable not set"); + let token = livekit_utils::generate_token("TestUser"); + let (room, _rx) = Room::connect(&url, &token, RoomOptions::default()) + .await + .unwrap(); + + println!("Participant connected. Waiting for textures..."); + tokio::time::sleep(std::time::Duration::from_secs(7)).await; + + let mut x = 0.3; + let y = 0.3; + let step = 0.15; + + // Move (normal cursor, no control) + println!("\n1. Move - Expected: normal cursor (no control)"); + smooth_cursor_move(&room, x, y, x + step, y).await?; + x += step; + sleep(Duration::from_millis(1000)).await; + + // Click (take control) + println!("\n2. Click - Expected: take control"); + send_mouse_click(&room, x, y, 0).await?; + sleep(Duration::from_millis(1000)).await; + + // Move (give control back, pointy finger) + println!("\n3. Move - Expected: give control back -> pointy finger"); + smooth_cursor_move(&room, x, y, x + step, y).await?; + x += step; + sleep(Duration::from_millis(1000)).await; + + // Enable animation (pointy finger still) + println!("\n4. Enable animation - Expected: pointy finger still"); + send_click_animation(&room, true).await?; + sleep(Duration::from_millis(1000)).await; + + // Move (pointy finger) + println!("\n5. Move - Expected: pointy finger"); + smooth_cursor_move(&room, x, y, x + step, y).await?; + x += step; + sleep(Duration::from_millis(1000)).await; + + // Click (click animation) + println!("\n6. Click - Expected: click animation plays"); + send_mouse_click(&room, x, y, 0).await?; + sleep(Duration::from_millis(1000)).await; + + // Disable animation (back to pointy finger) + println!("\n7. Disable animation - Expected: back to pointy finger"); + send_click_animation(&room, false).await?; + sleep(Duration::from_millis(1000)).await; + + // Move (pointy finger) + println!("\n8. Move - Expected: pointy finger"); + smooth_cursor_move(&room, x, y, x + step, y).await?; + x += step; + sleep(Duration::from_millis(1000)).await; + + // Click (take control, no animation) + println!("\n9. Click - Expected: take control (no animation)"); + send_mouse_click(&room, x, y, 0).await?; + sleep(Duration::from_millis(1000)).await; + + println!("\n=== TEST COMPLETED ==="); + screenshare_client::stop_screenshare_session(&mut cursor_socket)?; + Ok(()) +} + +/// Test cursor transitions with remote control toggling +/// Expected behavior: +/// - Move: normal cursor (no control) +/// - Click: take control +/// - Move: give control back -> pointy finger +/// - Disable remote control: pointy finger still (but clicks won't work) +/// - Move: pointy finger +/// - Click: noop (no action, stays pointy finger) +pub async fn test_transitions_remote_enabled_then_disabled() -> io::Result<()> { + println!("\n=== TEST: Remote Control Enabled Then Disabled ==="); + let (mut cursor_socket, _) = screenshare_client::start_screenshare_session()?; + + let url = std::env::var("LIVEKIT_URL").expect("LIVEKIT_URL environment variable not set"); + let token = livekit_utils::generate_token("TestUser"); + let (room, _rx) = Room::connect(&url, &token, RoomOptions::default()) + .await + .unwrap(); + + println!("Participant connected. Waiting for textures..."); + tokio::time::sleep(std::time::Duration::from_secs(7)).await; + + let mut x = 0.3; + let y = 0.4; + let step = 0.15; + + // Move (normal cursor, no control) + println!("\n1. Move - Expected: normal cursor (no control)"); + smooth_cursor_move(&room, x, y, x + step, y).await?; + x += step; + sleep(Duration::from_millis(1000)).await; + + // Click (take control) + println!("\n2. Click - Expected: take control"); + send_mouse_click(&room, x, y, 0).await?; + sleep(Duration::from_millis(1000)).await; + + // Move (give control back, pointy finger) + println!("\n3. Move - Expected: give control back -> pointy finger"); + smooth_cursor_move(&room, x, y, x + step, y).await?; + x += step; + sleep(Duration::from_millis(1000)).await; + + // Disable remote control + println!("\n4. Disable ControllerCursorEnabled - Expected: pointy finger (but no interaction)"); + cursor_socket.send_message(Message::ControllerCursorEnabled(false))?; + sleep(Duration::from_millis(1000)).await; + + // Move (still pointy finger) + println!("\n5. Move - Expected: pointy finger"); + smooth_cursor_move(&room, x, y, x + step, y).await?; + x += step; + sleep(Duration::from_millis(1000)).await; + + // Click (noop - no action) + println!("\n6. Click - Expected: noop (no action, stays pointy finger)"); + send_mouse_click(&room, x, y, 0).await?; + sleep(Duration::from_millis(1000)).await; + + println!("\n=== TEST COMPLETED ==="); + screenshare_client::stop_screenshare_session(&mut cursor_socket)?; + Ok(()) +} + +/// Test cursor transitions with remote control disabled from start +/// Expected behavior: +/// - Move: pointy finger (remote control disabled) +/// - Click: noop (no action) +/// - Move: pointy finger +/// - Enable animation: pointy finger with animation enabled +/// - Move: pointy finger +/// - Click: click animation plays (but doesn't take control) +/// - Disable animation: back to pointy finger +/// - Move: pointy finger +/// - Click: noop (no action) +pub async fn test_transitions_remote_disabled_with_animation() -> io::Result<()> { + println!("\n=== TEST: Remote Control Disabled - Animation Toggling ==="); + let (mut cursor_socket, _) = screenshare_client::start_screenshare_session()?; + + // Disable remote control before connecting + cursor_socket.send_message(Message::ControllerCursorEnabled(false))?; + + let url = std::env::var("LIVEKIT_URL").expect("LIVEKIT_URL environment variable not set"); + let token = livekit_utils::generate_token("TestUser"); + let (room, _rx) = Room::connect(&url, &token, RoomOptions::default()) + .await + .unwrap(); + + println!("Participant connected. Waiting for textures..."); + tokio::time::sleep(std::time::Duration::from_secs(7)).await; + + let mut x = 0.3; + let y = 0.5; + let step = 0.15; + + // Move (pointy finger) + println!("\n1. Move - Expected: pointy finger (remote control disabled)"); + smooth_cursor_move(&room, x, y, x + step, y).await?; + x += step; + sleep(Duration::from_millis(1000)).await; + + // Click (noop) + println!("\n2. Click - Expected: noop (no action)"); + send_mouse_click(&room, x, y, 0).await?; + sleep(Duration::from_millis(1000)).await; + + // Move (pointy finger) + println!("\n3. Move - Expected: pointy finger"); + smooth_cursor_move(&room, x, y, x + step, y).await?; + x += step; + sleep(Duration::from_millis(1000)).await; + + // Enable animation + println!("\n4. Enable animation - Expected: pointy finger with animation enabled"); + send_click_animation(&room, true).await?; + sleep(Duration::from_millis(1000)).await; + + // Move (pointy finger) + println!("\n5. Move - Expected: pointy finger"); + smooth_cursor_move(&room, x, y, x + step, y).await?; + x += step; + sleep(Duration::from_millis(1000)).await; + + // Click (animation plays but doesn't take control) + println!("\n6. Click - Expected: click animation plays (but doesn't take control)"); + send_mouse_click(&room, x, y, 0).await?; + sleep(Duration::from_millis(1000)).await; + + // Disable animation + println!("\n7. Disable animation - Expected: back to pointy finger"); + send_click_animation(&room, false).await?; + sleep(Duration::from_millis(1000)).await; + + // Move (pointy finger) + println!("\n8. Move - Expected: pointy finger"); + smooth_cursor_move(&room, x, y, x + step, y).await?; + x += step; + sleep(Duration::from_millis(1000)).await; + + // Click (noop) + println!("\n9. Click - Expected: noop (no action)"); + send_mouse_click(&room, x, y, 0).await?; + sleep(Duration::from_millis(1000)).await; + + println!("\n=== TEST COMPLETED ==="); + screenshare_client::stop_screenshare_session(&mut cursor_socket)?; + Ok(()) +} + +/// Test cursor transitions with remote control toggling and animation +/// Expected behavior: +/// - Move: normal cursor (no control) +/// - Click: take control +/// - Move: give control back -> pointy finger +/// - Enable animation: pointy finger with animation +/// - Move: pointy finger +/// - Click: click animation plays +/// - Disable remote control: pointy finger (clicks won't work) +/// - Move: pointy finger +/// - Click: noop (animation doesn't play, no control taken) +/// - Disable animation: pointy finger +/// - Move: pointy finger +/// - Click: noop (no action) +pub async fn test_transitions_mixed_remote_and_animation() -> io::Result<()> { + println!("\n=== TEST: Mixed Remote Control and Animation Transitions ==="); + let (mut cursor_socket, _) = screenshare_client::start_screenshare_session()?; + + let url = std::env::var("LIVEKIT_URL").expect("LIVEKIT_URL environment variable not set"); + let token = livekit_utils::generate_token("TestUser"); + let (room, _rx) = Room::connect(&url, &token, RoomOptions::default()) + .await + .unwrap(); + + println!("Participant connected. Waiting for textures..."); + tokio::time::sleep(std::time::Duration::from_secs(7)).await; + + let mut x = 0.1; + let y = 0.6; + let step = 0.12; + + // Move (normal cursor) + println!("\n1. Move - Expected: normal cursor (no control)"); + smooth_cursor_move(&room, x, y, x + step, y).await?; + x += step; + sleep(Duration::from_millis(1000)).await; + + // Click (take control) + println!("\n2. Click - Expected: take control"); + send_mouse_click(&room, x, y, 0).await?; + sleep(Duration::from_millis(1000)).await; + + // Move (give control, pointy finger) + println!("\n3. Move - Expected: give control back -> pointy finger"); + smooth_cursor_move(&room, x, y, x + step, y).await?; + x += step; + sleep(Duration::from_millis(1000)).await; + + // Enable animation + println!("\n4. Enable animation - Expected: pointy finger with animation"); + send_click_animation(&room, true).await?; + sleep(Duration::from_millis(1000)).await; + + // Move (pointy finger) + println!("\n5. Move - Expected: pointy finger"); + smooth_cursor_move(&room, x, y, x + step, y).await?; + x += step; + sleep(Duration::from_millis(1000)).await; + + // Click (animation plays) + println!("\n6. Click - Expected: click animation plays"); + send_mouse_click(&room, x, y, 0).await?; + sleep(Duration::from_millis(1000)).await; + + // Disable remote control + println!("\n7. Disable ControllerCursorEnabled - Expected: pointy finger (clicks won't work)"); + cursor_socket.send_message(Message::ControllerCursorEnabled(false))?; + sleep(Duration::from_millis(1000)).await; + + // Click (noop - animation doesn't play, no control) + println!("\n8. Click - Expected: noop (animation doesn't play, no control taken)"); + send_mouse_click(&room, x, y, 0).await?; + sleep(Duration::from_millis(1000)).await; + + // Disable remote control + println!("\n9. Enable ControllerCursorEnabled - Expected: pointy finger (clicks won't work)"); + cursor_socket.send_message(Message::ControllerCursorEnabled(true))?; + sleep(Duration::from_millis(1000)).await; + + println!("\n10. Click - Expected: click animation plays"); + send_mouse_click(&room, x, y, 0).await?; + sleep(Duration::from_millis(1000)).await; + + // Move (pointy finger) + println!("\n11. Move - Expected: pointy finger"); + smooth_cursor_move(&room, x, y, x + step, y).await?; + x += step; + sleep(Duration::from_millis(1000)).await; + + // Disable animation + println!("\n12. Disable animation - Expected: pointy finger"); + send_click_animation(&room, false).await?; + sleep(Duration::from_millis(1000)).await; + + // Move (pointy finger) + println!("\n13. Move - Expected: pointy finger"); + smooth_cursor_move(&room, x, y, x + step, y).await?; + x += step; + sleep(Duration::from_millis(1000)).await; + + // Click (noop) + println!("\n14. Click - Expected: noop (no action)"); + send_mouse_click(&room, x, y, 0).await?; + sleep(Duration::from_millis(1000)).await; + + println!("\n=== TEST COMPLETED ==="); + screenshare_client::stop_screenshare_session(&mut cursor_socket)?; + Ok(()) +} diff --git a/core/tests/src/screenshare_client.rs b/core/tests/src/screenshare_client.rs index ec288ba..3ca06fa 100644 --- a/core/tests/src/screenshare_client.rs +++ b/core/tests/src/screenshare_client.rs @@ -37,8 +37,17 @@ pub fn request_screenshare( }, token, resolution: Extent { width, height }, + accessibility_permission: true, + use_av1: false, }); - socket.send_message(message) + socket.send_message(message).unwrap(); + + match socket.receive_message() { + Ok(_message) => Ok(()), + Err(e) => Err(io::Error::other(format!( + "Failed to receive message: {e:?}" + ))), + } } /// Sends a request to stop screen sharing. diff --git a/tauri/src/App.css b/tauri/src/App.css index f7e0bbe..09d3cf8 100644 --- a/tauri/src/App.css +++ b/tauri/src/App.css @@ -466,6 +466,31 @@ body { animation: oscillate 0.5s ease-in-out infinite; } +@keyframes click-ripple-effect { + 0% { + opacity: 1; + transform: scale(0.8); + } + 50% { + opacity: 0.8; + transform: scale(1.2); + } + 100% { + opacity: 0; + transform: scale(1.6); + } +} + +.click-ripple { + position: fixed; + width: 10px; + height: 10px; + background-color: transparent; + border-radius: 50%; + border: 2px solid #3b82f6; + pointer-events: none; +} + /* Hack to make this feel native app */ * { -webkit-user-select: none; diff --git a/tauri/src/components/SharingScreen/Controls.tsx b/tauri/src/components/SharingScreen/Controls.tsx index 33238a5..42417f1 100644 --- a/tauri/src/components/SharingScreen/Controls.tsx +++ b/tauri/src/components/SharingScreen/Controls.tsx @@ -6,6 +6,7 @@ import { BiSolidJoystick } from "react-icons/bi"; import useStore from "@/store/store"; import { SegmentedControl } from "../ui/segmented-control"; import { useState } from "react"; +import { CustomIcons } from "../ui/icons"; export function ScreenSharingControls() { const { setIsSharingKeyEvents, setIsSharingMouse } = useSharingContext(); @@ -27,23 +28,25 @@ export function ScreenSharingControls() {
- , - tooltipContent: "Remote control", - }, - { - id: "pointing", - content: , - tooltipContent: "Pointing", - }, - ]} - value={remoteControlStatus} - onValueChange={handleRemoteControlChange} - className="pointer-events-auto" - /> +
+ , + tooltipContent: "Remote control", + }, + { + id: "pointing", + content: , + tooltipContent: "Pointing", + }, + ]} + value={remoteControlStatus} + onValueChange={handleRemoteControlChange} + className="pointer-events-auto" + /> +
{isRemoteControlEnabled === false && (
diff --git a/tauri/src/components/SharingScreen/SharingScreen.tsx b/tauri/src/components/SharingScreen/SharingScreen.tsx index 3ec6b14..c34ce0b 100644 --- a/tauri/src/components/SharingScreen/SharingScreen.tsx +++ b/tauri/src/components/SharingScreen/SharingScreen.tsx @@ -209,6 +209,21 @@ const ConsumerComponent = React.memo(() => { return () => clearInterval(interval); }, []); + // Apply cursor ripple effect function + const applyCursorRippleEffect = (e: MouseEvent) => { + const ripple = document.createElement("div"); + + ripple.className = "click-ripple"; + document.body.appendChild(ripple); + + ripple.style.left = `${e.clientX - 10}px`; + ripple.style.top = `${e.clientY - 10}px`; + ripple.style.animation = "click-ripple-effect 0.8s ease-out forwards"; + ripple.onanimationend = () => { + document.body.removeChild(ripple); + }; + }; + /** * Currently returning the last screen share track * If there are multiple screen share tracks, and some are "white" @@ -284,6 +299,11 @@ const ConsumerComponent = React.memo(() => { const { relativeX, relativeY } = getRelativePosition(videoElement, e); // console.debug(`Clicking down 🖱️: relativeX: ${relativeX}, relativeY: ${relativeY}, detail ${e.detail}`); + // Add click pulse when NOT sharing mouse (pointing mode) + if (!isSharingMouse) { + applyCursorRippleEffect(e); + } + const payload: TPMouseClick = { type: "MouseClick", payload: { @@ -356,18 +376,18 @@ const ConsumerComponent = React.memo(() => { if (videoElement) { const payload: TPMouseVisible = { type: "MouseVisible", - payload: { visible: isSharingMouse }, + payload: { visible: !isSharingMouse }, }; localParticipant.localParticipant?.publishData(encoder.encode(JSON.stringify(payload)), { reliable: true }); } if (videoElement) { videoElement.addEventListener("mousemove", handleMouseMove); + videoElement.addEventListener("mousedown", handleMouseDown); } if (videoElement && isSharingMouse) { videoElement.addEventListener("wheel", handleWheel); - videoElement.addEventListener("mousedown", handleMouseDown); videoElement.addEventListener("mouseup", handleMouseUp); videoElement.addEventListener("contextmenu", handleContextMenu); } diff --git a/tauri/src/components/ui/icons.tsx b/tauri/src/components/ui/icons.tsx index 95c5d5e..bf1ee80 100644 --- a/tauri/src/components/ui/icons.tsx +++ b/tauri/src/components/ui/icons.tsx @@ -31,9 +31,33 @@ const CornerIcon = (props: SVGProps) => ( ); +const PointerClickIcon = (props: SVGProps) => ( + + + + + + + + + + + +); + const CustomIcons = { Drag: DragIcon, Corner: CornerIcon, + PointerClick: PointerClickIcon, }; export { CustomIcons }; diff --git a/tauri/src/windows/window-utils.ts b/tauri/src/windows/window-utils.ts index 5751195..faf6968 100644 --- a/tauri/src/windows/window-utils.ts +++ b/tauri/src/windows/window-utils.ts @@ -64,7 +64,7 @@ const createContentPickerWindow = async (videoToken: string, useAv1: boolean) => hiddenTitle: true, titleBarStyle: "overlay", resizable: true, - alwaysOnTop: false, + alwaysOnTop: true, visible: true, title: "Content picker", });