From 836ecd667b2236f4b416fd85c0cbacdc63a368dd Mon Sep 17 00:00:00 2001 From: theodkp Date: Sat, 6 Jun 2026 01:40:33 +0100 Subject: [PATCH 1/9] feat: add pen icon when remote drawing --- core/src/graphics/cursor.rs | 23 ++++- core/src/graphics/participant.rs | 10 +- core/src/input/mouse.rs | 28 +++++- core/src/lib.rs | 16 +-- core/src/utils/svg_renderer.rs | 166 +++++++++++++++++++++++++++++-- 5 files changed, 221 insertions(+), 22 deletions(-) diff --git a/core/src/graphics/cursor.rs b/core/src/graphics/cursor.rs index b47e7361..acffb9df 100644 --- a/core/src/graphics/cursor.rs +++ b/core/src/graphics/cursor.rs @@ -5,7 +5,7 @@ //! to convert SVGs to PNGs at construction time. use crate::utils::geometry::Position; -use crate::utils::svg_renderer::{render_user_badge_to_png, SvgRenderError}; +use crate::utils::svg_renderer::{render_user_badge_to_png, SvgRenderError, UserBadgeKind}; use iced::widget::canvas::Frame; use iced::Rectangle; use std::time::{Duration, Instant}; @@ -17,6 +17,8 @@ pub enum CursorMode { Normal, /// Pointer/hand cursor Pointer, + /// Pencil cursor for drawing mode + Pencil, } /// An image-based cursor for rendering on iced canvas frames. @@ -28,6 +30,8 @@ pub struct Cursor { normal_cursor: (iced_core::image::Handle, (f32, f32)), /// (handle, (width, height)) for pointer/hand cursor pointer_cursor: (iced_core::image::Handle, (f32, f32)), + /// (handle, (width, height)) for pencil cursor + pencil_cursor: (iced_core::image::Handle, (f32, f32)), /// Current position of the cursor position: Option, /// Current cursor display mode @@ -39,8 +43,9 @@ pub struct Cursor { impl Cursor { /// Creates a new `Cursor` with the given color and name. pub fn new(color: &str, name: &str) -> Result { - let normal_png = render_user_badge_to_png(color, name, false)?; - let pointer_png = render_user_badge_to_png(color, name, true)?; + let normal_png = render_user_badge_to_png(color, name, UserBadgeKind::Normal)?; + let pointer_png = render_user_badge_to_png(color, name, UserBadgeKind::Pointer)?; + let pencil_png = render_user_badge_to_png(color, name, UserBadgeKind::Pencil)?; let normal_dims = image::load_from_memory(&normal_png).map_err(|e| { SvgRenderError::PngSaveError(format!("Failed to read PNG dimensions: {e}")) @@ -48,6 +53,9 @@ impl Cursor { let pointer_dims = image::load_from_memory(&pointer_png).map_err(|e| { SvgRenderError::PngSaveError(format!("Failed to read PNG dimensions: {e}")) })?; + let pencil_dims = image::load_from_memory(&pencil_png).map_err(|e| { + SvgRenderError::PngSaveError(format!("Failed to read PNG dimensions: {e}")) + })?; let normal_cursor = ( iced_core::image::Handle::from_bytes(normal_png), @@ -63,11 +71,19 @@ impl Cursor { pointer_dims.height() as f32 / 2.5, ), ); + let pencil_cursor = ( + iced_core::image::Handle::from_bytes(pencil_png), + ( + pencil_dims.width() as f32 / 2.5, + pencil_dims.height() as f32 / 2.5, + ), + ); Ok(Self { visible_name: name.to_string(), normal_cursor, pointer_cursor, + pencil_cursor, position: None, mode: CursorMode::Normal, last_update: None, @@ -112,6 +128,7 @@ impl Cursor { let (handle, (width, height)) = match self.mode { CursorMode::Pointer => &self.pointer_cursor, + CursorMode::Pencil => &self.pencil_cursor, CursorMode::Normal => &self.normal_cursor, }; diff --git a/core/src/graphics/participant.rs b/core/src/graphics/participant.rs index fb45ce54..527b35f9 100644 --- a/core/src/graphics/participant.rs +++ b/core/src/graphics/participant.rs @@ -248,7 +248,15 @@ impl ParticipantsManager { mode ); if let Some(participant) = self.participants.get_mut(identity) { - participant.draw_mut().set_mode(mode); + participant.draw_mut().set_mode(mode.clone()); + match mode { + DrawingMode::Draw(_) => participant.cursor_mut().set_mode(CursorMode::Pencil), + DrawingMode::ClickAnimation => { + participant.cursor_mut().set_mode(CursorMode::Pointer) + } + DrawingMode::Disabled => participant.cursor_mut().set_mode(CursorMode::Normal), + DrawingMode::Any => {} + } } else { log::warn!( "ParticipantsManager::set_drawing_mode: participant {} not found", diff --git a/core/src/input/mouse.rs b/core/src/input/mouse.rs index c0578362..1c3333ba 100644 --- a/core/src/input/mouse.rs +++ b/core/src/input/mouse.rs @@ -240,6 +240,7 @@ struct ControllerCursor { */ clicked: bool, mode: CursorMode, + visual_mode_override: Option, pointer_mode: bool, has_control: bool, identity: String, @@ -251,6 +252,7 @@ impl ControllerCursor { cursor_state, clicked: false, mode, + visual_mode_override: None, pointer_mode: mode == CursorMode::Pointer, has_control: false, identity, @@ -283,10 +285,18 @@ impl ControllerCursor { self.mode } + fn visual_mode(&self) -> CursorMode { + self.visual_mode_override.unwrap_or(self.mode) + } + fn set_mode(&mut self, mode: CursorMode) { self.mode = mode; } + fn set_visual_mode_override(&mut self, mode: Option) { + self.visual_mode_override = mode; + } + fn set_pointer_mode(&mut self, enabled: bool, remote_control_enabled: bool) { if !enabled && remote_control_enabled { self.mode = CursorMode::Normal; @@ -1026,6 +1036,21 @@ impl CursorController { } } + /// Overrides only the rendered cursor badge for a controller. + /// + /// This keeps input-routing mode independent from drawing/click visual state. + pub fn set_controller_visual_mode(&mut self, identity: &str, mode: Option) { + log::info!("set_controller_visual_mode: {identity} {mode:?}"); + + let mut controllers_cursors = self.controllers_cursors.lock().unwrap(); + for controller in controllers_cursors.iter_mut() { + if controller.identity == identity { + controller.set_visual_mode_override(mode); + break; + } + } + } + /// Updates cursor positions in the ParticipantsManager for rendering. /// /// This function translates cursor state to pixel positions and updates the @@ -1056,7 +1081,8 @@ impl CursorController { if controller.visible() { participants_manager .set_cursor_position(&controller.identity, Some(controller.local_position())); - participants_manager.set_cursor_mode(&controller.identity, controller.mode()); + participants_manager + .set_cursor_mode(&controller.identity, controller.visual_mode()); } else { participants_manager.set_cursor_position(&controller.identity, None); } diff --git a/core/src/lib.rs b/core/src/lib.rs index 613c6b34..734e5478 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -59,6 +59,7 @@ pub(crate) mod windows; use camera::capturer::{poll_camera_stream, CameraCapturer}; use capture::capturer::{poll_stream, Capturer}; +use graphics::graphics_context::participant::CursorMode; use graphics::graphics_context::GraphicsContext; use graphics::graphics_window_context::ContextManager; use input::clipboard::ClipboardController; @@ -1211,18 +1212,21 @@ impl<'a> ApplicationHandler for Application<'a> { UserEvent::DrawingMode(drawing_mode, sid) => { log::debug!("user_event: DrawingMode: {:?} {}", drawing_mode, sid); if let Some(remote_control) = &mut self.remote_control { + let visual_mode = match &drawing_mode { + DrawingMode::Draw(_) => Some(CursorMode::Pencil), + DrawingMode::ClickAnimation => Some(CursorMode::Pointer), + DrawingMode::Disabled | DrawingMode::Any => None, + }; + let cursor_controller = &mut remote_control.cursor_controller; match &drawing_mode { DrawingMode::Disabled => { - remote_control - .cursor_controller - .set_controller_pointer(false, sid.as_str()); + cursor_controller.set_controller_pointer(false, sid.as_str()); } _ => { - remote_control - .cursor_controller - .set_controller_pointer(true, sid.as_str()); + cursor_controller.set_controller_pointer(true, sid.as_str()); } } + cursor_controller.set_controller_visual_mode(sid.as_str(), visual_mode); remote_control .gfx .set_drawing_mode(sid.as_str(), drawing_mode.clone()); diff --git a/core/src/utils/svg_renderer.rs b/core/src/utils/svg_renderer.rs index 3c8295b4..75242f67 100644 --- a/core/src/utils/svg_renderer.rs +++ b/core/src/utils/svg_renderer.rs @@ -18,6 +18,13 @@ pub enum SvgRenderError { PngSaveError(String), } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UserBadgeKind { + Normal, + Pointer, + Pencil, +} + /// Calculate dynamic box width based on text length /// Increases box width for longer text to ensure it fits comfortably fn calculate_box_width(text: &str) -> f32 { @@ -80,7 +87,7 @@ fn get_box_width(text: &str, fontdb: std::sync::Arc) -> Result Result, SvgRenderError> { // Create font database let mut fontdb = Database::new(); @@ -117,6 +124,21 @@ pub fn render_user_badge_to_png( let svg_height_pointer = 74.0 * scale_factor; let svg_height_regular = 75.0 * scale_factor; + if kind == UserBadgeKind::Pencil { + return render_pencil_user_badge_to_png( + color, + &name, + box_width, + text_x_regular, + svg_width_regular, + svg_height_regular, + scale_factor, + fontdb, + ); + } + + let pointer = kind == UserBadgeKind::Pointer; + // Choose SVG template based on pointer flag let svg_template = if pointer { // Pointer template with hand cursor @@ -285,13 +307,109 @@ pub fn render_user_badge_to_png( .map_err(|e| SvgRenderError::PngSaveError(e.to_string())) } +fn render_pencil_user_badge_to_png( + color: &str, + name: &str, + box_width: f32, + text_x: f32, + svg_width: f32, + svg_height: f32, + scale_factor: f32, + fontdb: std::sync::Arc, +) -> Result, SvgRenderError> { + let svg_template = format!( + r##" + + + + + + + + + + +{name} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"##, + color = color, + name = name, + box_width = box_width, + box_width_stroke = box_width - 1.10065, + filter_width = box_width + 16.5097 * 2.0, + text_x = text_x, + svg_width = svg_width, + svg_height = svg_height, + scale_factor = scale_factor, + ); + + let usvg_options = usvg::Options { + fontdb, + ..Default::default() + }; + let tree = usvg::Tree::from_str(&svg_template, &usvg_options) + .map_err(|e| SvgRenderError::SvgParseError(e.to_string()))?; + + let svg_size = tree.size(); + let width = (svg_size.width() * scale_factor) as u32; + let height = (svg_size.height() * scale_factor) as u32; + println!("SVG size: {}x{}", width, height); + + let mut pixmap = + tiny_skia::Pixmap::new(width, height).ok_or(SvgRenderError::PixmapCreationError)?; + + resvg::render(&tree, tiny_skia::Transform::default(), &mut pixmap.as_mut()); + + pixmap + .encode_png() + .map_err(|e| SvgRenderError::PngSaveError(e.to_string())) +} + #[cfg(test)] mod tests { use super::*; #[test] fn test_render_user_badge_to_png() { - let png_data = render_user_badge_to_png("#FF5733", "Alice", false).unwrap(); + let png_data = render_user_badge_to_png("#FF5733", "Alice", UserBadgeKind::Normal).unwrap(); // Verify it's valid PNG data by checking PNG signature assert_eq!(&png_data[0..8], &[137, 80, 78, 71, 13, 10, 26, 10]); @@ -300,7 +418,8 @@ mod tests { assert!(png_data.len() > 100); // Test with different parameters - let png_data2 = render_user_badge_to_png("#00FF00", "Bob Doe", false).unwrap(); + let png_data2 = + render_user_badge_to_png("#00FF00", "Bob Doe", UserBadgeKind::Normal).unwrap(); assert_eq!(&png_data2[0..8], &[137, 80, 78, 71, 13, 10, 26, 10]); assert!(png_data2.len() > 100); @@ -330,14 +449,20 @@ mod tests { #[test] fn test_different_name_lengths() { // Test badges with different name lengths (now with dynamic box width) - let very_short_badge = render_user_badge_to_png("#0040FF", "Me", false).unwrap(); - let short_badge = render_user_badge_to_png("#0040FF", "Joe", false).unwrap(); - let medium_badge = render_user_badge_to_png("#0040FF", "Alice Doe", false).unwrap(); - let long_badge = render_user_badge_to_png("#0040FF", "Iason Parask", false).unwrap(); + let very_short_badge = + render_user_badge_to_png("#0040FF", "Me", UserBadgeKind::Normal).unwrap(); + let short_badge = + render_user_badge_to_png("#0040FF", "Joe", UserBadgeKind::Normal).unwrap(); + let medium_badge = + render_user_badge_to_png("#0040FF", "Alice Doe", UserBadgeKind::Normal).unwrap(); + let long_badge = + render_user_badge_to_png("#0040FF", "Iason Parask", UserBadgeKind::Normal).unwrap(); let extra_long_badge = - render_user_badge_to_png("#0040FF", "AlexanderGGGGGGGGGGG", false).unwrap(); + render_user_badge_to_png("#0040FF", "AlexanderGGGGGGGGGGG", UserBadgeKind::Normal) + .unwrap(); let extra_long_badge_two = - render_user_badge_to_png("#0040FF", "Lykourgos Mpezentakos", false).unwrap(); + render_user_badge_to_png("#0040FF", "Lykourgos Mpezentakos", UserBadgeKind::Normal) + .unwrap(); // All should generate valid PNG data assert_eq!(&short_badge[0..8], &[137, 80, 78, 71, 13, 10, 26, 10]); @@ -357,7 +482,8 @@ mod tests { #[test] fn test_pointer_badge() { // Test the pointer template - let pointer_badge = render_user_badge_to_png("#0040FF", "Costa", true).unwrap(); + let pointer_badge = + render_user_badge_to_png("#0040FF", "Costa", UserBadgeKind::Pointer).unwrap(); // Verify it's valid PNG data by checking PNG signature assert_eq!(&pointer_badge[0..8], &[137, 80, 78, 71, 13, 10, 26, 10]); @@ -366,7 +492,8 @@ mod tests { assert!(pointer_badge.len() > 100); // Test regular badge for comparison - let regular_badge = render_user_badge_to_png("#0040FF", "Costa", false).unwrap(); + let regular_badge = + render_user_badge_to_png("#0040FF", "Costa", UserBadgeKind::Normal).unwrap(); // The two images should be different (different templates) assert_ne!(pointer_badge, regular_badge); @@ -374,4 +501,21 @@ mod tests { // Save example for visual inspection std::fs::write("test_pointer_badge.png", pointer_badge).unwrap(); } + + #[test] + fn test_pencil_badge() { + let pencil_badge = + render_user_badge_to_png("#0040FF", "Costa", UserBadgeKind::Pencil).unwrap(); + + assert_eq!(&pencil_badge[0..8], &[137, 80, 78, 71, 13, 10, 26, 10]); + assert!(pencil_badge.len() > 100); + + let pointer_badge = + render_user_badge_to_png("#0040FF", "Costa", UserBadgeKind::Pointer).unwrap(); + let regular_badge = + render_user_badge_to_png("#0040FF", "Costa", UserBadgeKind::Normal).unwrap(); + + assert_ne!(pencil_badge, pointer_badge); + assert_ne!(pencil_badge, regular_badge); + } } From d0c53c35baab28ab2a99e454f92e13876d6fabea Mon Sep 17 00:00:00 2001 From: theodkp Date: Sat, 6 Jun 2026 13:40:30 +0100 Subject: [PATCH 2/9] refactor: share cursor badge layout --- core/src/utils/svg_renderer.rs | 123 +++++++++++++++++++-------------- 1 file changed, 70 insertions(+), 53 deletions(-) diff --git a/core/src/utils/svg_renderer.rs b/core/src/utils/svg_renderer.rs index 75242f67..da57eb48 100644 --- a/core/src/utils/svg_renderer.rs +++ b/core/src/utils/svg_renderer.rs @@ -25,6 +25,48 @@ pub enum UserBadgeKind { Pencil, } +#[derive(Debug, Clone, Copy)] +struct BadgeLayout { + box_width: f32, + box_width_stroke: f32, + filter_width: f32, + text_x: f32, + svg_width: f32, + svg_height: f32, + scale_factor: f32, +} + +impl BadgeLayout { + fn new(kind: UserBadgeKind, box_width: f32, scale_factor: f32) -> Self { + let is_pointer = kind == UserBadgeKind::Pointer; + let text_x = if is_pointer { + 16.5317 + 6.0 + } else { + 18.6445 + 6.0 + }; + let svg_width = if is_pointer { + (box_width + 34.0) * scale_factor + } else { + (box_width + 36.0) * scale_factor + }; + let svg_height = if is_pointer { + 74.0 * scale_factor + } else { + 75.0 * scale_factor + }; + + Self { + box_width, + box_width_stroke: box_width - 1.10065, + filter_width: box_width + 16.5097 * 2.0, + text_x, + svg_width, + svg_height, + scale_factor, + } + } +} + /// Calculate dynamic box width based on text length /// Increases box width for longer text to ensure it fits comfortably fn calculate_box_width(text: &str) -> f32 { @@ -110,31 +152,10 @@ pub fn render_user_badge_to_png( name = name.chars().take(17).collect::() + "..."; }; - // Calculate text x position with left padding - // For pointer template: rect starts at x=16.5317, original text at x=22.5317 (6px padding) - // For regular template: rect starts at x=18.6445, original text at x=24.6445 (6px padding) - let text_x_pointer = 16.5317 + 6.0; - let text_x_regular = 18.6445 + 6.0; - - // Calculate dynamic SVG dimensions based on box_width - // Original: box_width=70, viewBox width=104 (pointer) or 106 (regular) - // The difference accounts for the rect x position + padding on the right - let svg_width_pointer = (box_width + 34.0) * scale_factor; // 16.5317 (left margin) + 70 (original box) + 17.4683 (right margin) ≈ 104 - let svg_width_regular = (box_width + 36.0) * scale_factor; // 18.6445 (left margin) + 70 (original box) + 17.3555 (right margin) ≈ 106 - let svg_height_pointer = 74.0 * scale_factor; - let svg_height_regular = 75.0 * scale_factor; + let layout = BadgeLayout::new(kind, box_width, scale_factor); if kind == UserBadgeKind::Pencil { - return render_pencil_user_badge_to_png( - color, - &name, - box_width, - text_x_regular, - svg_width_regular, - svg_height_regular, - scale_factor, - fontdb, - ); + return render_pencil_user_badge_to_png(color, &name, layout, fontdb); } let pointer = kind == UserBadgeKind::Pointer; @@ -210,13 +231,13 @@ pub fn render_user_badge_to_png( "##, color = color, name = name, - box_width = box_width, - box_width_stroke = box_width - 1.10065, - filter_width = box_width + 16.5097 * 2.0, - text_x = text_x_pointer, - svg_width = svg_width_pointer, - svg_height = svg_height_pointer, - scale_factor = scale_factor, + box_width = layout.box_width, + box_width_stroke = layout.box_width_stroke, + filter_width = layout.filter_width, + text_x = layout.text_x, + svg_width = layout.svg_width, + svg_height = layout.svg_height, + scale_factor = layout.scale_factor, ) } else { // Regular cursor template @@ -269,13 +290,13 @@ pub fn render_user_badge_to_png( "##, color = color, name = name, - box_width = box_width, - box_width_stroke = box_width - 1.10065, - filter_width = box_width + 16.5097 * 2.0, - text_x = text_x_regular, - svg_width = svg_width_regular, - svg_height = svg_height_regular, - scale_factor = scale_factor, + box_width = layout.box_width, + box_width_stroke = layout.box_width_stroke, + filter_width = layout.filter_width, + text_x = layout.text_x, + svg_width = layout.svg_width, + svg_height = layout.svg_height, + scale_factor = layout.scale_factor, ) }; @@ -289,8 +310,8 @@ pub fn render_user_badge_to_png( // Get the SVG size (which is now already scaled by the SVG attributes) let svg_size = tree.size(); - let width = (svg_size.width() * scale_factor) as u32; - let height = (svg_size.height() * scale_factor) as u32; + let width = (svg_size.width() * layout.scale_factor) as u32; + let height = (svg_size.height() * layout.scale_factor) as u32; // Print the size that it will render println!("SVG size: {}x{}", width, height); @@ -310,11 +331,7 @@ pub fn render_user_badge_to_png( fn render_pencil_user_badge_to_png( color: &str, name: &str, - box_width: f32, - text_x: f32, - svg_width: f32, - svg_height: f32, - scale_factor: f32, + layout: BadgeLayout, fontdb: std::sync::Arc, ) -> Result, SvgRenderError> { let svg_template = format!( @@ -372,13 +389,13 @@ fn render_pencil_user_badge_to_png( "##, color = color, name = name, - box_width = box_width, - box_width_stroke = box_width - 1.10065, - filter_width = box_width + 16.5097 * 2.0, - text_x = text_x, - svg_width = svg_width, - svg_height = svg_height, - scale_factor = scale_factor, + box_width = layout.box_width, + box_width_stroke = layout.box_width_stroke, + filter_width = layout.filter_width, + text_x = layout.text_x, + svg_width = layout.svg_width, + svg_height = layout.svg_height, + scale_factor = layout.scale_factor, ); let usvg_options = usvg::Options { @@ -389,8 +406,8 @@ fn render_pencil_user_badge_to_png( .map_err(|e| SvgRenderError::SvgParseError(e.to_string()))?; let svg_size = tree.size(); - let width = (svg_size.width() * scale_factor) as u32; - let height = (svg_size.height() * scale_factor) as u32; + let width = (svg_size.width() * layout.scale_factor) as u32; + let height = (svg_size.height() * layout.scale_factor) as u32; println!("SVG size: {}x{}", width, height); let mut pixmap = From 930b95bed64fc61ec0558f912ec11819caf706fd Mon Sep 17 00:00:00 2001 From: theodkp Date: Sat, 6 Jun 2026 14:15:10 +0100 Subject: [PATCH 3/9] fix: ignore unknown drawing mode for cursor state --- core/src/lib.rs | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/core/src/lib.rs b/core/src/lib.rs index 734e5478..bfb0d2a4 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1212,21 +1212,26 @@ impl<'a> ApplicationHandler for Application<'a> { UserEvent::DrawingMode(drawing_mode, sid) => { log::debug!("user_event: DrawingMode: {:?} {}", drawing_mode, sid); if let Some(remote_control) = &mut self.remote_control { - let visual_mode = match &drawing_mode { - DrawingMode::Draw(_) => Some(CursorMode::Pencil), - DrawingMode::ClickAnimation => Some(CursorMode::Pointer), - DrawingMode::Disabled | DrawingMode::Any => None, - }; let cursor_controller = &mut remote_control.cursor_controller; match &drawing_mode { - DrawingMode::Disabled => { - cursor_controller.set_controller_pointer(false, sid.as_str()); + DrawingMode::Draw(_) => { + cursor_controller.set_controller_pointer(true, sid.as_str()); + cursor_controller + .set_controller_visual_mode(sid.as_str(), Some(CursorMode::Pencil)); } - _ => { + DrawingMode::ClickAnimation => { cursor_controller.set_controller_pointer(true, sid.as_str()); + cursor_controller.set_controller_visual_mode( + sid.as_str(), + Some(CursorMode::Pointer), + ); + } + DrawingMode::Disabled => { + cursor_controller.set_controller_pointer(false, sid.as_str()); + cursor_controller.set_controller_visual_mode(sid.as_str(), None); } + DrawingMode::Any => {} } - cursor_controller.set_controller_visual_mode(sid.as_str(), visual_mode); remote_control .gfx .set_drawing_mode(sid.as_str(), drawing_mode.clone()); From 2d284d233d68adba9c5b97687e7bbf99988f0b2e Mon Sep 17 00:00:00 2001 From: theodkp Date: Sat, 6 Jun 2026 15:27:09 +0100 Subject: [PATCH 4/9] fix: redraw after cursor badge mode changes --- core/src/input/mouse.rs | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/core/src/input/mouse.rs b/core/src/input/mouse.rs index 1c3333ba..aa5f6cce 100644 --- a/core/src/input/mouse.rs +++ b/core/src/input/mouse.rs @@ -293,8 +293,12 @@ impl ControllerCursor { self.mode = mode; } - fn set_visual_mode_override(&mut self, mode: Option) { + fn set_visual_mode_override(&mut self, mode: Option) -> bool { + if self.visual_mode_override == mode { + return false; + } self.visual_mode_override = mode; + true } fn set_pointer_mode(&mut self, enabled: bool, remote_control_enabled: bool) { @@ -1042,11 +1046,24 @@ impl CursorController { pub fn set_controller_visual_mode(&mut self, identity: &str, mode: Option) { log::info!("set_controller_visual_mode: {identity} {mode:?}"); - let mut controllers_cursors = self.controllers_cursors.lock().unwrap(); - for controller in controllers_cursors.iter_mut() { - if controller.identity == identity { - controller.set_visual_mode_override(mode); - break; + let changed = { + let mut controllers_cursors = self.controllers_cursors.lock().unwrap(); + let mut changed = false; + for controller in controllers_cursors.iter_mut() { + if controller.identity == identity { + changed = controller.set_visual_mode_override(mode); + break; + } + } + changed + }; + + if changed { + if let Err(e) = self + .redraw_thread_sender + .send(RedrawThreadCommands::Activity) + { + log::error!("set_controller_visual_mode: error sending redraw event: {e:?}"); } } } From d2c1464f4143afac83d5a86502cf43f4f26459f5 Mon Sep 17 00:00:00 2001 From: theodkp Date: Sat, 6 Jun 2026 20:11:52 +0100 Subject: [PATCH 5/9] fix: align remote pencil cursor tip --- core/src/graphics/cursor.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/core/src/graphics/cursor.rs b/core/src/graphics/cursor.rs index acffb9df..79e5132c 100644 --- a/core/src/graphics/cursor.rs +++ b/core/src/graphics/cursor.rs @@ -10,6 +10,9 @@ use iced::widget::canvas::Frame; use iced::Rectangle; use std::time::{Duration, Instant}; +const PENCIL_BADGE_LOGICAL_HEIGHT: f32 = 75.0; +const PENCIL_TIP_LOGICAL: (f32, f32) = (1.2, 13.5); + /// Cursor display mode #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CursorMode { @@ -32,6 +35,8 @@ pub struct Cursor { pointer_cursor: (iced_core::image::Handle, (f32, f32)), /// (handle, (width, height)) for pencil cursor pencil_cursor: (iced_core::image::Handle, (f32, f32)), + /// Offset from the pencil badge top-left to the visible pencil tip + pencil_hotspot: (f32, f32), /// Current position of the cursor position: Option, /// Current cursor display mode @@ -78,12 +83,18 @@ impl Cursor { pencil_dims.height() as f32 / 2.5, ), ); + let pencil_scale = pencil_cursor.1 .1 / PENCIL_BADGE_LOGICAL_HEIGHT; + let pencil_hotspot = ( + PENCIL_TIP_LOGICAL.0 * pencil_scale, + PENCIL_TIP_LOGICAL.1 * pencil_scale, + ); Ok(Self { visible_name: name.to_string(), normal_cursor, pointer_cursor, pencil_cursor, + pencil_hotspot, position: None, mode: CursorMode::Normal, last_update: None, @@ -131,13 +142,17 @@ impl Cursor { CursorMode::Pencil => &self.pencil_cursor, CursorMode::Normal => &self.normal_cursor, }; + let (hotspot_x, hotspot_y) = match self.mode { + CursorMode::Pencil => self.pencil_hotspot, + _ => (0.0, 0.0), + }; let image = iced_core::image::Image::new(handle.clone()); let position = translate(self.position.unwrap()); frame.draw_image( Rectangle { - x: position.x as f32, - y: position.y as f32, + x: position.x as f32 - hotspot_x, + y: position.y as f32 - hotspot_y, width: *width, height: *height, }, From 27a614a28a126ca8974409724100bd61551f6dec Mon Sep 17 00:00:00 2001 From: theodkp Date: Sat, 6 Jun 2026 20:17:52 +0100 Subject: [PATCH 6/9] revert: remove cursor badge redraw trigger --- core/src/input/mouse.rs | 29 ++++++----------------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/core/src/input/mouse.rs b/core/src/input/mouse.rs index aa5f6cce..1c3333ba 100644 --- a/core/src/input/mouse.rs +++ b/core/src/input/mouse.rs @@ -293,12 +293,8 @@ impl ControllerCursor { self.mode = mode; } - fn set_visual_mode_override(&mut self, mode: Option) -> bool { - if self.visual_mode_override == mode { - return false; - } + fn set_visual_mode_override(&mut self, mode: Option) { self.visual_mode_override = mode; - true } fn set_pointer_mode(&mut self, enabled: bool, remote_control_enabled: bool) { @@ -1046,24 +1042,11 @@ impl CursorController { pub fn set_controller_visual_mode(&mut self, identity: &str, mode: Option) { log::info!("set_controller_visual_mode: {identity} {mode:?}"); - let changed = { - let mut controllers_cursors = self.controllers_cursors.lock().unwrap(); - let mut changed = false; - for controller in controllers_cursors.iter_mut() { - if controller.identity == identity { - changed = controller.set_visual_mode_override(mode); - break; - } - } - changed - }; - - if changed { - if let Err(e) = self - .redraw_thread_sender - .send(RedrawThreadCommands::Activity) - { - log::error!("set_controller_visual_mode: error sending redraw event: {e:?}"); + let mut controllers_cursors = self.controllers_cursors.lock().unwrap(); + for controller in controllers_cursors.iter_mut() { + if controller.identity == identity { + controller.set_visual_mode_override(mode); + break; } } } From fa39326ff943a2a28579a4e82447c01641814b2c Mon Sep 17 00:00:00 2001 From: theodkp Date: Sat, 6 Jun 2026 20:42:24 +0100 Subject: [PATCH 7/9] fix: keep drawing mode separate from cursor visuals --- core/src/graphics/participant.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/core/src/graphics/participant.rs b/core/src/graphics/participant.rs index 527b35f9..fb45ce54 100644 --- a/core/src/graphics/participant.rs +++ b/core/src/graphics/participant.rs @@ -248,15 +248,7 @@ impl ParticipantsManager { mode ); if let Some(participant) = self.participants.get_mut(identity) { - participant.draw_mut().set_mode(mode.clone()); - match mode { - DrawingMode::Draw(_) => participant.cursor_mut().set_mode(CursorMode::Pencil), - DrawingMode::ClickAnimation => { - participant.cursor_mut().set_mode(CursorMode::Pointer) - } - DrawingMode::Disabled => participant.cursor_mut().set_mode(CursorMode::Normal), - DrawingMode::Any => {} - } + participant.draw_mut().set_mode(mode); } else { log::warn!( "ParticipantsManager::set_drawing_mode: participant {} not found", From 8f2a0b7b3a2bae61960b0a3d8888b3d8357e890a Mon Sep 17 00:00:00 2001 From: Iason Paraskevopoulos Date: Mon, 29 Jun 2026 18:20:14 +0100 Subject: [PATCH 8/9] feat: set pencil pointer mode in the screensharing window --- core/src/window/screensharing_window.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/core/src/window/screensharing_window.rs b/core/src/window/screensharing_window.rs index a4ac07a4..edbd14c8 100644 --- a/core/src/window/screensharing_window.rs +++ b/core/src/window/screensharing_window.rs @@ -48,7 +48,9 @@ use crate::components::segmented_control::{ self as seg_ctrl_mod, SegmentedButton, SegmentedControlAnim, }; use crate::graphics::graphics_context::click_animation::ClickAnimationRenderer; -use crate::graphics::graphics_context::participant::{ParticipantError, ParticipantsManager}; +use crate::graphics::graphics_context::participant::{ + CursorMode, ParticipantError, ParticipantsManager, +}; use crate::graphics::graphics_window_context::{ ContextManager, GraphicsWindowContext, GraphicsWindowContextError, }; @@ -814,6 +816,15 @@ impl ScreensharingWindow { } pub fn set_drawing_mode(&mut self, identity: &str, mode: crate::room_service::DrawingMode) { + let cursor_mode = match &mode { + crate::room_service::DrawingMode::Draw(_) => CursorMode::Pencil, + crate::room_service::DrawingMode::ClickAnimation => CursorMode::Pointer, + crate::room_service::DrawingMode::Disabled | crate::room_service::DrawingMode::Any => { + CursorMode::Normal + } + }; + self.participants_manager + .set_cursor_mode(identity, cursor_mode); self.participants_manager.set_drawing_mode(identity, mode); } From 86ddc6f7d6c4d9dc6e2ecfea23d7d4a60cc83e08 Mon Sep 17 00:00:00 2001 From: Iason Paraskevopoulos Date: Mon, 29 Jun 2026 22:28:49 +0100 Subject: [PATCH 9/9] improvement: merge visual mode with mode --- core/src/input/mouse.rs | 99 +++++++++++++++-------------------------- core/src/lib.rs | 14 ++---- 2 files changed, 40 insertions(+), 73 deletions(-) diff --git a/core/src/input/mouse.rs b/core/src/input/mouse.rs index 1c3333ba..492c2e98 100644 --- a/core/src/input/mouse.rs +++ b/core/src/input/mouse.rs @@ -231,7 +231,6 @@ impl CursorState { } struct ControllerCursor { - /// Cursor state cursor_state: CursorState, /* * This is used to record when the controller @@ -240,20 +239,20 @@ struct ControllerCursor { */ clicked: bool, mode: CursorMode, - visual_mode_override: Option, - pointer_mode: bool, + /// Caches mode before controllers are globally disabled. + /// `Some` = controllers disabled, `None` = controllers enabled. + cached_mode: Option, has_control: bool, identity: String, } impl ControllerCursor { - fn new(cursor_state: CursorState, identity: String, mode: CursorMode) -> Self { + fn new(cursor_state: CursorState, identity: String) -> Self { Self { cursor_state, clicked: false, - mode, - visual_mode_override: None, - pointer_mode: mode == CursorMode::Pointer, + mode: CursorMode::Normal, + cached_mode: None, has_control: false, identity, } @@ -285,29 +284,25 @@ impl ControllerCursor { self.mode } - fn visual_mode(&self) -> CursorMode { - self.visual_mode_override.unwrap_or(self.mode) - } - fn set_mode(&mut self, mode: CursorMode) { - self.mode = mode; - } - - fn set_visual_mode_override(&mut self, mode: Option) { - self.visual_mode_override = mode; + if self.cached_mode.is_some() { + self.cached_mode = Some(mode); + } else { + self.mode = mode; + } } - fn set_pointer_mode(&mut self, enabled: bool, remote_control_enabled: bool) { - if !enabled && remote_control_enabled { - self.mode = CursorMode::Normal; - } else { - self.mode = CursorMode::Pointer; + fn disable(&mut self) { + if self.cached_mode.is_none() { + self.cached_mode = Some(self.mode); } - self.pointer_mode = enabled; + self.mode = CursorMode::Pointer; } - fn pointer_mode(&self) -> bool { - self.pointer_mode + fn enable(&mut self) { + if let Some(cached) = self.cached_mode.take() { + self.mode = cached; + } } fn clicked(&mut self) -> bool { @@ -674,16 +669,14 @@ impl CursorController { } } - let mode = if self.controllers_cursors_enabled { - CursorMode::Normal - } else { - CursorMode::Pointer - }; - controllers_cursors.push(ControllerCursor::new( + let mut controller = ControllerCursor::new( CursorState::new(self.redraw_thread_sender.clone(), self.clock.clone()), identity, - mode, - )); + ); + if !self.controllers_cursors_enabled { + controller.disable(); + } + controllers_cursors.push(controller); } /// Removes a remote controller from the cursor management system. @@ -967,13 +960,9 @@ impl CursorController { let mut any_had_control = false; for controller in controllers_cursors.iter_mut() { if enabled { - if controller.pointer_mode() { - controller.set_mode(CursorMode::Pointer); - } else { - controller.set_mode(CursorMode::Normal); - } + controller.enable(); } else { - controller.set_mode(CursorMode::Pointer); + controller.disable(); } if controller.has_control() { @@ -996,13 +985,11 @@ impl CursorController { } } - /// Switch pointer mode made by the controller. - /// # Parameters + /// Sets the cursor mode for a specific controller. /// - /// * `identity` - Session ID identifying which controller to modify - /// * `enabled` - Whether to enable (true) or disable (false) pointer mode for the specified controller - pub fn set_controller_pointer(&mut self, enabled: bool, identity: &str) { - log::info!("set_controller_pointer: {identity} {enabled}"); + /// If the controller currently has control, it is given back to the sharer. + pub fn set_controller_mode(&mut self, identity: &str, mode: CursorMode) { + log::info!("set_controller_mode: {identity} {mode:?}"); let had_control = { let mut controllers_cursors = self.controllers_cursors.lock().unwrap(); @@ -1013,12 +1000,14 @@ impl CursorController { } if controller.has_control() { - log::info!("set_controller_pointer: controller {identity} has control, give control back to sharer."); + log::info!( + "set_controller_mode: controller {identity} has control, giving back to sharer." + ); controller.show(); had_control = true; } - controller.set_pointer_mode(enabled, self.controllers_cursors_enabled); + controller.set_mode(mode); break; } had_control @@ -1036,21 +1025,6 @@ impl CursorController { } } - /// Overrides only the rendered cursor badge for a controller. - /// - /// This keeps input-routing mode independent from drawing/click visual state. - pub fn set_controller_visual_mode(&mut self, identity: &str, mode: Option) { - log::info!("set_controller_visual_mode: {identity} {mode:?}"); - - let mut controllers_cursors = self.controllers_cursors.lock().unwrap(); - for controller in controllers_cursors.iter_mut() { - if controller.identity == identity { - controller.set_visual_mode_override(mode); - break; - } - } - } - /// Updates cursor positions in the ParticipantsManager for rendering. /// /// This function translates cursor state to pixel positions and updates the @@ -1081,8 +1055,7 @@ impl CursorController { if controller.visible() { participants_manager .set_cursor_position(&controller.identity, Some(controller.local_position())); - participants_manager - .set_cursor_mode(&controller.identity, controller.visual_mode()); + participants_manager.set_cursor_mode(&controller.identity, controller.mode()); } else { participants_manager.set_cursor_position(&controller.identity, None); } diff --git a/core/src/lib.rs b/core/src/lib.rs index c8bc992b..8befd7ce 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1262,20 +1262,14 @@ impl<'a> ApplicationHandler for Application<'a> { let cursor_controller = &mut remote_control.cursor_controller; match &drawing_mode { DrawingMode::Draw(_) => { - cursor_controller.set_controller_pointer(true, sid.as_str()); - cursor_controller - .set_controller_visual_mode(sid.as_str(), Some(CursorMode::Pencil)); + cursor_controller.set_controller_mode(sid.as_str(), CursorMode::Pencil); } DrawingMode::ClickAnimation => { - cursor_controller.set_controller_pointer(true, sid.as_str()); - cursor_controller.set_controller_visual_mode( - sid.as_str(), - Some(CursorMode::Pointer), - ); + cursor_controller + .set_controller_mode(sid.as_str(), CursorMode::Pointer); } DrawingMode::Disabled => { - cursor_controller.set_controller_pointer(false, sid.as_str()); - cursor_controller.set_controller_visual_mode(sid.as_str(), None); + cursor_controller.set_controller_mode(sid.as_str(), CursorMode::Normal); } DrawingMode::Any => {} }