diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/app.rs b/hcie-iced-app/crates/hcie-iced-gui/src/app.rs index b516d9f..9de002a 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/app.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/app.rs @@ -691,6 +691,11 @@ pub enum Message { x: u32, y: u32, }, + /// Lightweight no-op message emitted by the canvas overlay on every + /// `CursorMoved` event. Its sole purpose is to trigger a `view()` + /// rebuild so the crosshair overlay redraws at the latest cursor + /// position during idle (non-drawing) pointer movement. + CursorOverlayRedraw, ModifiersChanged(iced::keyboard::Modifiers), SpacePanChanged(bool), @@ -3554,6 +3559,11 @@ impl HcieIcedApp { self.canvas_cursor_pos = Some((x, y)); } + // No-op — this message exists solely to trigger a view() rebuild + // so the crosshair overlay redraws at the latest cursor position. + // See Strategy 1 in the cursor lag fix. + Message::CursorOverlayRedraw => {} + Message::CanvasZoomSet(z) => { let doc = &mut self.documents[self.active_doc]; doc.zoom = z.clamp(ZOOM_MIN, ZOOM_MAX); diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/canvas/mod.rs b/hcie-iced-app/crates/hcie-iced-gui/src/canvas/mod.rs index b819ca5..d6de205 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/canvas/mod.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/canvas/mod.rs @@ -1169,7 +1169,7 @@ impl canvas::Program for OverlayProgram { renderer: &iced::Renderer, _theme: &iced::Theme, bounds: Rectangle, - _cursor: Cursor, + cursor: Cursor, ) -> Vec { let (origin_x, origin_y, _display_w, _display_h) = self.canvas_origin(bounds.size()); let mut geometries = Vec::new(); @@ -2010,8 +2010,12 @@ impl canvas::Program for OverlayProgram { } // ── Crosshair cursor ───────────────────────────────────────────── - if state.is_hovered { - if let Some(cursor) = state.cursor_pos { + // Strategy 4: Use the `cursor` parameter from iced's current render + // frame instead of the retained `state.cursor_pos` (which lags by + // one Elm update cycle). This eliminates the 1-frame positional + // delay between the OS pointer and the brush-circle indicator. + if cursor.is_over(bounds) { + if let Some(cursor_point) = cursor.position_in(bounds) { let mut crosshair_frame = Frame::new(renderer, bounds.size()); let crosshair_size = 10.0; let crosshair_color = iced::Color::from_rgb(1.0, 1.0, 1.0); @@ -2023,7 +2027,7 @@ impl canvas::Program for OverlayProgram { | hcie_engine_api::Tool::Spray ) { let footprint = - Path::circle(cursor, (self.brush_size * self.zoom * 0.5).max(1.0)); + Path::circle(cursor_point, (self.brush_size * self.zoom * 0.5).max(1.0)); crosshair_frame.stroke( &footprint, Stroke { @@ -2035,8 +2039,8 @@ impl canvas::Program for OverlayProgram { } crosshair_frame.stroke( &Path::line( - Point::new(cursor.x - crosshair_size, cursor.y), - Point::new(cursor.x + crosshair_size, cursor.y), + Point::new(cursor_point.x - crosshair_size, cursor_point.y), + Point::new(cursor_point.x + crosshair_size, cursor_point.y), ), Stroke { style: canvas::stroke::Style::Solid(crosshair_color), @@ -2046,8 +2050,8 @@ impl canvas::Program for OverlayProgram { ); crosshair_frame.stroke( &Path::line( - Point::new(cursor.x, cursor.y - crosshair_size), - Point::new(cursor.x, cursor.y + crosshair_size), + Point::new(cursor_point.x, cursor_point.y - crosshair_size), + Point::new(cursor_point.x, cursor_point.y + crosshair_size), ), Stroke { style: canvas::stroke::Style::Solid(crosshair_color), @@ -2147,6 +2151,14 @@ impl canvas::Program for OverlayProgram { } } } + // Strategy 1: Emit a lightweight redraw message on every CursorMoved + // event so iced triggers a view() rebuild and the overlay's draw() + // runs with the latest cursor position. Without this, iced may + // skip repaints during idle cursor movement because no application + // state changed. + if matches!(event, canvas::Event::Mouse(mouse::Event::CursorMoved { .. })) { + return (canvas::event::Status::Ignored, Some(Message::CursorOverlayRedraw)); + } // Always pass events through to the shader widget below (canvas::event::Status::Ignored, None) }