Skip to content

refactor: remove wgpu rendering#257

Merged
iparaskev merged 17 commits intomainfrom
cursor_migrate_to_iced
Feb 11, 2026
Merged

refactor: remove wgpu rendering#257
iparaskev merged 17 commits intomainfrom
cursor_migrate_to_iced

Conversation

@iparaskev
Copy link
Contributor

@iparaskev iparaskev commented Feb 10, 2026

Summary by CodeRabbit

  • New Features

    • SVG support enabled for richer graphics
    • Per-participant drawing and cursor modes with a participants manager
    • Public clock abstraction and test clock; new render_frame entrypoint
  • Improvements

    • Click animations rewritten to a frame-based, clock-driven model
    • Cursor rendering simplified to image-based overlays with clearer mode handling
    • Unified redraw/render flow with background redraw thread for more reliable updates
  • Documentation

    • Expanded architecture and usage docs (Conventions, Plan Mode, examples)

@netlify
Copy link

netlify bot commented Feb 10, 2026

Deploy Preview for hoppdocs ready!

Name Link
🔨 Latest commit d5cb626
🔍 Latest deploy log https://app.netlify.com/projects/hoppdocs/deploys/698c44c27a9990000803608b
😎 Deploy Preview https://deploy-preview-257--hoppdocs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link

coderabbitai bot commented Feb 10, 2026

📝 Walkthrough

Walkthrough

Refactors graphics and input to replace GPU/wgpu-based rendering with an iced canvas + ParticipantsManager architecture; adds a Clock abstraction and TestClock; replaces thread-based cursor wrappers with CursorState; centralizes redraw/render flows and click animations via GraphicsContext and ClickAnimationRenderer.

Changes

Cohort / File(s) Summary
Docs & Dependency
core/AGENTS.md, core/Cargo.toml
Added architecture/conventions sections to AGENTS.md; enabled svg feature for iced in Cargo.toml.
Clock & Time API
core/src/utils/clock.rs
Introduced Clock trait, RealClock, TestClock, and default_clock() for injectable/testable time sources.
Click Animation (canvas)
core/src/graphics/click_animation.rs
Rewrote click animations to use shared Arc<dyn Clock>, CPU-timed lifecycles, and iced canvas drawing; removed per-animation GPU buffers, textures, bind-groups, and shader-based pipeline.
Cursor (canvas)
core/src/graphics/cursor.rs
Replaced GPU cursor pipeline with image-based cursors (SVG→PNG), added CursorMode, new cursor API (new, set_position, set_mode, draw), and removed low-level GPU resources.
Participant subsystem
core/src/graphics/participant.rs
Added Participant and ParticipantsManager to manage per-user Draw, Cursor, color assignment, names, and aggregated draw/cursor rendering.
Removed GPU shader & draw manager
core/src/graphics/draw.rs, core/src/graphics/shader.wgsl, core/src/graphics/point.rs
Deleted legacy DrawManager, WGSL shader file, and GPU transform/Point types used by the old renderer.
Iced renderer & canvas integration
core/src/graphics/iced_canvas.rs, core/src/graphics/iced_renderer.rs
OverlaySurface/Canvas and IcedRenderer::draw updated to accept ParticipantsManager and ClickAnimationRenderer; removed DrawManager APIs and associated draw orchestration methods.
GraphicsContext & redraw threading
core/src/graphics/graphics_context.rs
Added redraw-thread subsystem and commands, participants wiring, clock injection (new/with_clock), trigger_render/trigger_click_animation, redraw_sender() and Drop behavior to stop/join redraw thread.
Input / Cursor controller
core/src/input/mouse.rs
Replaced thread-based CursorWrapper with CursorState, simplified error variants, introduced mode-based ControllerCursor and SharerCursor, added update_cursors/hide_inactive_cursors, integrated clock and redraw signaling.
Library & API routing
core/src/lib.rs
Exposed utils::clock, added RemoteControl::render_frame, removed UserEvent::EnableClickAnimation, and updated call sites to use GraphicsContext/ParticipantsManager and clock injection.
Tests & minor edits
core/tests/src/main.rs, core/tests/src/remote_drawing.rs, core/src/window_manager.rs
Adjusted test branch handling, removed letter-stroke helpers from tests, and a minor log formatting tweak in window_manager.

