From bc70ff7af8c8b3faa8f5581b1291a0fc9c03a60b Mon Sep 17 00:00:00 2001 From: Iason Paraskevopoulos Date: Sun, 25 Jan 2026 13:11:07 +0000 Subject: [PATCH 01/10] feat(core): implement sharer drawing --- core/resources/pencil.png | Bin 0 -> 841 bytes core/socket_lib/src/lib.rs | 1 + core/src/graphics/draw.rs | 70 ++++++- core/src/graphics/graphics_context.rs | 13 +- core/src/graphics/iced_canvas.rs | 8 +- core/src/graphics/iced_renderer.rs | 8 +- core/src/lib.rs | 269 +++++++++++++++++++++++++- core/src/overlay_window.rs | 7 + core/src/room_service.rs | 175 +++++++++++++++++ core/tests/src/local_drawing.rs | 24 +++ core/tests/src/main.rs | 8 + 11 files changed, 569 insertions(+), 14 deletions(-) create mode 100644 core/resources/pencil.png create mode 100644 core/tests/src/local_drawing.rs diff --git a/core/resources/pencil.png b/core/resources/pencil.png new file mode 100644 index 0000000000000000000000000000000000000000..b301d8816772060399989f8cf593adc4e6f0526f GIT binary patch literal 841 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjjKx9jP7LeL$-D$|T2doC(|mmy zw18|523AHP24;{FAY@>aVqgWc85q16rQz%#Mh&PMCI*J~Oa>OHnkXO*0vxV%QuQiw3qZOUY$~jP%-qzHM1_jnoV;SI3R@+x3M(KRB&@Hb09I0xZL1XF8=&Bv zUzDm~re~mMpk&9TprBw=l#*r@P?Wt5Z@Sn2DRmzV368|&p4rRy77T3YHG z80i}s=>k>g7FXt#Bv$C=6)QswftllyTAW;zSx}OhpQivaH!&%{w8U0P31kr*K-^i9 znTD__uNdkrpa=CqGWv#k2KsQbfm&@qqE`MznW;dVLFU^T+JIG}h(YbK(Fa+M9avuEAz1d+B-a8&t;ucLK6U>qXUQl literal 0 HcmV?d00001 diff --git a/core/socket_lib/src/lib.rs b/core/socket_lib/src/lib.rs index 188261b1..d7968c2d 100644 --- a/core/socket_lib/src/lib.rs +++ b/core/socket_lib/src/lib.rs @@ -120,6 +120,7 @@ pub enum Message { ControllerCursorEnabled(bool), LivekitServerUrl(String), SentryMetadata(SentryMetadata), + DrawingEnabled(bool), } #[derive(Debug)] diff --git a/core/src/graphics/draw.rs b/core/src/graphics/draw.rs index e5bc7fe9..90a0d4cc 100644 --- a/core/src/graphics/draw.rs +++ b/core/src/graphics/draw.rs @@ -1,10 +1,13 @@ use std::collections::HashMap; +use std::time::{Duration, Instant}; use iced::widget::canvas::{path, stroke, Cache, Frame, Geometry, Stroke}; use iced::{Color, Point, Rectangle, Renderer}; use crate::{room_service::DrawingMode, utils::geometry::Position}; +const PATH_EXPIRY_DURATION: Duration = Duration::from_secs(3); + fn color_from_hex(hex: &str) -> Color { let hex = hex.trim_start_matches('#'); @@ -27,6 +30,7 @@ fn color_from_hex(hex: &str) -> Color { struct DrawPath { path_id: u64, points: Vec, + finished_at: Option, } impl DrawPath { @@ -34,6 +38,7 @@ impl DrawPath { Self { path_id, points: vec![point], + finished_at: None, } } } @@ -44,6 +49,7 @@ pub struct Draw { completed_cache: Cache, mode: DrawingMode, color: Color, + auto_clear: bool, } impl std::fmt::Debug for Draw { @@ -58,13 +64,14 @@ impl std::fmt::Debug for Draw { } impl Draw { - pub fn new(color: &str) -> Self { + pub fn new(color: &str, auto_clear: bool) -> Self { Self { in_progress_path: None, completed_paths: Vec::new(), completed_cache: Cache::new(), mode: DrawingMode::Disabled, color: color_from_hex(color), + auto_clear, } } @@ -104,8 +111,9 @@ impl Draw { return; } - if let Some(in_progress_path) = self.in_progress_path.take() { + if let Some(mut in_progress_path) = self.in_progress_path.take() { log::info!("finish_path: finishing path {}", in_progress_path.path_id); + in_progress_path.finished_at = Some(Instant::now()); self.completed_paths.push(in_progress_path); self.completed_cache.clear(); } else { @@ -134,6 +142,42 @@ impl Draw { self.completed_cache.clear(); } + pub fn clear_expired_paths(&mut self) -> Vec { + if !self.auto_clear { + return Vec::new(); + } + + // Only clear in non-permanent mode + if let DrawingMode::Draw(settings) = &self.mode { + if settings.permanent { + return Vec::new(); + } + } else { + return Vec::new(); + } + + let now = Instant::now(); + let mut removed_ids = Vec::new(); + + self.completed_paths.retain(|path| { + if let Some(finished_at) = path.finished_at { + let should_keep = now.duration_since(finished_at) < PATH_EXPIRY_DURATION; + if !should_keep { + removed_ids.push(path.path_id); + } + should_keep + } else { + true + } + }); + + if !removed_ids.is_empty() { + self.completed_cache.clear(); + } + + removed_ids + } + /// Returns cached geometry for completed paths. pub fn draw_completed(&self, renderer: &Renderer, bounds: Rectangle) -> Geometry { self.completed_cache.draw(renderer, bounds.size(), |frame| { @@ -209,9 +253,14 @@ impl DrawManager { } /// Adds a new participant with their color. - pub fn add_participant(&mut self, sid: String, color: &str) { - log::info!("DrawManager::add_participant: sid={} color={}", sid, color); - self.draws.insert(sid, Draw::new(color)); + pub fn add_participant(&mut self, sid: String, color: &str, auto_clear: bool) { + log::info!( + "DrawManager::add_participant: sid={} color={} auto_clear={}", + sid, + color, + auto_clear + ); + self.draws.insert(sid, Draw::new(color, auto_clear)); } /// Removes a participant and their drawing data. @@ -299,6 +348,17 @@ impl DrawManager { } } + pub fn update_auto_clear(&mut self) -> Vec { + let mut removed_path_ids = Vec::new(); + + for draw in self.draws.values_mut() { + let removed = draw.clear_expired_paths(); + removed_path_ids.extend(removed); + } + + removed_path_ids + } + /// Renders all draws and returns the geometries. pub fn draw(&self, renderer: &Renderer, bounds: Rectangle) -> Vec { let mut geometries = Vec::with_capacity(self.draws.len() + 1); diff --git a/core/src/graphics/graphics_context.rs b/core/src/graphics/graphics_context.rs index 7728795f..d34e88dc 100644 --- a/core/src/graphics/graphics_context.rs +++ b/core/src/graphics/graphics_context.rs @@ -445,8 +445,9 @@ impl<'a> GraphicsContext<'a> { /// # Arguments /// * `sid` - Session ID identifying the participant /// * `color` - Hex color string for the participant's drawings - pub fn add_draw_participant(&mut self, sid: String, color: &str) { - self.iced_renderer.add_draw_participant(sid, color); + /// * `auto_clear` - Whether to automatically clear paths after 3 seconds (for local participant) + pub fn add_draw_participant(&mut self, sid: String, color: &str, auto_clear: bool) { + self.iced_renderer.add_draw_participant(sid, color, auto_clear); } /// Removes a participant from the draw manager. @@ -510,6 +511,14 @@ impl<'a> GraphicsContext<'a> { pub fn draw_clear_all_paths(&mut self, sid: &str) { self.iced_renderer.draw_clear_all_paths(sid); } + + /// Updates auto-clear for all participants and returns removed path IDs. + /// + /// # Returns + /// A vector of removed path IDs + pub fn update_auto_clear(&mut self) -> Vec { + self.iced_renderer.update_auto_clear() + } } impl Drop for GraphicsContext<'_> { diff --git a/core/src/graphics/iced_canvas.rs b/core/src/graphics/iced_canvas.rs index 56fc96a1..c7769489 100644 --- a/core/src/graphics/iced_canvas.rs +++ b/core/src/graphics/iced_canvas.rs @@ -67,8 +67,12 @@ impl OverlaySurface { .into() } - pub fn add_draw_participant(&mut self, sid: String, color: &str) { - self.draws.add_participant(sid, color); + pub fn update_auto_clear(&mut self) -> Vec { + self.draws.update_auto_clear() + } + + pub fn add_draw_participant(&mut self, sid: String, color: &str, auto_clear: bool) { + self.draws.add_participant(sid, color, auto_clear); } pub fn remove_draw_participant(&mut self, sid: &str) { diff --git a/core/src/graphics/iced_renderer.rs b/core/src/graphics/iced_renderer.rs index 361cc425..0062fe6c 100644 --- a/core/src/graphics/iced_renderer.rs +++ b/core/src/graphics/iced_renderer.rs @@ -101,8 +101,8 @@ impl IcedRenderer { wgpu_renderer.present(None, frame.texture.format(), view, &self.viewport); } - pub fn add_draw_participant(&mut self, sid: String, color: &str) { - self.overlay_surface.add_draw_participant(sid, color); + pub fn add_draw_participant(&mut self, sid: String, color: &str, auto_clear: bool) { + self.overlay_surface.add_draw_participant(sid, color, auto_clear); } pub fn remove_draw_participant(&mut self, sid: &str) { @@ -132,4 +132,8 @@ impl IcedRenderer { pub fn draw_clear_all_paths(&mut self, sid: &str) { self.overlay_surface.draw_clear_all_paths(sid); } + + pub fn update_auto_clear(&mut self) -> Vec { + self.overlay_surface.update_auto_clear() + } } diff --git a/core/src/lib.rs b/core/src/lib.rs index 986c0443..8d0a8918 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -26,6 +26,7 @@ pub(crate) mod overlay_window; use capture::capturer::{poll_stream, Capturer}; use graphics::graphics_context::GraphicsContext; +use image::GenericImageView; use input::clipboard::ClipboardController; use input::keyboard::{KeyboardController, KeyboardLayout}; use input::mouse::CursorController; @@ -129,6 +130,7 @@ struct RemoteControl<'a> { cursor_controller: CursorController, keyboard_controller: KeyboardController, clipboard_controller: Option, + pencil_cursor: winit::window::CustomCursor, } /// The main application struct that manages the entire remote desktop control session. @@ -186,6 +188,7 @@ pub struct Application<'a> { socket: CursorSocket, room_service: Option, event_loop_proxy: EventLoopProxy, + local_drawing: LocalDrawing, } #[derive(Error, Debug)] @@ -194,6 +197,21 @@ pub enum ApplicationError { RoomServiceError(#[from] std::io::Error), } +#[derive(Debug)] +struct LocalDrawing { + enabled: bool, + left_mouse_pressed: bool, + current_path_id: u64, + last_cursor_position: Option, + last_redraw_time: std::time::Instant, +} + +impl fmt::Display for LocalDrawing { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "LocalDrawing: enabled: {} left_mouse_pressed: {} current_path_id: {} last_cursor_position: {:?} last_redraw_time: {:?}", self.enabled, self.left_mouse_pressed, self.current_path_id, self.last_cursor_position, self.last_redraw_time) + } +} + impl<'a> Application<'a> { /// Creates a new Application instance with the specified configuration. /// @@ -233,6 +251,13 @@ impl<'a> Application<'a> { socket, room_service: None, event_loop_proxy, + local_drawing: LocalDrawing { + enabled: false, + left_mouse_pressed: false, + current_path_id: 0, + last_cursor_position: None, + last_redraw_time: std::time::Instant::now(), + }, }) } @@ -463,6 +488,34 @@ impl<'a> Application<'a> { } }; + // Add local participant to draw manager with auto-clear enabled + graphics_context.add_draw_participant("local".to_string(), "#FFDF20", true); + + // Load pencil cursor image once during window creation + let pencil_path = format!("{}/pencil.png", self.textures_path); + let pencil_image = image::open(&pencil_path).map_err(|e| { + log::error!("create_overlay_window: Failed to load pencil.png: {e:?}"); + ServerError::GfxCreationError + })?; + let rgba = pencil_image.to_rgba8(); + let (width, height) = pencil_image.dimensions(); + let hotspot_x = 0; // Pencil tip at top-left + let hotspot_y = height.saturating_sub(1); // Bottom of image (pencil tip) + + let custom_cursor_source = winit::window::CustomCursor::from_rgba( + rgba.into_raw(), + width as u16, + height as u16, + hotspot_x as u16, + hotspot_y as u16, + ) + .map_err(|e| { + log::error!("create_overlay_window: Failed to create cursor source: {e:?}"); + ServerError::GfxCreationError + })?; + + let pencil_cursor = event_loop.create_custom_cursor(custom_cursor_source); + /* Hardcode window frame to zero as we only support displays for now.*/ let window_frame = Frame::default(); let scaled = { @@ -518,6 +571,7 @@ impl<'a> Application<'a> { cursor_controller: cursor_controller.unwrap(), keyboard_controller: KeyboardController::::new(), clipboard_controller, + pencil_cursor, }); #[cfg(target_os = "linux")] @@ -796,7 +850,7 @@ impl<'a> ApplicationHandler for Application<'a> { } // Add participant to draw manager with their color if let Some(color) = remote_control.cursor_controller.get_participant_color(&sid) { - remote_control.gfx.add_draw_participant(sid, color); + remote_control.gfx.add_draw_participant(sid, color, false); } } UserEvent::ParticipantDisconnected(participant) => { @@ -1010,6 +1064,87 @@ impl<'a> ApplicationHandler for Application<'a> { .cursor_controller .trigger_click_animation(position, sid.as_str()); } + UserEvent::LocalDrawingEnabled(enabled) => { + log::debug!("user_event: LocalDrawingEnabled: {}", enabled); + if self.remote_control.is_none() { + log::warn!("user_event: remote control is none local drawing enabled"); + return; + } + + let remote_control = &mut self.remote_control.as_mut().unwrap(); + if enabled { + // Enable drawing mode + self.local_drawing.enabled = true; + + let window = remote_control.gfx.window(); + + // Enable cursor hittest so we can receive mouse events + if let Err(e) = window.set_cursor_hittest(true) { + log::error!("user_event: Failed to enable cursor hittest: {e:?}"); + return; + } + + window.set_cursor(winit::window::Cursor::Custom( + remote_control.pencil_cursor.clone(), + )); + + // Disable remote control + remote_control + .cursor_controller + .set_controllers_enabled(false); + remote_control.keyboard_controller.set_enabled(false); + + // Set drawing mode for local participant (non-permanent) + remote_control.gfx.set_drawing_mode( + "local", + room_service::DrawingMode::Draw(room_service::DrawSettings { + permanent: false, + }), + ); + + log::info!("Local drawing mode enabled"); + } else { + // Disable drawing mode + self.local_drawing.enabled = false; + self.local_drawing.left_mouse_pressed = false; + self.local_drawing.last_cursor_position = None; + + // Clear all local drawing paths + remote_control.gfx.draw_clear_all_paths("local"); + let cursor_controller = &mut remote_control.cursor_controller; + remote_control.gfx.draw(&cursor_controller); + + // Send LiveKit event to clear all paths + if let Some(room_service) = &self.room_service { + room_service.publish_draw_clear_all_paths(); + } + + let window = remote_control.gfx.window(); + + // Disable cursor hittest + if let Err(e) = window.set_cursor_hittest(false) { + log::error!("user_event: Failed to disable cursor hittest: {e:?}"); + } + + // Restore default cursor + window.set_cursor(winit::window::Cursor::Icon( + winit::window::CursorIcon::Default, + )); + + // Re-enable remote control + remote_control + .cursor_controller + .set_controllers_enabled(true); + remote_control.keyboard_controller.set_enabled(true); + + // Set drawing mode to disabled for local participant + remote_control + .gfx + .set_drawing_mode("local", room_service::DrawingMode::Disabled); + + log::info!("Local drawing mode disabled"); + } + } } } @@ -1034,8 +1169,134 @@ impl<'a> ApplicationHandler for Application<'a> { return; } let remote_control = &mut self.remote_control.as_mut().unwrap(); - let cursor_controller = &mut remote_control.cursor_controller; - remote_control.gfx.draw(cursor_controller); + + // Update auto-clear and send events for cleared paths + let cleared_path_ids = remote_control.gfx.update_auto_clear(); + if !cleared_path_ids.is_empty() && self.room_service.is_some() { + self.room_service + .as_ref() + .unwrap() + .publish_draw_clear_paths(cleared_path_ids); + } + + if !self.local_drawing.enabled { + let cursor_controller = &mut remote_control.cursor_controller; + remote_control.gfx.draw(cursor_controller); + } else { + if self.local_drawing.last_redraw_time.elapsed() > std::time::Duration::from_millis(33) { + let cursor_controller = &mut remote_control.cursor_controller; + remote_control.gfx.draw(cursor_controller); + self.local_drawing.last_redraw_time = std::time::Instant::now(); + } + remote_control.gfx.window().request_redraw(); + } + } + WindowEvent::MouseInput { state, button, .. } => { + if self.local_drawing.enabled && button == winit::event::MouseButton::Left { + if state == winit::event::ElementState::Pressed { + self.local_drawing.left_mouse_pressed = true; + // Start a new path if we have a cursor position + if let Some(position) = self.local_drawing.last_cursor_position { + if let Some(remote_control) = &mut self.remote_control { + self.local_drawing.current_path_id += 1; + remote_control.gfx.draw_start( + "local", + position, + self.local_drawing.current_path_id, + ); + remote_control.cursor_controller.trigger_render(); + + // Send LiveKit event + if let Some(room_service) = &self.room_service { + let overlay_window = + remote_control.cursor_controller.get_overlay_window(); + let normalized_point = overlay_window + .get_local_percentage_from_pixel(position.x, position.y); + room_service.publish_draw_start(room_service::DrawPathPoint { + point: room_service::ClientPoint { + x: normalized_point.x, + y: normalized_point.y, + }, + path_id: self.local_drawing.current_path_id, + }); + } + + log::debug!( + "Local draw_start at {:?} with path_id {}", + position, + self.local_drawing.current_path_id + ); + } + } + } else { + self.local_drawing.left_mouse_pressed = false; + // End the current path + if let Some(position) = self.local_drawing.last_cursor_position { + if let Some(remote_control) = &mut self.remote_control { + remote_control.gfx.draw_end("local", position); + remote_control.cursor_controller.trigger_render(); + + // Send LiveKit event + if let Some(room_service) = &self.room_service { + let overlay_window = + remote_control.cursor_controller.get_overlay_window(); + let normalized_point = overlay_window + .get_local_percentage_from_pixel(position.x, position.y); + room_service.publish_draw_end(room_service::ClientPoint { + x: normalized_point.x, + y: normalized_point.y, + }); + } + + log::debug!("Local draw_end at {:?}", position); + } + } + } + } + } + WindowEvent::CursorMoved { position, .. } => { + if self.local_drawing.enabled { + // Convert physical position to our Position type + let pos = Position { + x: position.x, + y: position.y, + }; + self.local_drawing.last_cursor_position = Some(pos); + + // If we're actively drawing, add a point + if self.local_drawing.left_mouse_pressed { + if let Some(remote_control) = &mut self.remote_control { + remote_control.gfx.draw_add_point("local", pos); + remote_control.cursor_controller.trigger_render(); + + // Send LiveKit event + if let Some(room_service) = &self.room_service { + let overlay_window = + remote_control.cursor_controller.get_overlay_window(); + let normalized_point = + overlay_window.get_local_percentage_from_pixel(pos.x, pos.y); + room_service.publish_draw_add_point(room_service::ClientPoint { + x: normalized_point.x, + y: normalized_point.y, + }); + } + } + } + } + } + WindowEvent::KeyboardInput { event, .. } => { + if self.local_drawing.enabled && event.state == winit::event::ElementState::Pressed + { + if let winit::keyboard::Key::Named(winit::keyboard::NamedKey::Escape) = + event.logical_key + { + // Disable drawing mode + let _ = self + .event_loop_proxy + .send_event(UserEvent::LocalDrawingEnabled(false)); + log::debug!("Escape pressed, disabling local drawing"); + } + } } _ => {} } @@ -1104,6 +1365,7 @@ pub enum UserEvent { DrawClearPath(u64, String), DrawClearAllPaths(String), ClickAnimationFromParticipant(room_service::ClientPoint, String), + LocalDrawingEnabled(bool), } pub struct RenderEventLoop { @@ -1199,6 +1461,7 @@ impl RenderEventLoop { Message::ControllerCursorEnabled(enabled) => { UserEvent::ControllerCursorEnabled(enabled) } + Message::DrawingEnabled(enabled) => UserEvent::LocalDrawingEnabled(enabled), // Ping is on purpose empty. We use it only for stopping the above receive to timeout. Message::Ping => { continue; diff --git a/core/src/overlay_window.rs b/core/src/overlay_window.rs index 85bcae09..f05e91a8 100644 --- a/core/src/overlay_window.rs +++ b/core/src/overlay_window.rs @@ -262,6 +262,13 @@ impl OverlayWindow { y: y * self.display_info.display_extent.height / self.display_info.display_scale, } } + + pub fn get_local_percentage_from_pixel(&self, x: f64, y: f64) -> Position { + Position { + x: (x * self.display_info.display_scale) / self.display_info.display_extent.width, + y: (y * self.display_info.display_scale) / self.display_info.display_extent.height, + } + } } impl fmt::Display for OverlayWindow { diff --git a/core/src/room_service.rs b/core/src/room_service.rs index 7bdda978..1ff398c6 100644 --- a/core/src/room_service.rs +++ b/core/src/room_service.rs @@ -20,6 +20,7 @@ const TOPIC_REMOTE_CONTROL_ENABLED: &str = "remote_control_enabled"; const TOPIC_PARTICIPANT_IN_CONTROL: &str = "participant_in_control"; const TOPIC_TICK_RESPONSE: &str = "tick_response"; const VIDEO_TRACK_NAME: &str = "screen_share"; +const TOPIC_DRAW: &str = "draw"; const MAX_FRAMERATE: f64 = 40.0; // Bitrate constants (in bits per second) @@ -55,6 +56,11 @@ enum RoomServiceCommand { TickResponse(u128), IterateParticipants, PublishParticipantInControl(String), + PublishDrawStart(DrawPathPoint), + PublishDrawAddPoint(ClientPoint), + PublishDrawEnd(ClientPoint), + PublishDrawClearPaths(Vec), + PublishDrawClearAllPaths, } #[derive(Debug)] @@ -335,6 +341,56 @@ impl RoomService { log::error!("publish_participant_in_control: Failed to send command: {e:?}"); } } + + pub fn publish_draw_start(&self, point: DrawPathPoint) { + log::debug!("publish_draw_start: {:?}", point); + let res = self + .service_command_tx + .send(RoomServiceCommand::PublishDrawStart(point)); + if let Err(e) = res { + log::error!("publish_draw_start: Error sending command: {e:?}"); + } + } + + pub fn publish_draw_add_point(&self, point: ClientPoint) { + log::debug!("publish_draw_add_point: {:?}", point); + let res = self + .service_command_tx + .send(RoomServiceCommand::PublishDrawAddPoint(point)); + if let Err(e) = res { + log::error!("publish_draw_add_point: Error sending command: {e:?}"); + } + } + + pub fn publish_draw_end(&self, point: ClientPoint) { + log::debug!("publish_draw_end: {:?}", point); + let res = self + .service_command_tx + .send(RoomServiceCommand::PublishDrawEnd(point)); + if let Err(e) = res { + log::error!("publish_draw_end: Error sending command: {e:?}"); + } + } + + pub fn publish_draw_clear_paths(&self, path_ids: Vec) { + log::debug!("publish_draw_clear_paths: {:?}", path_ids); + let res = self + .service_command_tx + .send(RoomServiceCommand::PublishDrawClearPaths(path_ids)); + if let Err(e) = res { + log::error!("publish_draw_clear_paths: Error sending command: {e:?}"); + } + } + + pub fn publish_draw_clear_all_paths(&self) { + log::debug!("publish_draw_clear_all_paths"); + let res = self + .service_command_tx + .send(RoomServiceCommand::PublishDrawClearAllPaths); + if let Err(e) = res { + log::error!("publish_draw_clear_all_paths: Error sending command: {e:?}"); + } + } } /// Handles room service commands in an async loop. @@ -661,6 +717,125 @@ async fn room_service_commands( ); } } + RoomServiceCommand::PublishDrawStart(point) => { + let room = inner.room.lock().await; + if room.is_none() { + log::warn!("room_service_commands: Room doesn't exist"); + continue; + } + let room = room.as_ref().unwrap(); + let local_participant = room.local_participant(); + let event = ClientEvent::DrawStart(point); + let payload = serde_json::to_vec(&event).unwrap(); + let res = local_participant + .publish_data(DataPacket { + payload, + reliable: true, + topic: Some(TOPIC_DRAW.to_string()), + ..Default::default() + }) + .await; + + if let Err(e) = res { + log::error!("room_service_commands: Failed to publish draw start: {e:?}"); + } + } + RoomServiceCommand::PublishDrawAddPoint(point) => { + let room = inner.room.lock().await; + if room.is_none() { + log::warn!("room_service_commands: Room doesn't exist"); + continue; + } + let room = room.as_ref().unwrap(); + let local_participant = room.local_participant(); + let event = ClientEvent::DrawAddPoint(point); + let payload = serde_json::to_vec(&event).unwrap(); + let res = local_participant + .publish_data(DataPacket { + payload, + reliable: false, + topic: Some(TOPIC_DRAW.to_string()), + ..Default::default() + }) + .await; + + if let Err(e) = res { + log::error!("room_service_commands: Failed to publish draw add point: {e:?}"); + } + } + RoomServiceCommand::PublishDrawEnd(point) => { + let room = inner.room.lock().await; + if room.is_none() { + log::warn!("room_service_commands: Room doesn't exist"); + continue; + } + let room = room.as_ref().unwrap(); + let local_participant = room.local_participant(); + let event = ClientEvent::DrawEnd(point); + let payload = serde_json::to_vec(&event).unwrap(); + let res = local_participant + .publish_data(DataPacket { + payload, + reliable: true, + topic: Some(TOPIC_DRAW.to_string()), + ..Default::default() + }) + .await; + + if let Err(e) = res { + log::error!("room_service_commands: Failed to publish draw end: {e:?}"); + } + } + RoomServiceCommand::PublishDrawClearPaths(path_ids) => { + let room = inner.room.lock().await; + if room.is_none() { + log::warn!("room_service_commands: Room doesn't exist"); + continue; + } + let room = room.as_ref().unwrap(); + let local_participant = room.local_participant(); + + // Send individual DrawClearPath events for each path ID + for path_id in path_ids { + let event = ClientEvent::DrawClearPath { path_id }; + let payload = serde_json::to_vec(&event).unwrap(); + let res = local_participant + .publish_data(DataPacket { + payload, + reliable: true, + topic: Some(TOPIC_DRAW.to_string()), + ..Default::default() + }) + .await; + + if let Err(e) = res { + log::error!("room_service_commands: Failed to publish draw clear path {}: {e:?}", path_id); + } + } + } + RoomServiceCommand::PublishDrawClearAllPaths => { + let room = inner.room.lock().await; + if room.is_none() { + log::warn!("room_service_commands: Room doesn't exist"); + continue; + } + let room = room.as_ref().unwrap(); + let local_participant = room.local_participant(); + let event = ClientEvent::DrawClearAllPaths; + let payload = serde_json::to_vec(&event).unwrap(); + let res = local_participant + .publish_data(DataPacket { + payload, + reliable: true, + topic: Some(TOPIC_DRAW.to_string()), + ..Default::default() + }) + .await; + + if let Err(e) = res { + log::error!("room_service_commands: Failed to publish draw clear all paths: {e:?}"); + } + } } } } diff --git a/core/tests/src/local_drawing.rs b/core/tests/src/local_drawing.rs new file mode 100644 index 00000000..1806ab74 --- /dev/null +++ b/core/tests/src/local_drawing.rs @@ -0,0 +1,24 @@ +use crate::screenshare_client; +use socket_lib::Message; +use std::{io, time::Duration}; + +pub fn test_local_drawing() -> io::Result<()> { + println!("\n=== TEST: Local Drawing ==="); + + // Start screenshare session + let (mut socket, _) = screenshare_client::start_screenshare_session()?; + + // Enable drawing mode + socket.send_message(Message::DrawingEnabled(true))?; + println!("Drawing enabled. Draw with mouse, press Escape to exit."); + println!("You have 30 seconds to test drawing..."); + + // Wait for user to test manually + std::thread::sleep(Duration::from_secs(30)); + + // Stop screenshare + screenshare_client::stop_screenshare_session(&mut socket)?; + + println!("Test completed."); + Ok(()) +} diff --git a/core/tests/src/main.rs b/core/tests/src/main.rs index 9e52e09a..5258a51b 100644 --- a/core/tests/src/main.rs +++ b/core/tests/src/main.rs @@ -3,6 +3,7 @@ use std::io; mod events; mod livekit_utils; +mod local_drawing; mod remote_clipboard; mod remote_cursor; mod remote_drawing; @@ -44,6 +45,8 @@ enum Commands { #[arg(value_enum)] test_type: DrawingTest, }, + /// Test local drawing functionality (sharer drawing) + LocalDrawing, } #[derive(Clone, ValueEnum, Debug)] @@ -238,6 +241,11 @@ async fn main() -> io::Result<()> { } println!("Drawing test finished."); } + Commands::LocalDrawing => { + println!("Running local drawing test..."); + local_drawing::test_local_drawing()?; + println!("Local drawing test finished."); + } } Ok(()) From 22d8c64573a20b5e5fb7356e5b73544f0b0013c6 Mon Sep 17 00:00:00 2001 From: Iason Paraskevopoulos Date: Sun, 25 Jan 2026 17:59:13 +0000 Subject: [PATCH 02/10] feat(core): set permanent and non permanant mode --- core/socket_lib/src/lib.rs | 7 +- core/src/lib.rs | 159 +++++++++++++++++++------------- core/tests/src/local_drawing.rs | 37 ++++++-- core/tests/src/main.rs | 28 +++++- 4 files changed, 153 insertions(+), 78 deletions(-) diff --git a/core/socket_lib/src/lib.rs b/core/socket_lib/src/lib.rs index d7968c2d..6741ee16 100644 --- a/core/socket_lib/src/lib.rs +++ b/core/socket_lib/src/lib.rs @@ -108,6 +108,11 @@ pub struct SentryMetadata { pub app_version: String, } +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct DrawingEnabled { + pub permanent: bool, +} + #[derive(Debug, Serialize, Deserialize)] pub enum Message { GetAvailableContent, @@ -120,7 +125,7 @@ pub enum Message { ControllerCursorEnabled(bool), LivekitServerUrl(String), SentryMetadata(SentryMetadata), - DrawingEnabled(bool), + DrawingEnabled(DrawingEnabled), } #[derive(Debug)] diff --git a/core/src/lib.rs b/core/src/lib.rs index 8d0a8918..807cd134 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -200,6 +200,7 @@ pub enum ApplicationError { #[derive(Debug)] struct LocalDrawing { enabled: bool, + permanent: bool, left_mouse_pressed: bool, current_path_id: u64, last_cursor_position: Option, @@ -208,7 +209,7 @@ struct LocalDrawing { impl fmt::Display for LocalDrawing { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "LocalDrawing: enabled: {} left_mouse_pressed: {} current_path_id: {} last_cursor_position: {:?} last_redraw_time: {:?}", self.enabled, self.left_mouse_pressed, self.current_path_id, self.last_cursor_position, self.last_redraw_time) + write!(f, "LocalDrawing: enabled: {} permanent: {} left_mouse_pressed: {} current_path_id: {} last_cursor_position: {:?} last_redraw_time: {:?}", self.enabled, self.permanent, self.left_mouse_pressed, self.current_path_id, self.last_cursor_position, self.last_redraw_time) } } @@ -253,6 +254,7 @@ impl<'a> Application<'a> { event_loop_proxy, local_drawing: LocalDrawing { enabled: false, + permanent: false, left_mouse_pressed: false, current_path_id: 0, last_cursor_position: None, @@ -1064,17 +1066,18 @@ impl<'a> ApplicationHandler for Application<'a> { .cursor_controller .trigger_click_animation(position, sid.as_str()); } - UserEvent::LocalDrawingEnabled(enabled) => { - log::debug!("user_event: LocalDrawingEnabled: {}", enabled); + UserEvent::LocalDrawingEnabled(drawing_enabled) => { + log::debug!("user_event: LocalDrawingEnabled: {:?}", drawing_enabled); if self.remote_control.is_none() { log::warn!("user_event: remote control is none local drawing enabled"); return; } let remote_control = &mut self.remote_control.as_mut().unwrap(); - if enabled { + if !self.local_drawing.enabled { // Enable drawing mode self.local_drawing.enabled = true; + self.local_drawing.permanent = drawing_enabled.permanent; let window = remote_control.gfx.window(); @@ -1084,9 +1087,7 @@ impl<'a> ApplicationHandler for Application<'a> { return; } - window.set_cursor(winit::window::Cursor::Custom( - remote_control.pencil_cursor.clone(), - )); + window.set_cursor(remote_control.pencil_cursor.clone()); // Disable remote control remote_control @@ -1094,15 +1095,17 @@ impl<'a> ApplicationHandler for Application<'a> { .set_controllers_enabled(false); remote_control.keyboard_controller.set_enabled(false); - // Set drawing mode for local participant (non-permanent) remote_control.gfx.set_drawing_mode( "local", room_service::DrawingMode::Draw(room_service::DrawSettings { - permanent: false, + permanent: drawing_enabled.permanent, }), ); - log::info!("Local drawing mode enabled"); + log::info!( + "Local drawing mode enabled (permanent: {})", + drawing_enabled.permanent + ); } else { // Disable drawing mode self.local_drawing.enabled = false; @@ -1183,7 +1186,9 @@ impl<'a> ApplicationHandler for Application<'a> { let cursor_controller = &mut remote_control.cursor_controller; remote_control.gfx.draw(cursor_controller); } else { - if self.local_drawing.last_redraw_time.elapsed() > std::time::Duration::from_millis(33) { + if self.local_drawing.last_redraw_time.elapsed() + > std::time::Duration::from_millis(33) + { let cursor_controller = &mut remote_control.cursor_controller; remote_control.gfx.draw(cursor_controller); self.local_drawing.last_redraw_time = std::time::Instant::now(); @@ -1192,64 +1197,86 @@ impl<'a> ApplicationHandler for Application<'a> { } } WindowEvent::MouseInput { state, button, .. } => { - if self.local_drawing.enabled && button == winit::event::MouseButton::Left { - if state == winit::event::ElementState::Pressed { - self.local_drawing.left_mouse_pressed = true; - // Start a new path if we have a cursor position - if let Some(position) = self.local_drawing.last_cursor_position { - if let Some(remote_control) = &mut self.remote_control { - self.local_drawing.current_path_id += 1; - remote_control.gfx.draw_start( - "local", - position, - self.local_drawing.current_path_id, - ); - remote_control.cursor_controller.trigger_render(); - - // Send LiveKit event - if let Some(room_service) = &self.room_service { - let overlay_window = - remote_control.cursor_controller.get_overlay_window(); - let normalized_point = overlay_window - .get_local_percentage_from_pixel(position.x, position.y); - room_service.publish_draw_start(room_service::DrawPathPoint { - point: room_service::ClientPoint { + if self.local_drawing.enabled { + if button == winit::event::MouseButton::Left { + if state == winit::event::ElementState::Pressed { + self.local_drawing.left_mouse_pressed = true; + // Start a new path if we have a cursor position + if let Some(position) = self.local_drawing.last_cursor_position { + if let Some(remote_control) = &mut self.remote_control { + self.local_drawing.current_path_id += 1; + remote_control.gfx.draw_start( + "local", + position, + self.local_drawing.current_path_id, + ); + remote_control.cursor_controller.trigger_render(); + + // Send LiveKit event + if let Some(room_service) = &self.room_service { + let overlay_window = + remote_control.cursor_controller.get_overlay_window(); + let normalized_point = overlay_window + .get_local_percentage_from_pixel( + position.x, position.y, + ); + room_service.publish_draw_start( + room_service::DrawPathPoint { + point: room_service::ClientPoint { + x: normalized_point.x, + y: normalized_point.y, + }, + path_id: self.local_drawing.current_path_id, + }, + ); + } + + log::debug!( + "Local draw_start at {:?} with path_id {}", + position, + self.local_drawing.current_path_id + ); + } + } + } else { + self.local_drawing.left_mouse_pressed = false; + // End the current path + if let Some(position) = self.local_drawing.last_cursor_position { + if let Some(remote_control) = &mut self.remote_control { + remote_control.gfx.draw_end("local", position); + remote_control.cursor_controller.trigger_render(); + + // Send LiveKit event + if let Some(room_service) = &self.room_service { + let overlay_window = + remote_control.cursor_controller.get_overlay_window(); + let normalized_point = overlay_window + .get_local_percentage_from_pixel( + position.x, position.y, + ); + room_service.publish_draw_end(room_service::ClientPoint { x: normalized_point.x, y: normalized_point.y, - }, - path_id: self.local_drawing.current_path_id, - }); - } + }); + } - log::debug!( - "Local draw_start at {:?} with path_id {}", - position, - self.local_drawing.current_path_id - ); + log::debug!("Local draw_end at {:?}", position); + } } } - } else { - self.local_drawing.left_mouse_pressed = false; - // End the current path - if let Some(position) = self.local_drawing.last_cursor_position { - if let Some(remote_control) = &mut self.remote_control { - remote_control.gfx.draw_end("local", position); - remote_control.cursor_controller.trigger_render(); - - // Send LiveKit event - if let Some(room_service) = &self.room_service { - let overlay_window = - remote_control.cursor_controller.get_overlay_window(); - let normalized_point = overlay_window - .get_local_percentage_from_pixel(position.x, position.y); - room_service.publish_draw_end(room_service::ClientPoint { - x: normalized_point.x, - y: normalized_point.y, - }); - } + } else if button == winit::event::MouseButton::Right + && state == winit::event::ElementState::Pressed + { + if let Some(remote_control) = &mut self.remote_control { + // Clear all local drawing paths + remote_control.gfx.draw_clear_all_paths("local"); + remote_control.cursor_controller.trigger_render(); - log::debug!("Local draw_end at {:?}", position); + // Send LiveKit event to clear all paths + if let Some(room_service) = &self.room_service { + room_service.publish_draw_clear_all_paths(); } + log::debug!("Local draw_clear_all_paths on right click"); } } } @@ -1293,7 +1320,9 @@ impl<'a> ApplicationHandler for Application<'a> { // Disable drawing mode let _ = self .event_loop_proxy - .send_event(UserEvent::LocalDrawingEnabled(false)); + .send_event(UserEvent::LocalDrawingEnabled( + socket_lib::DrawingEnabled { permanent: false }, + )); log::debug!("Escape pressed, disabling local drawing"); } } @@ -1365,7 +1394,7 @@ pub enum UserEvent { DrawClearPath(u64, String), DrawClearAllPaths(String), ClickAnimationFromParticipant(room_service::ClientPoint, String), - LocalDrawingEnabled(bool), + LocalDrawingEnabled(socket_lib::DrawingEnabled), } pub struct RenderEventLoop { @@ -1461,7 +1490,7 @@ impl RenderEventLoop { Message::ControllerCursorEnabled(enabled) => { UserEvent::ControllerCursorEnabled(enabled) } - Message::DrawingEnabled(enabled) => UserEvent::LocalDrawingEnabled(enabled), + Message::DrawingEnabled(permanent) => UserEvent::LocalDrawingEnabled(permanent), // Ping is on purpose empty. We use it only for stopping the above receive to timeout. Message::Ping => { continue; diff --git a/core/tests/src/local_drawing.rs b/core/tests/src/local_drawing.rs index 1806ab74..1a79011c 100644 --- a/core/tests/src/local_drawing.rs +++ b/core/tests/src/local_drawing.rs @@ -1,20 +1,41 @@ use crate::screenshare_client; -use socket_lib::Message; +use socket_lib::{Message, DrawingEnabled}; use std::{io, time::Duration}; -pub fn test_local_drawing() -> io::Result<()> { - println!("\n=== TEST: Local Drawing ==="); +pub fn test_local_drawing_permanent() -> io::Result<()> { + println!("\n=== TEST: Local Drawing (Permanent) ==="); // Start screenshare session let (mut socket, _) = screenshare_client::start_screenshare_session()?; - // Enable drawing mode - socket.send_message(Message::DrawingEnabled(true))?; - println!("Drawing enabled. Draw with mouse, press Escape to exit."); - println!("You have 30 seconds to test drawing..."); + // Enable permanent drawing mode + socket.send_message(Message::DrawingEnabled(DrawingEnabled { permanent: true }))?; + println!("Permanent drawing enabled. Draw with mouse, press Escape to exit."); + println!("You have 15 seconds to test drawing..."); // Wait for user to test manually - std::thread::sleep(Duration::from_secs(30)); + std::thread::sleep(Duration::from_secs(15)); + + // Stop screenshare + screenshare_client::stop_screenshare_session(&mut socket)?; + + println!("Test completed."); + Ok(()) +} + +pub fn test_local_drawing_non_permanent() -> io::Result<()> { + println!("\n=== TEST: Local Drawing (Non-Permanent) ==="); + + // Start screenshare session + let (mut socket, _) = screenshare_client::start_screenshare_session()?; + + // Enable non-permanent drawing mode + socket.send_message(Message::DrawingEnabled(DrawingEnabled { permanent: false }))?; + println!("Non-permanent drawing enabled. Draw with mouse, press Escape to exit."); + println!("You have 15 seconds to test drawing..."); + + // Wait for user to test manually + std::thread::sleep(Duration::from_secs(15)); // Stop screenshare screenshare_client::stop_screenshare_session(&mut socket)?; diff --git a/core/tests/src/main.rs b/core/tests/src/main.rs index 5258a51b..83cbc9a9 100644 --- a/core/tests/src/main.rs +++ b/core/tests/src/main.rs @@ -46,7 +46,19 @@ enum Commands { test_type: DrawingTest, }, /// Test local drawing functionality (sharer drawing) - LocalDrawing, + LocalDrawing { + /// Type of local drawing test to run + #[arg(value_enum)] + test_type: LocalDrawingTest, + }, +} + +#[derive(Clone, ValueEnum, Debug)] +enum LocalDrawingTest { + /// Test local drawing with permanent mode ON + Permanent, + /// Test local drawing with permanent mode OFF + NonPermanent, } #[derive(Clone, ValueEnum, Debug)] @@ -241,9 +253,17 @@ async fn main() -> io::Result<()> { } println!("Drawing test finished."); } - Commands::LocalDrawing => { - println!("Running local drawing test..."); - local_drawing::test_local_drawing()?; + Commands::LocalDrawing { test_type } => { + match test_type { + LocalDrawingTest::Permanent => { + println!("Running local drawing test (permanent)..."); + local_drawing::test_local_drawing_permanent()?; + } + LocalDrawingTest::NonPermanent => { + println!("Running local drawing test (non-permanent)..."); + local_drawing::test_local_drawing_non_permanent()?; + } + } println!("Local drawing test finished."); } } From 88f2a6d57a09ad0a00a10f1171bc19cbfccf5583 Mon Sep 17 00:00:00 2001 From: Iason Paraskevopoulos Date: Sun, 25 Jan 2026 18:38:52 +0000 Subject: [PATCH 03/10] feat(tauri): enable drawing for sharer --- tauri/src-tauri/src/app_state.rs | 22 +++++++ tauri/src-tauri/src/main.rs | 42 ++++++++++++- tauri/src/components/ui/call-center.tsx | 80 +++++++++++++++++++++++++ tauri/src/windows/window-utils.ts | 15 +++++ 4 files changed, 158 insertions(+), 1 deletion(-) diff --git a/tauri/src-tauri/src/app_state.rs b/tauri/src-tauri/src/app_state.rs index 667feb36..24362b11 100644 --- a/tauri/src-tauri/src/app_state.rs +++ b/tauri/src-tauri/src/app_state.rs @@ -65,6 +65,9 @@ struct AppStateInternal { /// The user's preferred interaction mode for screen sharing sessions pub last_mode: Option, + + /// Whether drawing mode should persist until right-click (permanent mode) + pub drawing_permanent: Option, } /// Legacy version of the application state structure. @@ -87,6 +90,7 @@ impl Default for AppStateInternal { /// - Hopp server URL: none /// - Feedback disabled: false /// - Last mode: none + /// - Drawing permanent: none fn default() -> Self { AppStateInternal { tray_notification: true, @@ -96,6 +100,7 @@ impl Default for AppStateInternal { hopp_server_url: None, feedback_disabled: false, last_mode: None, + drawing_permanent: None, } } } @@ -353,6 +358,23 @@ impl AppState { } } + /// Gets whether drawing mode should persist until right-click (permanent mode). + /// Returns false if the setting has not been configured. + pub fn drawing_permanent(&self) -> bool { + let _lock = self.lock.lock().unwrap(); + self.state.drawing_permanent.unwrap_or(false) + } + + /// Updates the drawing permanent setting and saves to disk. + pub fn set_drawing_permanent(&mut self, permanent: bool) { + log::info!("set_drawing_permanent: {permanent}"); + let _lock = self.lock.lock().unwrap(); + self.state.drawing_permanent = Some(permanent); + if !self.save() { + log::error!("set_drawing_permanent: Failed to save app state"); + } + } + /// Saves the current state to disk. /// /// # Returns diff --git a/tauri/src-tauri/src/main.rs b/tauri/src-tauri/src/main.rs index 6d0d1377..384756d9 100644 --- a/tauri/src-tauri/src/main.rs +++ b/tauri/src-tauri/src/main.rs @@ -3,7 +3,7 @@ use hopp::sounds::{self, SoundConfig}; use log::LevelFilter; -use socket_lib::{CaptureContent, Content, Extent, Message, ScreenShareMessage, SentryMetadata}; +use socket_lib::{CaptureContent, Content, DrawingEnabled, Extent, Message, ScreenShareMessage, SentryMetadata}; use tauri::Manager; use tauri::{ menu::{MenuBuilder, MenuItemBuilder}, @@ -449,6 +449,43 @@ fn set_last_mode(app: tauri::AppHandle, mode: StoredMode) { data.app_state.set_last_mode(mode); } +#[tauri::command] +fn get_drawing_permanent(app: tauri::AppHandle) -> bool { + log::info!("get_drawing_permanent"); + let data = app.state::>(); + let data = data.lock().unwrap(); + let value = data.app_state.drawing_permanent(); + log::info!("get_drawing_permanent: {value}"); + value +} + +#[tauri::command] +fn set_drawing_permanent(app: tauri::AppHandle, permanent: bool) { + log::info!("set_drawing_permanent: {permanent}"); + let data = app.state::>(); + let mut data = data.lock().unwrap(); + data.app_state.set_drawing_permanent(permanent); +} + +#[tauri::command] +fn enable_drawing(app: tauri::AppHandle, permanent: bool) { + log::info!("enable_drawing: permanent={permanent}"); + let data = app.state::>(); + let mut data = data.lock().unwrap(); + let res = data + .socket + .send_message(Message::DrawingEnabled(DrawingEnabled { permanent })); + if let Err(e) = res { + log::error!("enable_drawing: failed to send message: {e:?}"); + } + drop(data); + + // Hide main window + if let Some(window) = app.get_webview_window("main") { + let _ = window.hide(); + } +} + #[tauri::command] fn minimize_main_window(app: tauri::AppHandle) { log::info!("minimize_main_window"); @@ -1012,6 +1049,9 @@ fn main() { get_last_used_mic, set_last_mode, get_last_mode, + get_drawing_permanent, + set_drawing_permanent, + enable_drawing, minimize_main_window, set_livekit_url, get_livekit_url, diff --git a/tauri/src/components/ui/call-center.tsx b/tauri/src/components/ui/call-center.tsx index da1c71f3..743ed959 100644 --- a/tauri/src/components/ui/call-center.tsx +++ b/tauri/src/components/ui/call-center.tsx @@ -1,9 +1,11 @@ import { formatDistanceToNow } from "date-fns"; import { LuMicOff, LuVideo, LuVideoOff, LuScreenShare, LuScreenShareOff } from "react-icons/lu"; +import { PiScribbleLoopBold } from "react-icons/pi"; import useStore, { CallState, ParticipantRole } from "@/store/store"; import { useKrispNoiseFilter } from "@livekit/components-react/krisp"; import { Separator } from "@/components/ui/separator"; import { ToggleIconButton } from "@/components/ui/toggle-icon-button"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, DropdownMenuCheckboxItem } from "@/components/ui/dropdown-menu"; import { useLocalParticipant, useMediaDeviceSelect, @@ -189,6 +191,7 @@ export function ConnectedActions() { )} + {callTokens?.role === ParticipantRole.SHARER && } + + Enable drawing + + + + + + + + + + + Drawing settings + + + + + Persist until right click + + + + + ); +} + function MicrophoneIcon() { const [retry, setRetry] = useState(0); const { updateCallTokens, callTokens } = useStore(); diff --git a/tauri/src/windows/window-utils.ts b/tauri/src/windows/window-utils.ts index 8179c2f2..b6d845f3 100644 --- a/tauri/src/windows/window-utils.ts +++ b/tauri/src/windows/window-utils.ts @@ -236,6 +236,18 @@ const setLastMode = async (mode: TStoredMode): Promise => { return await invoke("set_last_mode", { mode }); }; +const getDrawingPermanent = async (): Promise => { + return await invoke("get_drawing_permanent"); +}; + +const setDrawingPermanent = async (permanent: boolean): Promise => { + return await invoke("set_drawing_permanent", { permanent }); +}; + +const enableDrawing = async (permanent: boolean): Promise => { + await invoke("enable_drawing", { permanent }); +}; + const minimizeMainWindow = async () => { return await invoke("minimize_main_window"); }; @@ -353,6 +365,9 @@ export const tauriUtils = { setLastUsedMic, getLastMode, setLastMode, + getDrawingPermanent, + setDrawingPermanent, + enableDrawing, minimizeMainWindow, setLivekitUrl, getLivekitUrl, From 631745d1042cef93c9571f0d3b624f763795ddbe Mon Sep 17 00:00:00 2001 From: Iason Paraskevopoulos Date: Sun, 25 Jan 2026 19:58:26 +0000 Subject: [PATCH 04/10] bug: cursor doesn't change --- core/resources/pencil.png | Bin 841 -> 1216 bytes core/src/lib.rs | 19 ++++++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/core/resources/pencil.png b/core/resources/pencil.png index b301d8816772060399989f8cf593adc4e6f0526f..49ad09b94e1b28c975b7749dd7cde45514c51d4f 100644 GIT binary patch delta 663 zcmX@fc7St&iX}_Bqpu?a!^VE@KZ&di3=9g%9znhg3{`3j3=J&|48MRv4KElNN(~qo zUL`OvSj}Ky5HFasE6`@5-f||U6BDn07voYu0u{MAdBs+#6`7OQGK$v&=`?*qJp+9u zJA^8mlFYO;tH_|#;{2S_lFa-(J98r&eGEZ#4G8t2d6^}4FfC}pD7ut^_Cz2QAnZXB zoP3Vaq8{i08<1PA0x~O7b0SH8O8;SgiD?-jv*erZzmaY9dZzGZN3rJ5mDV2@{#Xb zQ^f?Imz+9Zyu{=j&MP$S{UW=rMj|j+QoBlc214)a;*-Bb+K&@>8A@W?GMU-tv6eP zegDC|7lZD-m90N``<{zszyqTmf0+`U9OLHi8~!Us@%~nv)x0Pn?f3LOYzxFrh}_Ii z+B9Fb-=ea8E9ddlf)2k01tR=AIu-haIouDarB_U}(LSB1_JOHuZo4AuSXCO?Bh3(g~_t{ zAxq6PQHT@APX}!VIopstax+l?PMFI}l@yw@Q#>33q?41I(U8?ld;myQ-BN= z@O4OAfJsPKKnRi5$wxwty%8{uflX70ALy4Hi$k=z)Vik~rVz4M z ApplicationHandler for Application<'a> { return; } - window.set_cursor(remote_control.pencil_cursor.clone()); + // This doesn't work + //window.set_cursor(remote_control.pencil_cursor.clone()); // Disable remote control remote_control @@ -1124,16 +1125,16 @@ impl<'a> ApplicationHandler for Application<'a> { let window = remote_control.gfx.window(); - // Disable cursor hittest - if let Err(e) = window.set_cursor_hittest(false) { - log::error!("user_event: Failed to disable cursor hittest: {e:?}"); - } - // Restore default cursor window.set_cursor(winit::window::Cursor::Icon( winit::window::CursorIcon::Default, )); + // Disable cursor hittest + if let Err(e) = window.set_cursor_hittest(false) { + log::error!("user_event: Failed to disable cursor hittest: {e:?}"); + } + // Re-enable remote control remote_control .cursor_controller @@ -1187,8 +1188,12 @@ impl<'a> ApplicationHandler for Application<'a> { remote_control.gfx.draw(cursor_controller); } else { if self.local_drawing.last_redraw_time.elapsed() - > std::time::Duration::from_millis(33) + > std::time::Duration::from_millis(20) { + // This works only if there is a click straight after the mode is enabled + //let window = remote_control.gfx.window(); + //window.set_cursor(remote_control.pencil_cursor.clone()); + let cursor_controller = &mut remote_control.cursor_controller; remote_control.gfx.draw(cursor_controller); self.local_drawing.last_redraw_time = std::time::Instant::now(); From 7455bf03bdac6f789ad608462a12ffcdfe87b6e4 Mon Sep 17 00:00:00 2001 From: Iason Paraskevopoulos Date: Mon, 26 Jan 2026 17:59:40 +0000 Subject: [PATCH 05/10] fix: restore previous state of remote control enabled --- core/src/graphics/graphics_context.rs | 3 ++- core/src/graphics/iced_renderer.rs | 3 ++- core/src/input/mouse.rs | 4 ++++ core/src/lib.rs | 18 +++++++++++++----- core/src/room_service.rs | 11 ++++++++--- core/tests/src/local_drawing.rs | 2 +- 6 files changed, 30 insertions(+), 11 deletions(-) diff --git a/core/src/graphics/graphics_context.rs b/core/src/graphics/graphics_context.rs index d34e88dc..89421a45 100644 --- a/core/src/graphics/graphics_context.rs +++ b/core/src/graphics/graphics_context.rs @@ -447,7 +447,8 @@ impl<'a> GraphicsContext<'a> { /// * `color` - Hex color string for the participant's drawings /// * `auto_clear` - Whether to automatically clear paths after 3 seconds (for local participant) pub fn add_draw_participant(&mut self, sid: String, color: &str, auto_clear: bool) { - self.iced_renderer.add_draw_participant(sid, color, auto_clear); + self.iced_renderer + .add_draw_participant(sid, color, auto_clear); } /// Removes a participant from the draw manager. diff --git a/core/src/graphics/iced_renderer.rs b/core/src/graphics/iced_renderer.rs index 0062fe6c..bb585eae 100644 --- a/core/src/graphics/iced_renderer.rs +++ b/core/src/graphics/iced_renderer.rs @@ -102,7 +102,8 @@ impl IcedRenderer { } pub fn add_draw_participant(&mut self, sid: String, color: &str, auto_clear: bool) { - self.overlay_surface.add_draw_participant(sid, color, auto_clear); + self.overlay_surface + .add_draw_participant(sid, color, auto_clear); } pub fn remove_draw_participant(&mut self, sid: &str) { diff --git a/core/src/input/mouse.rs b/core/src/input/mouse.rs index eca4f91f..2b8c7bd6 100644 --- a/core/src/input/mouse.rs +++ b/core/src/input/mouse.rs @@ -1331,6 +1331,10 @@ impl CursorController { log::error!("trigger_render: error sending redraw event: {e:?}"); } } + + pub fn is_controllers_enabled(&self) -> bool { + self.controllers_cursors_enabled + } } impl Drop for CursorController { diff --git a/core/src/lib.rs b/core/src/lib.rs index a391c247..75e1da7f 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -205,11 +205,12 @@ struct LocalDrawing { current_path_id: u64, last_cursor_position: Option, last_redraw_time: std::time::Instant, + previous_controllers_enabled: bool, } impl fmt::Display for LocalDrawing { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "LocalDrawing: enabled: {} permanent: {} left_mouse_pressed: {} current_path_id: {} last_cursor_position: {:?} last_redraw_time: {:?}", self.enabled, self.permanent, self.left_mouse_pressed, self.current_path_id, self.last_cursor_position, self.last_redraw_time) + write!(f, "LocalDrawing: enabled: {} permanent: {} left_mouse_pressed: {} current_path_id: {} last_cursor_position: {:?} last_redraw_time: {:?} previous_controllers_enabled: {}", self.enabled, self.permanent, self.left_mouse_pressed, self.current_path_id, self.last_cursor_position, self.last_redraw_time, self.previous_controllers_enabled) } } @@ -259,6 +260,7 @@ impl<'a> Application<'a> { current_path_id: 0, last_cursor_position: None, last_redraw_time: std::time::Instant::now(), + previous_controllers_enabled: false, }, }) } @@ -1088,7 +1090,11 @@ impl<'a> ApplicationHandler for Application<'a> { } // This doesn't work - //window.set_cursor(remote_control.pencil_cursor.clone()); + window.set_cursor(remote_control.pencil_cursor.clone()); + + // Store the current controller state before disabling + self.local_drawing.previous_controllers_enabled = + remote_control.cursor_controller.is_controllers_enabled(); // Disable remote control remote_control @@ -1135,11 +1141,13 @@ impl<'a> ApplicationHandler for Application<'a> { log::error!("user_event: Failed to disable cursor hittest: {e:?}"); } - // Re-enable remote control + // Restore remote control to previous state remote_control .cursor_controller - .set_controllers_enabled(true); - remote_control.keyboard_controller.set_enabled(true); + .set_controllers_enabled(self.local_drawing.previous_controllers_enabled); + remote_control + .keyboard_controller + .set_enabled(self.local_drawing.previous_controllers_enabled); // Set drawing mode to disabled for local participant remote_control diff --git a/core/src/room_service.rs b/core/src/room_service.rs index 1ff398c6..12f3659d 100644 --- a/core/src/room_service.rs +++ b/core/src/room_service.rs @@ -794,7 +794,7 @@ async fn room_service_commands( } let room = room.as_ref().unwrap(); let local_participant = room.local_participant(); - + // Send individual DrawClearPath events for each path ID for path_id in path_ids { let event = ClientEvent::DrawClearPath { path_id }; @@ -809,7 +809,10 @@ async fn room_service_commands( .await; if let Err(e) = res { - log::error!("room_service_commands: Failed to publish draw clear path {}: {e:?}", path_id); + log::error!( + "room_service_commands: Failed to publish draw clear path {}: {e:?}", + path_id + ); } } } @@ -833,7 +836,9 @@ async fn room_service_commands( .await; if let Err(e) = res { - log::error!("room_service_commands: Failed to publish draw clear all paths: {e:?}"); + log::error!( + "room_service_commands: Failed to publish draw clear all paths: {e:?}" + ); } } } diff --git a/core/tests/src/local_drawing.rs b/core/tests/src/local_drawing.rs index 1a79011c..f9ef0419 100644 --- a/core/tests/src/local_drawing.rs +++ b/core/tests/src/local_drawing.rs @@ -1,5 +1,5 @@ use crate::screenshare_client; -use socket_lib::{Message, DrawingEnabled}; +use socket_lib::{DrawingEnabled, Message}; use std::{io, time::Duration}; pub fn test_local_drawing_permanent() -> io::Result<()> { From 39f42b45eda4c66033cba5ef950f63491ac8d197 Mon Sep 17 00:00:00 2001 From: Iason Paraskevopoulos Date: Mon, 26 Jan 2026 22:58:05 +0000 Subject: [PATCH 06/10] fix: show custom cursor --- core/resources/pencil.png | Bin 1216 -> 628 bytes core/src/lib.rs | 26 +++++++++++++++++++------- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/core/resources/pencil.png b/core/resources/pencil.png index 49ad09b94e1b28c975b7749dd7cde45514c51d4f..2f80d85d2fc29af80966dc5dc3defcd4b9628750 100644 GIT binary patch delta 618 zcmV-w0+s#13G@UZiBL{Q4GJ0x0000DNk~Le0000Q0000Q2nGNE0I5n$&XMgGe*gh< zQb$4nuFf3k00004XF*Lt006O%3;baP0006BNkl><3`O-eNH<^vvjN?N zPJ-E>EI>9WBXk4k25dk&fmPeFu$+l#>M|f1u(-fJpFn5pvX;9n%ddr98|Ou-BFyk@R&&xY^gI4+Q= zD%gf(3yuYq)}gYCoV3l!Fbpnk3C012EOgAeix9_=l;|b#;Ni`}uGb7^f9_wGivYnh z8dA87tFB`a9ynM~X4u@)eqtR92y6cHl6D^(6O<4&6=vsp*foW5Q~K-1DcwXR5rPF2 zRAC-dYTM7rvo=xWBzImM=q4V029{#;;o>}8lqsTiV+s)Ienm+N5ebmT^q3)N*IYfI zcw>qZ#tJ0n#Au=p1RX6#e^NpnB@f>;wFu~eoz>w&m2eVrl41cNNK#v|jnVIuqo5>f zl&d+(1xW8)?{a@dxrD>UL{-A5Rw_~Jp|W>XkkHT6whmW&Zm-nj#{V}E`W97HCgr4S znDZs}Hnz!eAj&r+dYcioSBcfkEEG|-F%u;16S2{+Zqx?!UgTxFK-M5DuT6>f{t4VO zlfw0qlG<@SYCZ0Rm%jjh;&-k@IQn2}6!F}JNt_bw4+YSH#QZe2egFUf07*qoM6N<$ Ef=nS3i~s-t literal 1216 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjEa{HEjtmSN`?>!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBDAAG{;hE;^%b*2hb1<+n z3NbJPS&Tr)z$nE4G7ZRL@M4sPvx68lplX;H7}_%#SfFa6fHVkr05M1pgl1mAh%j*h z6I`{#0%imoq)m`tVjYm;EbxddW?XQ2>tmIipR1RclAn~S zSCLx)(#2p?VFhI7rj{fsROII56h?X&sHg;q@=(~U%$M(T(8_%FTW^V-_X+1Qs2Nx-^fT8 zs6w~6GOr}DLN~8i8Da>`9GBGMxocyBTg2d!hJD@#aEl5J>s=?Mo;<5%w0;%3G~_D#EZ4$jW0FgTe~DWM4f D+`4Nb diff --git a/core/src/lib.rs b/core/src/lib.rs index 75e1da7f..3de7c60f 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -206,6 +206,7 @@ struct LocalDrawing { last_cursor_position: Option, last_redraw_time: std::time::Instant, previous_controllers_enabled: bool, + cursor_set_times: u32, } impl fmt::Display for LocalDrawing { @@ -261,6 +262,7 @@ impl<'a> Application<'a> { last_cursor_position: None, last_redraw_time: std::time::Instant::now(), previous_controllers_enabled: false, + cursor_set_times: 0, }, }) } @@ -501,7 +503,13 @@ impl<'a> Application<'a> { log::error!("create_overlay_window: Failed to load pencil.png: {e:?}"); ServerError::GfxCreationError })?; - let rgba = pencil_image.to_rgba8(); + let mut rgba = pencil_image.to_rgba8(); + for pixel in rgba.chunks_exact_mut(4) { + let a = pixel[3] as f32 / 255.0; + pixel[0] = (pixel[0] as f32 * a) as u8; + pixel[1] = (pixel[1] as f32 * a) as u8; + pixel[2] = (pixel[2] as f32 * a) as u8; + } let (width, height) = pencil_image.dimensions(); let hotspot_x = 0; // Pencil tip at top-left let hotspot_y = height.saturating_sub(1); // Bottom of image (pencil tip) @@ -1089,8 +1097,8 @@ impl<'a> ApplicationHandler for Application<'a> { return; } - // This doesn't work - window.set_cursor(remote_control.pencil_cursor.clone()); + // Reset cursor set times counter + self.local_drawing.cursor_set_times = 0; // Store the current controller state before disabling self.local_drawing.previous_controllers_enabled = @@ -1198,10 +1206,14 @@ impl<'a> ApplicationHandler for Application<'a> { if self.local_drawing.last_redraw_time.elapsed() > std::time::Duration::from_millis(20) { - // This works only if there is a click straight after the mode is enabled - //let window = remote_control.gfx.window(); - //window.set_cursor(remote_control.pencil_cursor.clone()); - + if self.local_drawing.cursor_set_times < 1000 { + let window = remote_control.gfx.window(); + window.focus_window(); + window.set_cursor_visible(false); + window.set_cursor_visible(true); + window.set_cursor(remote_control.pencil_cursor.clone()); + self.local_drawing.cursor_set_times += 1; + } let cursor_controller = &mut remote_control.cursor_controller; remote_control.gfx.draw(cursor_controller); self.local_drawing.last_redraw_time = std::time::Instant::now(); From 8470e1951025b624f4db0bf07e287495750566ff Mon Sep 17 00:00:00 2001 From: Iason Paraskevopoulos Date: Tue, 27 Jan 2026 09:11:31 +0000 Subject: [PATCH 07/10] fix: follow cursor on drawing --- core/src/input/mouse_macos.rs | 3 ++- core/src/lib.rs | 12 ++++++------ tauri/src-tauri/src/main.rs | 4 +++- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/core/src/input/mouse_macos.rs b/core/src/input/mouse_macos.rs index 28d070c7..d7ac5509 100644 --- a/core/src/input/mouse_macos.rs +++ b/core/src/input/mouse_macos.rs @@ -61,6 +61,7 @@ impl MouseObserver { CGEventType::LeftMouseDown, CGEventType::RightMouseDown, CGEventType::MouseMoved, + CGEventType::LeftMouseDragged, CGEventType::ScrollWheel, ], move |_a, _b, d| { @@ -78,7 +79,7 @@ impl MouseObserver { } match d.get_type() { - CGEventType::MouseMoved => { + CGEventType::MouseMoved | CGEventType::LeftMouseDragged => { log::debug!("Mouse moved event received"); let mut sharer_cursor = internal.lock().unwrap(); diff --git a/core/src/lib.rs b/core/src/lib.rs index 3de7c60f..aa2e7f35 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1085,10 +1085,6 @@ impl<'a> ApplicationHandler for Application<'a> { let remote_control = &mut self.remote_control.as_mut().unwrap(); if !self.local_drawing.enabled { - // Enable drawing mode - self.local_drawing.enabled = true; - self.local_drawing.permanent = drawing_enabled.permanent; - let window = remote_control.gfx.window(); // Enable cursor hittest so we can receive mouse events @@ -1097,6 +1093,10 @@ impl<'a> ApplicationHandler for Application<'a> { return; } + // Enable drawing mode + self.local_drawing.enabled = true; + self.local_drawing.permanent = drawing_enabled.permanent; + // Reset cursor set times counter self.local_drawing.cursor_set_times = 0; @@ -1130,7 +1130,7 @@ impl<'a> ApplicationHandler for Application<'a> { // Clear all local drawing paths remote_control.gfx.draw_clear_all_paths("local"); let cursor_controller = &mut remote_control.cursor_controller; - remote_control.gfx.draw(&cursor_controller); + remote_control.gfx.draw(cursor_controller); // Send LiveKit event to clear all paths if let Some(room_service) = &self.room_service { @@ -1206,7 +1206,7 @@ impl<'a> ApplicationHandler for Application<'a> { if self.local_drawing.last_redraw_time.elapsed() > std::time::Duration::from_millis(20) { - if self.local_drawing.cursor_set_times < 1000 { + if self.local_drawing.cursor_set_times < 500 { let window = remote_control.gfx.window(); window.focus_window(); window.set_cursor_visible(false); diff --git a/tauri/src-tauri/src/main.rs b/tauri/src-tauri/src/main.rs index 384756d9..3bc59680 100644 --- a/tauri/src-tauri/src/main.rs +++ b/tauri/src-tauri/src/main.rs @@ -3,7 +3,9 @@ use hopp::sounds::{self, SoundConfig}; use log::LevelFilter; -use socket_lib::{CaptureContent, Content, DrawingEnabled, Extent, Message, ScreenShareMessage, SentryMetadata}; +use socket_lib::{ + CaptureContent, Content, DrawingEnabled, Extent, Message, ScreenShareMessage, SentryMetadata, +}; use tauri::Manager; use tauri::{ menu::{MenuBuilder, MenuItemBuilder}, From b884f500830a6ab14d04639b78dfb8162daea6d6 Mon Sep 17 00:00:00 2001 From: Iason Paraskevopoulos Date: Tue, 27 Jan 2026 17:34:00 +0000 Subject: [PATCH 08/10] fix: pencil image --- core/resources/pencil.png | Bin 628 -> 789 bytes core/src/lib.rs | 12 ++++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/core/resources/pencil.png b/core/resources/pencil.png index 2f80d85d2fc29af80966dc5dc3defcd4b9628750..34b24cf55b67ae6224fab66644f9ee2731e97e92 100644 GIT binary patch delta 750 zcmVf2v7D zK~#7FeU>|N8bJ_-=SKuCIUyl{G6g5Fs3J1~#SMG|WrUP76tR%)M&<-kIkXZVLDmHf zPOQw>k|PqxY$qO1_s-1DYqhG?&TISYzo&axAt3nD58d&4)o{>&-yP0ov*Ua|->dee zyZ9_+3PPrhQ;Ry%pHtmop67R+e@>?bpf#CH-rj${|DmS7);f;*MzDte6X3X7tq3jE zh@^IwWs(&c$I>*!W)bNUuP-h>9gjyj7z`M|!a$(42H{eqyo?}8MHu#az4xlo8jr`@ z?e+_UbgcQ_Y&MeiDHae})(ZSJ9HRYxFGr)%O$5>qQlb+9&v=h;)B-3Re-!)u_YkyY z5CMv!AQeL`1BvLku{Tn{fc1K1y(O5h=*^b;1^_o2^?PGUC4jRC79w;uxuABtea8wA zcnpz-!2$|SQ_dj3W)V=OErDK9G6eoMOl9#(p7aaR6xyG^zgxD_q0`A?Eypti2(2cG0f?j-vh4XbwYu2XUh@* zTWH$9XOB7#7EzJvaN{ zD1bRC>WrC|JwD3OMz`B-J^p;u3!D}p7Lmu+Un~< ztX7#Y-yZ#bUmOmHMAPDGVHL>02BfGt2T>79bKyHYg)>1Ly{9Kstd{+p*)H9H`F8&vA79 zBv~Pf1}dz2d}_?B>~Uu{lf54P`+eV#ZUSUFeV+JZzvpRkEIyQz1tp;3M1V-}cM)>b znjO;(DWyEj6tLHp9g)op3%Oade*lD|gjw35bOOmeiK;0n`L>>XZqDO*RuojZW4GHm zkh6iLg|?uQ7Q+>Car4iH@8dWwkft z5H%HM=X%&Rg>h5*>&7YFL?sb|1r$_a9#d-D&&jhkQRF0dUL5Eq9(@LuV)NnRJY1A1 zqIP2n5b1tJNeU4OkjM0xA!yfJJ)n4FiW0^OB<93uq7DQdEk;s89VHLnG_?rmft}Ui zLX~h5a*|>JAxKhNv5nF1f0Lu2Bx{tbImrb`?_BS4e?_^3!^T8a!l+g%QR|_ycU6$k z&(yXKS9@-+)a1thHxT+3RaGYCq-&V-CH6M9$#EdcHzazS5w%x|)yymuQMEA>B<&Ni z(XVdQ2J~L!WxLiOE3Zw7_x=goGn2ygl9JkSJ!(DfgqObne&TnoB1Aa)U}_Zc+=WS; a66_BJ(1FDKG`4;K0000 ApplicationHandler for Application<'a> { } WindowEvent::CursorMoved { position, .. } => { if self.local_drawing.enabled { + let display_scale = if let Some(remote_control) = &mut self.remote_control { + remote_control + .cursor_controller + .get_overlay_window() + .get_display_scale() + } else { + 1.0 + }; // Convert physical position to our Position type let pos = Position { - x: position.x, - y: position.y, + x: position.x / display_scale, + y: position.y / display_scale, }; self.local_drawing.last_cursor_position = Some(pos); From 14eaf7885fb8a0e2de7436da060f13f3ab8bbd7a Mon Sep 17 00:00:00 2001 From: Iason Paraskevopoulos Date: Tue, 27 Jan 2026 17:37:32 +0000 Subject: [PATCH 09/10] fix: reset local drawing on screen share start --- core/src/lib.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/core/src/lib.rs b/core/src/lib.rs index 12544e6d..8abb2879 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -209,6 +209,18 @@ struct LocalDrawing { cursor_set_times: u32, } +impl LocalDrawing { + fn reset(&mut self) { + self.enabled = false; + self.permanent = false; + self.left_mouse_pressed = false; + self.current_path_id = 0; + self.last_cursor_position = None; + self.last_redraw_time = std::time::Instant::now(); + self.previous_controllers_enabled = false; + } +} + impl fmt::Display for LocalDrawing { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "LocalDrawing: enabled: {} permanent: {} left_mouse_pressed: {} current_path_id: {} last_cursor_position: {:?} last_redraw_time: {:?} previous_controllers_enabled: {}", self.enabled, self.permanent, self.left_mouse_pressed, self.current_path_id, self.last_cursor_position, self.last_redraw_time, self.previous_controllers_enabled) @@ -586,6 +598,9 @@ impl<'a> Application<'a> { pencil_cursor, }); + // Reset local drawing state on start of screenshare. + self.local_drawing.reset(); + #[cfg(target_os = "linux")] { /* We can't support the overlay surface on linux yet. */ From b53dfac2f9e7c5ac170b9a39addd30a619161371 Mon Sep 17 00:00:00 2001 From: konsalex Date: Sun, 1 Feb 2026 17:17:32 +0100 Subject: [PATCH 10/10] chore: minor cosmetics --- tauri/src/components/ui/call-center.tsx | 34 +++++++++++++++---------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/tauri/src/components/ui/call-center.tsx b/tauri/src/components/ui/call-center.tsx index 3478a2c2..81950603 100644 --- a/tauri/src/components/ui/call-center.tsx +++ b/tauri/src/components/ui/call-center.tsx @@ -38,6 +38,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/comp import { CustomIcons } from "@/components/ui/icons"; import clsx from "clsx"; import { ChevronDownIcon } from "@radix-ui/react-icons"; +import { MoreHorizontal } from "lucide-react"; import { HiOutlinePhoneXMark } from "react-icons/hi2"; import toast from "react-hot-toast"; import ListenToRemoteAudio from "./listen-to-remote-audio"; @@ -274,17 +275,18 @@ function DrawingEnableButton() { }; return ( -
+
- + + Enable drawing @@ -294,18 +296,24 @@ function DrawingEnableButton() { - + + - Drawing settings + Drawing options - + e.preventDefault()} + align="start" + className="w-auto min-w-[200px]" + > Persist until right click