@@ -26,11 +26,15 @@ const CURSOR_MAX_STRENGTH: f32 = 5.0;
2626/// The size to render the svg to.
2727static SVG_CURSOR_RASTERIZED_HEIGHT : u32 = 200 ;
2828
29+ const CIRCLE_CURSOR_SIZE : u32 = 64 ;
30+
2931pub struct CursorLayer {
3032 statics : Statics ,
3133 bind_group : Option < BindGroup > ,
3234 cursors : HashMap < String , CursorTexture > ,
35+ circle_cursor : Option < CursorTexture > ,
3336 prev_is_svg_assets_enabled : Option < bool > ,
37+ prev_cursor_type : Option < CursorType > ,
3438}
3539
3640struct Statics {
@@ -187,8 +191,51 @@ impl CursorLayer {
187191 statics,
188192 bind_group : None ,
189193 cursors : Default :: default ( ) ,
194+ circle_cursor : None ,
190195 prev_is_svg_assets_enabled : None ,
196+ prev_cursor_type : None ,
197+ }
198+ }
199+
200+ fn create_circle_cursor ( constants : & RenderVideoConstants ) -> CursorTexture {
201+ let size = CIRCLE_CURSOR_SIZE ;
202+ let mut rgba = vec ! [ 0u8 ; ( size * size * 4 ) as usize ] ;
203+ let center = size as f32 / 2.0 ;
204+ let outer_radius = center - 2.0 ;
205+ let inner_radius = outer_radius - 4.0 ;
206+
207+ for y in 0 ..size {
208+ for x in 0 ..size {
209+ let dx = x as f32 - center;
210+ let dy = y as f32 - center;
211+ let dist = ( dx * dx + dy * dy) . sqrt ( ) ;
212+ let idx = ( ( y * size + x) * 4 ) as usize ;
213+
214+ if dist <= outer_radius && dist >= inner_radius {
215+ let edge_softness = 1.5 ;
216+ let outer_alpha = 1.0
217+ - ( ( dist - outer_radius + edge_softness) / edge_softness) . clamp ( 0.0 , 1.0 ) ;
218+ let inner_alpha = ( ( dist - inner_radius) / edge_softness) . clamp ( 0.0 , 1.0 ) ;
219+ let alpha = ( outer_alpha * inner_alpha * 255.0 ) as u8 ;
220+
221+ rgba[ idx] = 80 ;
222+ rgba[ idx + 1 ] = 80 ;
223+ rgba[ idx + 2 ] = 80 ;
224+ rgba[ idx + 3 ] = alpha;
225+ } else if dist < inner_radius {
226+ let fill_alpha = 0.15 ;
227+ let edge_alpha = ( ( inner_radius - dist) / 2.0 ) . clamp ( 0.0 , 1.0 ) ;
228+ let alpha = ( fill_alpha * edge_alpha * 255.0 ) as u8 ;
229+
230+ rgba[ idx] = 128 ;
231+ rgba[ idx + 1 ] = 128 ;
232+ rgba[ idx + 2 ] = 128 ;
233+ rgba[ idx + 3 ] = alpha;
234+ }
235+ }
191236 }
237+
238+ CursorTexture :: prepare ( constants, & rgba, ( size, size) , XY :: new ( 0.5 , 0.5 ) )
192239 }
193240
194241 pub fn prepare (
@@ -270,71 +317,81 @@ impl CursorLayer {
270317 }
271318 }
272319
273- // Remove all cursor assets if the svg configuration changes.
274- // it might change the texture.
275- //
276- // This would be better if it only invalidated the required assets but that would be more complicated.
320+ let cursor_type = uniforms. project . cursor . r#type . clone ( ) ;
321+
322+ if self . prev_cursor_type . as_ref ( ) != Some ( & cursor_type) {
323+ self . prev_cursor_type = Some ( cursor_type. clone ( ) ) ;
324+ self . circle_cursor = None ;
325+ }
326+
277327 if self . prev_is_svg_assets_enabled != Some ( uniforms. project . cursor . use_svg ) {
278328 self . prev_is_svg_assets_enabled = Some ( uniforms. project . cursor . use_svg ) ;
279329 self . cursors . drain ( ) ;
280330 }
281331
282- if !self . cursors . contains_key ( & interpolated_cursor. cursor_id ) {
283- let mut cursor = None ;
284-
285- let cursor_shape = match & constants. recording_meta . inner {
286- RecordingMetaInner :: Studio ( StudioRecordingMeta :: MultipleSegments {
287- inner :
288- MultipleSegments {
289- cursors : Cursors :: Correct ( cursors) ,
290- ..
291- } ,
292- } ) => cursors
293- . get ( & interpolated_cursor. cursor_id )
294- . and_then ( |v| v. shape ) ,
295- _ => None ,
296- } ;
297-
298- // Attempt to find and load a higher-quality SVG cursor included in Cap.
299- // These are used instead of the OS provided cursor images when possible as the quality is better.
300- if let Some ( cursor_shape) = cursor_shape
301- && uniforms. project . cursor . use_svg
302- && let Some ( info) = cursor_shape. resolve ( )
303- {
304- cursor = CursorTexture :: prepare_svg ( constants, info. raw , info. hotspot . into ( ) )
305- . map_err ( |err| {
306- error ! (
307- "Error loading SVG cursor {:?}: {err}" ,
308- interpolated_cursor. cursor_id
309- )
310- } )
311- . ok ( ) ;
332+ let cursor_texture = if cursor_type == CursorType :: Circle {
333+ if self . circle_cursor . is_none ( ) {
334+ self . circle_cursor = Some ( Self :: create_circle_cursor ( constants) ) ;
312335 }
313-
314- // If not we attempt to load the low-quality image cursor
315- if let StudioRecordingMeta :: MultipleSegments { inner, .. } = & constants. meta
316- && cursor. is_none ( )
317- && let Some ( c) = inner
318- . get_cursor_image ( & constants. recording_meta , & interpolated_cursor. cursor_id )
319- && let Ok ( img) = image:: open ( & c. path )
320- . map_err ( |err| error ! ( "Failed to load cursor image from {:?}: {err}" , c. path) )
321- {
322- cursor = Some ( CursorTexture :: prepare (
323- constants,
324- & img. to_rgba8 ( ) ,
325- img. dimensions ( ) ,
326- c. hotspot ,
327- ) ) ;
328- }
329-
330- if let Some ( cursor) = cursor {
331- self . cursors
332- . insert ( interpolated_cursor. cursor_id . clone ( ) , cursor) ;
336+ self . circle_cursor . as_ref ( ) . unwrap ( )
337+ } else {
338+ if !self . cursors . contains_key ( & interpolated_cursor. cursor_id ) {
339+ let mut loaded_cursor = None ;
340+
341+ let cursor_shape = match & constants. recording_meta . inner {
342+ RecordingMetaInner :: Studio ( StudioRecordingMeta :: MultipleSegments {
343+ inner :
344+ MultipleSegments {
345+ cursors : Cursors :: Correct ( cursors) ,
346+ ..
347+ } ,
348+ } ) => cursors
349+ . get ( & interpolated_cursor. cursor_id )
350+ . and_then ( |v| v. shape ) ,
351+ _ => None ,
352+ } ;
353+
354+ if let Some ( cursor_shape) = cursor_shape
355+ && uniforms. project . cursor . use_svg
356+ && let Some ( info) = cursor_shape. resolve ( )
357+ {
358+ loaded_cursor =
359+ CursorTexture :: prepare_svg ( constants, info. raw , info. hotspot . into ( ) )
360+ . map_err ( |err| {
361+ error ! (
362+ "Error loading SVG cursor {:?}: {err}" ,
363+ interpolated_cursor. cursor_id
364+ )
365+ } )
366+ . ok ( ) ;
367+ }
368+
369+ if let StudioRecordingMeta :: MultipleSegments { inner, .. } = & constants. meta
370+ && loaded_cursor. is_none ( )
371+ && let Some ( c) = inner
372+ . get_cursor_image ( & constants. recording_meta , & interpolated_cursor. cursor_id )
373+ && let Ok ( img) = image:: open ( & c. path ) . map_err ( |err| {
374+ error ! ( "Failed to load cursor image from {:?}: {err}" , c. path)
375+ } )
376+ {
377+ loaded_cursor = Some ( CursorTexture :: prepare (
378+ constants,
379+ & img. to_rgba8 ( ) ,
380+ img. dimensions ( ) ,
381+ c. hotspot ,
382+ ) ) ;
383+ }
384+
385+ if let Some ( c) = loaded_cursor {
386+ self . cursors
387+ . insert ( interpolated_cursor. cursor_id . clone ( ) , c) ;
388+ }
333389 }
334- }
335- let Some ( cursor_texture) = self . cursors . get ( & interpolated_cursor. cursor_id ) else {
336- error ! ( "Cursor {:?} not found!" , interpolated_cursor. cursor_id) ;
337- return ;
390+ let Some ( tex) = self . cursors . get ( & interpolated_cursor. cursor_id ) else {
391+ error ! ( "Cursor {:?} not found!" , interpolated_cursor. cursor_id) ;
392+ return ;
393+ } ;
394+ tex
338395 } ;
339396
340397 let size = {
0 commit comments