Sequence Diagram(s)

sequenceDiagram
  participant App
  participant GraphicsContext as "GraphicsContext\n(redraw thread)"
  participant Participants as "ParticipantsManager"
  participant ClickAnim as "ClickAnimationRenderer"
  participant Iced as "IcedRenderer / Canvas"
  participant Clock as "Clock (Real/Test)"

  App->>GraphicsContext: trigger_render() / render_frame()
  GraphicsContext->>Clock: now()
  GraphicsContext->>Participants: request participant state (drawings, cursors)
  Participants-->>GraphicsContext: geometries + cursor positions
  GraphicsContext->>ClickAnim: collect active animations (positions, start times)
  ClickAnim->>Clock: now()
  ClickAnim-->>GraphicsContext: animation geometries
  GraphicsContext->>Iced: draw(participants, click_animations, frame)
  Iced->>Iced: canvas draw (images, paths, rings)
  Iced-->>App: present frame
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • konsalex

"I'm a rabbit, nibbling clocks and pens,
Hopping through cursors, joining all the dens.
Participants gather, colors take their place,
Canvas rings expand with a gentle grace.
Hop—render—cheer: a brighter frame embraced!" 🐇✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'refactor: remove wgpu rendering' accurately and concisely summarizes the main objective: migrating from wgpu-based GPU rendering to iced canvas-based rendering across the graphics subsystem.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch cursor_migrate_to_iced

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
core/src/graphics/click_animation.rs (1)

155-159: ⚠️ Potential issue | 🟠 Major

Fix the incorrect buffer argument in disable call at line 158.

The disable method expects transforms_buffer but receives radius_buffer. Since both are &wgpu::Buffer, this compiles silently but writes the off-screen position data to the wrong buffer, preventing the animation from being hidden after its duration expires.

Update the draw method signature to accept transforms_buffer:

