Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 51 additions & 12 deletions desktop/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use crate::preferences;
use crate::render::{RenderError, RenderState};
use crate::ui::{UiCommand, UiInstance};
use crate::window::Window;
use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, Preferences};
use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, InputMessage, Key, ModifierKeys, Preferences};
use crate::wrapper::{DesktopWrapper, MmapResourceStorage, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages};

pub(crate) struct App {
Expand Down Expand Up @@ -349,6 +349,9 @@ impl App {
window.start_pointer_lock();
}
}
DesktopFrontendMessage::PointerUnlock => {
self.unlock_pointer();
}
DesktopFrontendMessage::WindowClose => {
self.app_event_scheduler.schedule(AppEvent::Exit);
}
Expand Down Expand Up @@ -506,6 +509,40 @@ impl App {
}
}
}

fn unlock_pointer(&mut self) {
if let Some(pos) = self.input_state.unlock_pointer()
&& let Some(window) = &self.window
{
window.end_pointer_lock();
self.ui.send(UiCommand::Input(WindowEvent::PointerMoved {
device_id: None,
position: pos,
primary: true,
source: winit::event::PointerSource::Mouse,
}));
} else if let Some(window) = &self.window {
window.end_pointer_lock();
}
}

/// Synthesize an Escape press/release so the editor cancels the active transform (same as a user pressing Escape)
fn send_cancel_escape(&mut self) {
for message in [
InputMessage::KeyDown {
key: Key::Escape,
key_repeat: false,
modifier_keys: ModifierKeys::empty(),
},
InputMessage::KeyUp {
key: Key::Escape,
key_repeat: false,
modifier_keys: ModifierKeys::empty(),
},
] {
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(DesktopWrapperMessage::Input(message)));
}
}
}
impl ApplicationHandler for App {
fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) {
Expand Down Expand Up @@ -536,23 +573,22 @@ impl ApplicationHandler for App {
}

fn window_event(&mut self, _event_loop: &dyn ActiveEventLoop, _window_id: WindowId, event: WindowEvent) {
// Handle pointer lock release
if let WindowEvent::PointerButton {
state: ElementState::Released,
button,
..
} = &event && button.clone().mouse_button() == Some(MouseButton::Left)
&& let Some(pointer_lock_position) = self.input_state.unlock_pointer()
&& self.input_state.pointer_locked()
{
if let Some(window) = &self.window {
window.end_pointer_lock();
}
self.ui.send(UiCommand::Input(WindowEvent::PointerMoved {
device_id: None,
position: pointer_lock_position,
primary: true,
source: winit::event::PointerSource::Mouse,
}));
self.unlock_pointer();
}

// The editor can no longer track the pointer, so cancel any pointer-locked operation (G/R/S) and release the grab
if let WindowEvent::Focused(false) = &event
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
&& self.input_state.pointer_locked()
{
self.unlock_pointer();
self.send_cancel_escape();
}

for action in self.input_state.process(&event) {
Expand Down Expand Up @@ -642,6 +678,9 @@ impl ApplicationHandler for App {
if self.input_state.pointer_locked()
&& let winit::event::DeviceEvent::PointerMotion { delta: (x, y) } = event
{
// DeviceEvent deltas are physical pixels
let scale = self.input_state.viewport_scale();
let (x, y) = if scale != 0. { (x / scale, y / scale) } else { (x, y) };
let message = DesktopWrapperMessage::PointerLockMove { x, y };
self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
}
Expand Down
4 changes: 4 additions & 0 deletions desktop/src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,10 @@ impl InputState {
self.viewport_info.as_ref().map_or(1., |info| info.scale)
}

pub(crate) fn viewport_scale(&self) -> f64 {
self.scale()
}

fn in_viewport(&self, position: PhysicalPosition<f64>) -> bool {
self.viewport_info.as_ref().is_some_and(|info| info.contains(position))
}
Expand Down
3 changes: 3 additions & 0 deletions desktop/wrapper/src/intercept_frontend_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
FrontendMessage::WindowPointerLock => {
dispatcher.respond(DesktopFrontendMessage::PointerLock);
}
FrontendMessage::WindowPointerUnlock => {
dispatcher.respond(DesktopFrontendMessage::PointerUnlock);
}
FrontendMessage::WindowClose => {
dispatcher.respond(DesktopFrontendMessage::WindowClose);
}
Expand Down
1 change: 1 addition & 0 deletions desktop/wrapper/src/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ pub enum DesktopFrontendMessage {
content: String,
},
PointerLock,
PointerUnlock,
WindowClose,
WindowMinimize,
WindowMaximize,
Expand Down
1 change: 1 addition & 0 deletions editor/src/messages/app_window/app_window_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::messages::prelude::*;
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum AppWindowMessage {
PointerLock,
PointerUnlock,
PointerLockMove { x: f64, y: f64 },
Restart,
Close,
Expand Down
6 changes: 6 additions & 0 deletions editor/src/messages/app_window/app_window_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,14 @@ impl MessageHandler<AppWindowMessage, ()> for AppWindowMessageHandler {
#[cfg(not(target_family = "wasm"))]
responses.add(FrontendMessage::WindowPointerLock);
}
AppWindowMessage::PointerUnlock => {
#[cfg(not(target_family = "wasm"))]
responses.add(FrontendMessage::WindowPointerUnlock);
}
AppWindowMessage::PointerLockMove { x, y } => {
responses.add(FrontendMessage::WindowPointerLockMove { position: (x, y) });
// Keep G/R/S dragging moving while the pointer is locked
responses.add(InputPreprocessorMessage::RelativePointerMove { delta: glam::DVec2::new(x, y) });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: On web, each locked pointermove reaches the editor through both the existing absolute path and this new relative path. The absolute update resets the position that the relative update advances, so G/R/S motion can cancel or jump; suppress normal pointer forwarding during software-cursor pointer lock, or use only one movement path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/messages/app_window/app_window_message_handler.rs, line 26:

<comment>On web, each locked `pointermove` reaches the editor through both the existing absolute path and this new relative path. The absolute update resets the position that the relative update advances, so G/R/S motion can cancel or jump; suppress normal pointer forwarding during software-cursor pointer lock, or use only one movement path.</comment>

<file context>
@@ -14,8 +14,16 @@ impl MessageHandler<AppWindowMessage, ()> for AppWindowMessageHandler {
+				// Also feed relative delta into InputPreprocessor for G/R/S infinite drag (fake cursor)
+				// Divide by viewport scale will be handled at source (desktop physical -> logical); here we keep raw
+				// but transform_layer will handle scaling via document_to_viewport
+				responses.add(InputPreprocessorMessage::RelativePointerMove { delta: glam::DVec2::new(x, y) });
 			}
 			AppWindowMessage::Close => {
</file context>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: AppWindowMessage::PointerLockMove is also the native NumberInput drag protocol, not only G/R/S. This unconditional relative message therefore makes every native number-field drag invoke the active canvas tool's generic PointerMove mappings in addition to updating the field; gate the relative forwarding to G/R/S or introduce a separate transform-only pointer-lock message.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/messages/app_window/app_window_message_handler.rs, line 26:

<comment>`AppWindowMessage::PointerLockMove` is also the native NumberInput drag protocol, not only G/R/S. This unconditional relative message therefore makes every native number-field drag invoke the active canvas tool's generic `PointerMove` mappings in addition to updating the field; gate the relative forwarding to G/R/S or introduce a separate transform-only pointer-lock message.</comment>

<file context>
@@ -14,8 +14,16 @@ impl MessageHandler<AppWindowMessage, ()> for AppWindowMessageHandler {
+				// Also feed relative delta into InputPreprocessor for G/R/S infinite drag (fake cursor)
+				// Divide by viewport scale will be handled at source (desktop physical -> logical); here we keep raw
+				// but transform_layer will handle scaling via document_to_viewport
+				responses.add(InputPreprocessorMessage::RelativePointerMove { delta: glam::DVec2::new(x, y) });
 			}
 			AppWindowMessage::Close => {
</file context>

}
AppWindowMessage::Close => {
#[cfg(not(target_family = "wasm"))]
Expand Down
7 changes: 7 additions & 0 deletions editor/src/messages/frontend/frontend_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,13 @@ pub enum FrontendMessage {
position: (f64, f64),
},
#[cfg(not(target_family = "wasm"))]
WindowPointerUnlock,
UpdateSoftwareCursor {
visible: bool,
x: f64,
y: f64,
},
#[cfg(not(target_family = "wasm"))]
WindowClose,
#[cfg(not(target_family = "wasm"))]
WindowMinimize,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::messages::input_mapper::utility_types::keyboard::{Key, ModifierKeys};
use crate::messages::input_mapper::utility_types::pointer::EditorPointerState;
use crate::messages::prelude::*;
use glam::DVec2;

#[impl_message(Message, InputPreprocessor)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
Expand All @@ -14,4 +15,5 @@ pub enum InputPreprocessorMessage {
PointerShake { editor_mouse_state: EditorPointerState, modifier_keys: ModifierKeys },
CurrentTime { timestamp: u64 },
WheelScroll { editor_mouse_state: EditorPointerState, modifier_keys: ModifierKeys },
RelativePointerMove { delta: DVec2 },
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ impl<'a> MessageHandler<InputPreprocessorMessage, InputPreprocessorMessageContex

responses.add(InputMapperMessage::WheelScroll);
}
InputPreprocessorMessage::RelativePointerMove { delta } => {
self.mouse.position += delta;

responses.add(InputMapperMessage::PointerMove);
}
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::consts::{ANGLE_MEASURE_RADIUS_FACTOR, ARC_MEASURE_RADIUS_FACTOR_RANGE, COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GRAY, SLOWING_DIVISOR};
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::input_mapper::utility_types::pointer::{DocumentPosition, ViewportPosition};
use crate::messages::portfolio::document::overlays::utility_functions::text_width;
use crate::messages::portfolio::document::overlays::utility_types::{OverlayProvider, Pivot};
Expand All @@ -12,6 +13,7 @@ use crate::messages::tool::common_functionality::shapes::shape_utility::format_r
use crate::messages::tool::tool_messages::select_tool;
use crate::messages::tool::tool_messages::tool_prelude::Key;
use crate::messages::tool::utility_types::{ToolData, ToolType};
use crate::messages::viewport::Position;
use glam::{DAffine2, DVec2};
use graphene_std::renderer::Quad;
use graphene_std::vector::click_target::ClickTargetType;
Expand Down Expand Up @@ -95,6 +97,10 @@ pub struct TransformLayerMessageHandler {

// Path tool (ghost outlines showing pre-transform geometry)
ghost_outline: Vec<(Vec<ClickTargetType>, DAffine2)>,

// Software cursor for wrap-around (visible fake cursor on Wayland/Web, OS warp on X11)
software_cursor_active: bool,
software_cursor_pos: ViewportPosition,
}

#[message_handler_data]
Expand Down Expand Up @@ -339,6 +345,7 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
responses.add(OverlaysMessage::RemoveProvider {
provider: TRANSFORM_GRS_OVERLAY_PROVIDER,
});
self.disable_software_cursor(responses);
}
}
TransformLayerMessage::BeginTransformOperation { operation } => {
Expand Down Expand Up @@ -384,6 +391,7 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
responses.add(OverlaysMessage::AddProvider {
provider: TRANSFORM_GRS_OVERLAY_PROVIDER,
});
self.enable_software_cursor(responses, input.mouse.position);
// Find a way better than this hack
responses.add(TransformLayerMessage::PointerMove {
slow_key: SLOW_KEY,
Expand Down Expand Up @@ -471,6 +479,7 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
responses.add(OverlaysMessage::AddProvider {
provider: TRANSFORM_GRS_OVERLAY_PROVIDER,
});
self.enable_software_cursor(responses, input.mouse.position);
}
responses.add(TransformLayerMessage::BeginTransformOperation { operation: transform_type });
responses.add(TransformLayerMessage::PointerMove {
Expand Down Expand Up @@ -510,6 +519,7 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
responses.add(OverlaysMessage::RemoveProvider {
provider: TRANSFORM_GRS_OVERLAY_PROVIDER,
});
self.disable_software_cursor(responses);
}
TransformLayerMessage::ConstrainX => {
self.state.is_transforming_in_local_space = self.transform_operation.constrain_axis(Axis::X, &mut selected, &self.state, document);
Expand Down Expand Up @@ -581,6 +591,26 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
};
}

if self.software_cursor_active {
let delta = input.mouse.position - self.mouse_position;
self.software_cursor_pos += delta;

// Wrap around the viewport edges
let size = viewport.size();
if size.x() > 0. && size.y() > 0. {
self.software_cursor_pos = DVec2::new(
((self.software_cursor_pos.x % size.x()) + size.x()) % size.x(),
((self.software_cursor_pos.y % size.y()) + size.y()) % size.y(),
);
}

responses.add(FrontendMessage::UpdateSoftwareCursor {
visible: true,
x: self.software_cursor_pos.x,
y: self.software_cursor_pos.y,
});
}

self.mouse_position = input.mouse.position;
}
TransformLayerMessage::SelectionChanged => {
Expand Down Expand Up @@ -651,6 +681,27 @@ impl TransformLayerMessageHandler {
self.transform_operation.hints(responses, self.state.is_transforming_in_local_space);
}

fn enable_software_cursor(&mut self, responses: &mut VecDeque<Message>, pos: ViewportPosition) {
if self.software_cursor_active {
return;
}
self.software_cursor_active = true;
self.software_cursor_pos = pos;
responses.add(FrontendMessage::UpdateSoftwareCursor { visible: true, x: pos.x, y: pos.y });
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::None });
responses.add(AppWindowMessage::PointerLock);
}

fn disable_software_cursor(&mut self, responses: &mut VecDeque<Message>) {
if !self.software_cursor_active {
return;
}
self.software_cursor_active = false;
responses.add(FrontendMessage::UpdateSoftwareCursor { visible: false, x: 0., y: 0. });
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: When a G/R/S transform ends in a tool with a non-default cursor, this line overwrites that tool cursor with Default. Restore the cursor that was active before hiding it, or ask the owning tool to refresh its cursor instead of hardcoding Default.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs, line 701:

<comment>When a G/R/S transform ends in a tool with a non-default cursor, this line overwrites that tool cursor with `Default`. Restore the cursor that was active before hiding it, or ask the owning tool to refresh its cursor instead of hardcoding `Default`.</comment>

<file context>
@@ -651,6 +681,27 @@ impl TransformLayerMessageHandler {
+		}
+		self.software_cursor_active = false;
+		responses.add(FrontendMessage::UpdateSoftwareCursor { visible: false, x: 0., y: 0. });
+		responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default });
+		responses.add(AppWindowMessage::PointerUnlock);
+	}
</file context>

responses.add(AppWindowMessage::PointerUnlock);
}

fn set_ghost_outline(ghost_outline: &mut Vec<(Vec<ClickTargetType>, DAffine2)>, shape_editor: &ShapeState, document: &DocumentMessageHandler) {
ghost_outline.clear();
for &layer in shape_editor.selected_shape_state.keys() {
Expand Down
Loading