diff --git a/core/src/graphics/cursor.rs b/core/src/graphics/cursor.rs index b47e7361..79e5132c 100644 --- a/core/src/graphics/cursor.rs +++ b/core/src/graphics/cursor.rs @@ -5,11 +5,14 @@ //! 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}; +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 { @@ -17,6 +20,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 +33,10 @@ 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)), + /// 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 @@ -39,8 +48,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 +58,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 +76,25 @@ 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, + ), + ); + 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, @@ -112,15 +139,20 @@ impl Cursor { let (handle, (width, height)) = match self.mode { CursorMode::Pointer => &self.pointer_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, }, diff --git a/core/src/input/mouse.rs b/core/src/input/mouse.rs index c0578362..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,18 +239,20 @@ struct ControllerCursor { */ clicked: bool, mode: CursorMode, - 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, - pointer_mode: mode == CursorMode::Pointer, + mode: CursorMode::Normal, + cached_mode: None, has_control: false, identity, } @@ -284,20 +285,24 @@ impl ControllerCursor { } fn set_mode(&mut self, mode: CursorMode) { - self.mode = 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 { @@ -664,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. @@ -957,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() { @@ -986,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(); @@ -1003,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 diff --git a/core/src/lib.rs b/core/src/lib.rs index 4e3c0338..8befd7ce 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; @@ -1258,17 +1259,19 @@ impl<'a> ApplicationHandler for Application<'a> { if let (Some(window_manager), Some(remote_control)) = (self.window_manager.as_mut(), self.remote_control.as_mut()) { + let cursor_controller = &mut remote_control.cursor_controller; match &drawing_mode { - DrawingMode::Disabled => { - remote_control - .cursor_controller - .set_controller_pointer(false, sid.as_str()); + DrawingMode::Draw(_) => { + cursor_controller.set_controller_mode(sid.as_str(), CursorMode::Pencil); + } + DrawingMode::ClickAnimation => { + cursor_controller + .set_controller_mode(sid.as_str(), CursorMode::Pointer); } - _ => { - remote_control - .cursor_controller - .set_controller_pointer(true, sid.as_str()); + DrawingMode::Disabled => { + cursor_controller.set_controller_mode(sid.as_str(), CursorMode::Normal); } + DrawingMode::Any => {} } if let Some(gfx) = window_manager.active_gfx_mut() { 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..da57eb48 100644 --- a/core/src/utils/svg_renderer.rs +++ b/core/src/utils/svg_renderer.rs @@ -18,6 +18,55 @@ pub enum SvgRenderError { PngSaveError(String), } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UserBadgeKind { + Normal, + Pointer, + 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 { @@ -80,7 +129,7 @@ fn get_box_width(text: &str, fontdb: std::sync::Arc) -> Result Result, SvgRenderError> { // Create font database let mut fontdb = Database::new(); @@ -103,19 +152,13 @@ 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; + let layout = BadgeLayout::new(kind, box_width, scale_factor); + + if kind == UserBadgeKind::Pencil { + return render_pencil_user_badge_to_png(color, &name, layout, fontdb); + } - // 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 pointer = kind == UserBadgeKind::Pointer; // Choose SVG template based on pointer flag let svg_template = if pointer { @@ -188,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 @@ -247,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, ) }; @@ -267,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); @@ -285,13 +328,105 @@ 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, + layout: BadgeLayout, + fontdb: std::sync::Arc, +) -> Result, SvgRenderError> { + let svg_template = format!( + r##" + + + + + + + + + + +{name} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"##, + color = color, + name = name, + 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 { + 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() * layout.scale_factor) as u32; + let height = (svg_size.height() * layout.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 +435,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 +466,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 +499,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 +509,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 +518,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); + } } 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); }