Proposed fix
     pub fn draw(
         &mut self,
         render_pass: &mut wgpu::RenderPass,
         queue: &wgpu::Queue,
         radius_buffer: &wgpu::Buffer,
+        transforms_buffer: &wgpu::Buffer,
         transforms_bind_group: &wgpu::BindGroup,
         radius_bind_group: &wgpu::BindGroup,
     ) {

Then update the call at line 158:

-        if elapsed > ANIMATION_DURATION.into() {
-            self.disable(queue, radius_buffer);
-        }
+        if elapsed > ANIMATION_DURATION.into() {
+            self.disable(queue, transforms_buffer);
+        }

And update the call site in ClickAnimationRenderer::draw (line 702):

             self.click_animations[*slot].draw(
                 render_pass,
                 queue,
                 &self.radius_buffer,
+                &self.transforms_buffer,
                 &self.transforms_bind_group,
                 &self.radius_bind_group,
             );
core/src/graphics/graphics_context.rs (1)

531-539: ⚠️ Potential issue | 🟡 Minor

Remove enable_click_animation — it is superseded by trigger_click_animation.

trigger_click_animation (line 437) performs the same operation while also signaling the redraw thread via RedrawThreadCommands::Activity. The public enable_click_animation method (line 535) is only called internally from trigger_click_animation and has no external callers. Remove it to reduce API surface and eliminate dead code.

🤖 Fix all issues with AI agents
In `@core/src/graphics/graphics_context.rs`:
- Around line 109-113: The MaxParticipantsReached error variant in the error
enum in graphics_context.rs is unused and triggers clippy warnings; remove the
MaxParticipantsReached variant from that enum (delete the line #[error("Maximum
number of participants reached")] MaxParticipantsReached,) so the enum contains
only actually used variants and rerun cargo clippy -D warnings; if this variant
is planned for future behavior, instead leave a TODO comment and add the variant
only when a code path returns it.

In `@core/src/graphics/participant.rs`:
- Around line 34-70: The disambiguation currently only uses parts[1]; update
generate_unique_visible_name to treat everything after the first token as the
"last" portion (e.g., let remaining = parts[1..].join(" ")), then build
progressive candidates by taking increasing character prefixes of that entire
remaining string (partial_last_name = remaining.chars().take(i).collect()) and
formatting candidates as format!("{first_name} {partial_last_name}"); keep the
original first_name-only candidate, iterate candidates to find the first unused,
and retain the existing numeric fallback using candidates.last().
- Around line 197-211: remove_participant currently logs when
self.participants.remove(sid) returns None but then calls unwrap(), causing a
panic; change the control flow to handle the Option safely (e.g., use if let
Some(participant) = self.participants.remove(sid) { ... } else { log::warn!(...)
; return; }) so you return early when the participant is missing and only push
participant.color into self.available_colors for non-"local" participants inside
the Some branch; reference the remove_participant method, the participants map,
the available_colors deque, and the sid parameter when making the fix.

In `@core/src/lib.rs`:
- Around line 463-470: The participant name passed to
graphics_context.add_participant uses "Me " with a trailing space; remove the
trailing space or explicitly trim it (replace "Me " with "Me" or call .trim())
in the add_participant call, or if the space is intentional leave "Me " but add
a clarifying comment above the add_participant invocation explaining why the
trailing space is required; reference the add_participant("local", "Me ", true)
call in core::graphics_context usage.
- Around line 1182-1191: The redraw handler currently calls
remote_control.gfx.window().focus_window(), toggles visibility, and re-sets the
pencil cursor every RedrawRequested while self.local_drawing.enabled and
cursor_set_times < 500; change this so focus_window() and the visibility toggle
run far less frequently: e.g., only when self.local_drawing.cursor_set_times ==
0 (first frame) or when a stored timestamp (add a field like
local_drawing.last_cursor_set: Instant/Option<Instant>) shows more than a
threshold (e.g., 100–200ms) has passed; still call
set_cursor(remote_control.pencil_cursor.clone()) as needed and increment
cursor_set_times, but guard focus_window() and set_cursor_visible() behind the
one-time or time-based check so they aren’t executed every RedrawRequested.
Ensure you update the struct to track last_cursor_set and use it in the
conditional alongside cursor_set_times.

In `@core/tests/src/main.rs`:
- Around line 243-250: The match arms for DrawingTest::PermanentOn and
DrawingTest::PermanentOff are inverted: swap the printed messages and the test
function calls so that DrawingTest::PermanentOn prints "permanent mode ON..."
and calls remote_drawing::test_draw_and_clear_paths_individually().await?, and
DrawingTest::PermanentOff prints "permanent mode OFF..." and calls
remote_drawing::test_draw_and_clear_all_paths().await?; update the two arms that
currently reference those functions and println! messages accordingly.
🧹 Nitpick comments (10)
core/AGENTS.md (1)

33-40: Architecture overview will become stale as wgpu rendering is removed.

Line 37 states "rendering uses wgpu", which is accurate today but contradicts the direction of this PR ("remove wgpu rendering"). Consider updating this to reflect the iced-based rendering target, or adding a note about the ongoing migration, so the docs don't immediately go stale.

core/src/input/mouse.rs (2)

1060-1070: Minor inconsistency: direct cursor_state access on SharerCursor.

ControllerCursor exposes a hide_if_expired() wrapper (line 327), but SharerCursor doesn't — so line 1068 reaches directly into sharer_cursor.cursor_state.hide_if_expired(). Consider adding a matching wrapper on SharerCursor for consistency.

♻️ Proposed fix

Add to SharerCursor:

fn hide_if_expired(&mut self) {
    self.cursor_state.hide_if_expired();
}

Then update line 1068:

-            sharer_cursor.cursor_state.hide_if_expired();
+            sharer_cursor.hide_if_expired();

286-301: set_pointer_mode logic is correct but could benefit from a brief inline comment.

The conditional !enabled && remote_control_enabled → Normal is non-obvious. A one-liner explaining the interaction between per-controller pointer mode and the global enable/disable state would help future readers.

core/src/utils/clock.rs (1)

34-37: Consider documenting that advance panics on poisoned mutex.

This is test-only code so panicking is acceptable, but a # Panics doc section would be consistent with Rust documentation conventions.

core/src/graphics/cursor.rs (3)

49-61: Magic divisor 2.5 for cursor dimensions is undocumented.

The scaling factor / 2.5 applied to both normal and pointer cursor dimensions has no explanation. Consider extracting it as a named constant with a comment explaining how this value relates to the SVG scale_factor (which is 2.0 in render_user_badge_to_png). If the intent is to downscale the 2× rendered image slightly further, the rationale should be clear for future maintainers.

Proposed fix
+/// Scale divisor for cursor image dimensions.
+/// The SVG is rendered at 2× scale; dividing by 2.5 produces a slightly
+/// smaller on-screen cursor to match the design specification.
+const CURSOR_DISPLAY_SCALE: f32 = 2.5;
+
 let normal_cursor = (
     iced_core::image::Handle::from_bytes(normal_png),
     (
-        normal_dims.width() as f32 / 2.5,
-        normal_dims.height() as f32 / 2.5,
+        normal_dims.width() as f32 / CURSOR_DISPLAY_SCALE,
+        normal_dims.height() as f32 / CURSOR_DISPLAY_SCALE,
     ),
 );
 let pointer_cursor = (
     iced_core::image::Handle::from_bytes(pointer_png),
     (
-        pointer_dims.width() as f32 / 2.5,
-        pointer_dims.height() as f32 / 2.5,
+        pointer_dims.width() as f32 / CURSOR_DISPLAY_SCALE,
+        pointer_dims.height() as f32 / CURSOR_DISPLAY_SCALE,
     ),
 );

88-110: Prefer if let over is_none() + unwrap().

The early-return guard at line 90 makes the unwrap() at line 100 safe, but idiomatic Rust would use pattern matching to avoid the unwrap() entirely.

Suggested refactor
     pub fn draw(&self, frame: &mut Frame) {
-        if self.position.is_none() {
+        let Some(position) = &self.position else {
             return;
-        }
+        };
 
         let (handle, (width, height)) = match self.mode {
             CursorMode::Pointer => &self.pointer_cursor,
             CursorMode::Normal => &self.normal_cursor,
         };
 
         let image = iced_core::image::Image::new(handle.clone());
-        let position = self.position.as_ref().unwrap();
         frame.draw_image(
             Rectangle {
                 x: position.x as f32,
                 y: position.y as f32,
                 width: *width,
                 height: *height,
             },
             image,
         );
     }

42-47: Full PNG decode just to read dimensions.

image::load_from_memory decodes the entire PNG into a pixel buffer only to extract width/height. Since this runs at construction time it's unlikely to be a bottleneck, but if you want to avoid the unnecessary decode you could use image::ImageReader::new(std::io::Cursor::new(&normal_png)).with_guessed_format()?.into_dimensions()? which only reads the header.

core/src/graphics/participant.rs (3)

89-95: Redundant field name: color: color.

Clippy's redundant_field_names lint will flag color: color at line 93. Use the shorthand color instead.

As per coding guidelines, "Ensure code passes cargo fmt --all -- --check and cargo clippy -D warnings in CI".

Fix
         Ok(Self {
             draw: Draw::new(color, auto_clear),
             cursor: Cursor::new(color, name)?,
-            color: color,
+            color,
         })

166-176: Unnecessary & on DEFAULT_COLOR.

DEFAULT_COLOR is already &'static str, so &DEFAULT_COLOR creates &&str which then gets coerced back via deref. Using DEFAULT_COLOR directly is clearer.

Fix
             self.available_colors.pop_front().unwrap_or_else(|| {
                 log::warn!(
                     "ParticipantsManager::add_participant: no colors available for participant {}",
                     sid
                 );
-                &DEFAULT_COLOR
+                DEFAULT_COLOR
             })

328-340: Silent no-op for missing participants in cursor methods.

set_cursor_position and set_cursor_mode silently ignore missing participants, while every other method in ParticipantsManager logs a warning when a participant is not found. Consider adding consistent warning logs here too, to aid debugging.

Add consistent logging
     pub fn set_cursor_position(&mut self, sid: &str, position: Option<Position>) {
         if let Some(participant) = self.participants.get_mut(sid) {
             participant.cursor_mut().set_position(position);
+        } else {
+            log::warn!("ParticipantsManager::set_cursor_position: participant {} not found", sid);
         }
     }
 
     pub fn set_cursor_mode(&mut self, sid: &str, mode: CursorMode) {
         if let Some(participant) = self.participants.get_mut(sid) {
             participant.cursor_mut().set_mode(mode);
+        } else {
+            log::warn!("ParticipantsManager::set_cursor_mode: participant {} not found", sid);
         }
     }

Comment on lines +109 to 113

/// Maximum number of participants reached.
#[error("Maximum number of participants reached")]
MaxParticipantsReached,
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

rg -n --type=rust 'MaxParticipantsReached'

Repository: gethopp/hopp

Length of output: 125


MaxParticipantsReached error variant is unused and will fail clippy checks.

This variant is defined at line 112 but has no usages in the codebase. Per the coding guidelines, code must pass cargo clippy -D warnings, which flags unused enum variants. Either remove this variant or defer adding it until a code path actually produces this error.

🤖 Prompt for AI Agents
In `@core/src/graphics/graphics_context.rs` around lines 109 - 113, The
MaxParticipantsReached error variant in the error enum in graphics_context.rs is
unused and triggers clippy warnings; remove the MaxParticipantsReached variant
from that enum (delete the line #[error("Maximum number of participants
reached")] MaxParticipantsReached,) so the enum contains only actually used
variants and rerun cargo clippy -D warnings; if this variant is planned for
future behavior, instead leave a TODO comment and add the variant only when a
code path returns it.

Comment on lines +34 to +70
fn generate_unique_visible_name(name: &str, used_names: &[String]) -> String {
let parts: Vec<&str> = name.split_whitespace().collect();
let first_name = parts.first().unwrap_or(&name);

// Try progressively longer candidates
let candidates = if parts.len() > 1 {
let last_name = parts[1];
let mut candidates = vec![first_name.to_string()];

// Add candidates with increasing characters from last name
for i in 1..=last_name.chars().count() {
let partial_last_name: String = last_name.chars().take(i).collect();
candidates.push(format!("{first_name} {partial_last_name}"));
}
candidates
} else {
vec![first_name.to_string()]
};

// Find first unused candidate
for candidate in candidates.iter() {
if !used_names.contains(candidate) {
return candidate.clone();
}
}

// Fall back to numbering
let base = candidates.last().unwrap().clone();
for num in 2.. {
let candidate = format!("{base}{num}");
if !used_names.contains(&candidate) {
return candidate;
}
}

unreachable!()
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Name disambiguation only considers the second word.

For names with more than two parts (e.g., "John van der Berg"), the function only uses parts[1] ("van") for progressive disambiguation, skipping the rest. If two participants share "John van", it falls through to numeric suffixes ("John van2") instead of trying "John van d", "John van de", etc.

This is a minor UX edge case with a safe fallback (numbering), so not blocking.

🤖 Prompt for AI Agents
In `@core/src/graphics/participant.rs` around lines 34 - 70, The disambiguation
currently only uses parts[1]; update generate_unique_visible_name to treat
everything after the first token as the "last" portion (e.g., let remaining =
parts[1..].join(" ")), then build progressive candidates by taking increasing
character prefixes of that entire remaining string (partial_last_name =
remaining.chars().take(i).collect()) and formatting candidates as
format!("{first_name} {partial_last_name}"); keep the original first_name-only
candidate, iterate candidates to find the first unused, and retain the existing
numeric fallback using candidates.last().

Comment on lines +463 to +470
graphics_context
.add_participant("local".to_string(), "Me ", true)
.map_err(|e| {
log::error!(
"create_overlay_window: Failed to create local participant cursor: {e}"
);
ServerError::GfxCreationError
})?;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Trailing space in local participant name "Me ".

The name "Me " has a trailing space. If this is intentional (e.g., to ensure consistent SVG badge padding), a comment would help clarify. If accidental, it should be trimmed to "Me".

🤖 Prompt for AI Agents
In `@core/src/lib.rs` around lines 463 - 470, The participant name passed to
graphics_context.add_participant uses "Me " with a trailing space; remove the
trailing space or explicitly trim it (replace "Me " with "Me" or call .trim())
in the add_participant call, or if the space is intentional leave "Me " but add
a clarifying comment above the add_participant invocation explaining why the
trailing space is required; reference the add_participant("local", "Me ", true)
call in core::graphics_context usage.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@core/src/graphics/participant.rs`:
- Around line 160-195: add_participant currently overwrites an existing
Participant for the same sid without returning its color to available_colors,
draining the color pool; modify add_participant to check for an existing
participant (e.g., lookup or remove via self.participants.get/ remove/entry for
the given sid) before inserting the new one, capture the old participant's color
(using the Participant accessor or by taking the old Participant value), and
recycle that color back into self.available_colors (push_back) unless it's the
reserved SHARER_COLOR or a fallback DEFAULT_COLOR; then proceed to insert the
new Participant via Participant::new as before.
🧹 Nitpick comments (1)
core/src/graphics/participant.rs (1)

214-312: Consider extracting repeated participant-lookup-or-warn into a helper.

Every method follows the same if let Some(p) = self.participants.get_mut(sid) { … } else { log::warn!(…) } pattern. A small private helper (e.g., fn get_participant_mut(&mut self, sid: &str, caller: &str) -> Option<&mut Participant>) could reduce boilerplate.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@core/src/graphics/iced_canvas.rs`:
- Line 66: The constructor signature pub fn new(texture_path: &String) -> Self
will trigger clippy::ptr_arg; change it to accept a borrowed string slice (pub
fn new(texture_path: &str) -> Self) and update any internal uses of texture_path
and all call sites to pass &my_string or a &str as appropriate so the type
matches; ensure any conversions (e.g., to Path/PathBuf) still compile by calling
Path::new(texture_path) or texture_path.to_string() where needed.
🧹 Nitpick comments (5)
core/src/graphics/click_animation.rs (3)

9-10: Inconsistent types between duration constants.

ANIMATION_DURATION is u64 (and pub) while RING_DELAY_MS is u128. This forces .into() conversions at every comparison site (e.g., Line 99, 124). Consider making both the same type for clarity.


23-28: Per-animation Arc<dyn Clock> clone is unnecessary.

Every ClickAnimation stores its own Arc<dyn Clock>, but the clock is only used in enable() to snapshot the instant. Since ClickAnimationRenderer already owns the clock and has &mut self when calling enable(), you could pass the instant directly to enable() and remove the clock field from ClickAnimation.

Sketch
 struct ClickAnimation {
     position: Position,
     enabled_instant: Option<std::time::Instant>,
-    clock: Arc<dyn Clock>,
 }

 impl ClickAnimation {
-    fn enable(&mut self, position: Position) {
+    fn enable(&mut self, position: Position, now: std::time::Instant) {
         self.position = position;
-        self.enabled_instant = Some(self.clock.now());
+        self.enabled_instant = Some(now);
     }

In ClickAnimationRenderer::update():

+        let now = self.clock.now();
         // ...
-            self.click_animations[slot].enable(position);
+            self.click_animations[slot].enable(position, now);

75-79: enable_click_animation only needs &self, not &mut self.

Sender::send takes &self in the standard library. If this method only sends on the channel, the &mut self requirement is needlessly restrictive and prevents calling it while other immutable borrows are held.

Proposed fix
-    pub fn enable_click_animation(&mut self, position: Position) {
+    pub fn enable_click_animation(&self, position: Position) {
core/src/graphics/graphics_context.rs (1)

257-266: Non-idiomatic error handling for adapter request.

The if let Err followed by unwrap() pattern is safe but not idiomatic Rust. Consider using the ? operator or match directly.

Proposed fix
-        let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
+        let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
             power_preference: wgpu::PowerPreference::HighPerformance,
             compatible_surface: Some(&surface),
             force_fallback_adapter: false,
-        }));
-        if let Err(e) = adapter {
-            log::error!("GraphicsContext::new request_adapter: {e:?}");
-            return Err(OverlayError::AdapterRequestError);
-        }
-        let adapter = adapter.unwrap();
+        }))
+        .map_err(|e| {
+            log::error!("GraphicsContext::new request_adapter: {e:?}");
+            OverlayError::AdapterRequestError
+        })?;
core/src/graphics/participant.rs (1)

212-227: remove_participant no longer panics — consider using let-else for clarity.

The early return on line 221 makes the unwrap() on line 223 safe. However, the idiomatic Rust pattern is cleaner with let-else:

Proposed fix
     pub fn remove_participant(&mut self, sid: &str) {
         log::info!("ParticipantsManager::remove_participant: sid={}", sid);
-        let participant = self.participants.remove(sid);
-        if participant.is_none() {
+        let Some(participant) = self.participants.remove(sid) else {
             log::warn!(
                 "ParticipantsManager::remove_participant: participant {} not found",
                 sid
             );
+            return;
         };
-        let participant = participant.unwrap();
         if sid != "local" {
             self.available_colors.push_back(participant.color);
         }
     }

}

impl OverlaySurface {
pub fn new(texture_path: &String) -> Self {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

&String parameter will trigger clippy::ptr_arg warning.

Per the coding guidelines, code must pass cargo clippy -D warnings. Prefer &str over &String.

Proposed fix
-    pub fn new(texture_path: &String) -> Self {
+    pub fn new(texture_path: &str) -> Self {

As per coding guidelines, "Ensure code passes cargo fmt --all -- --check and cargo clippy -D warnings in CI".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub fn new(texture_path: &String) -> Self {
pub fn new(texture_path: &str) -> Self {
🤖 Prompt for AI Agents
In `@core/src/graphics/iced_canvas.rs` at line 66, The constructor signature pub
fn new(texture_path: &String) -> Self will trigger clippy::ptr_arg; change it to
accept a borrowed string slice (pub fn new(texture_path: &str) -> Self) and
update any internal uses of texture_path and all call sites to pass &my_string
or a &str as appropriate so the type matches; ensure any conversions (e.g., to
Path/PathBuf) still compile by calling Path::new(texture_path) or
texture_path.to_string() where needed.

@iparaskev iparaskev merged commit 1a1b576 into main Feb 11, 2026
27 of 32 checks passed
@iparaskev iparaskev deleted the cursor_migrate_to_iced branch February 11, 2026 10:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant