Skip to content

Commit e98b0f4

Browse files
committed
Stabilize native numeric field drags
1 parent ba6ad23 commit e98b0f4

9 files changed

Lines changed: 172 additions & 319 deletions

File tree

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
11
//! The pointer while a numeric field is being dragged.
22
//!
33
//! A port of what `NumericField:__StartDragging` / `__StopDragging` do in Lua:
4-
//! swap the cursor for an empty one and pin the pointer to where the drag began,
5-
//! warping it back after every move. The value then follows the mouse for as far
6-
//! as the user cares to push it, and the pointer never runs off the field, off
7-
//! the panel, or off the screen -- the way every content-creation tool behaves.
8-
//!
9-
//! The pointer is restored to the anchor when the drag ends, so the cursor is
10-
//! exactly where the user left it.
4+
//! swap the cursor for an empty one while the value changes. The engine owns
5+
//! pointer pinning; this helper only owns cursor presentation.
116
127
use spring_native::prelude::NativeInterfaceRef;
138

@@ -16,16 +11,11 @@ const EMPTY_CURSOR: &str = "empty";
1611

1712
#[derive(Default)]
1813
pub(crate) struct DragCursor {
19-
/// Where the drag began, in engine mouse coordinates. The pointer is warped
20-
/// back here every tick, so it never actually moves.
21-
anchor: Option<(i32, i32)>,
2214
assigned: bool,
2315
}
2416

2517
impl DragCursor {
26-
pub(crate) fn begin(&mut self, interface: &NativeInterfaceRef, anchor: (i32, i32)) {
27-
self.anchor = Some(anchor);
28-
18+
pub(crate) fn begin(&mut self, interface: &NativeInterfaceRef) {
2919
let ctrl = interface.unsynced_ctrl();
3020
if !self.assigned {
3121
// Lua does the same on first use: the cursor has to exist before it
@@ -36,8 +26,7 @@ impl DragCursor {
3626
let _ = ctrl.set_mouse_cursor(EMPTY_CURSOR, 1.0);
3727
}
3828

39-
/// Re-assert the empty cursor while a drag is active. Motion itself is
40-
/// pinned synchronously from RmlUi's `drag` listener.
29+
/// Re-assert the empty cursor while a drag is active.
4130
pub(crate) fn reassert(&self, interface: &NativeInterfaceRef) {
4231
// Re-assert it every tick: the engine syncs the cursor to whatever RmlUi
4332
// is hovering on each update, so setting it once at dragstart is undone
@@ -48,21 +37,16 @@ impl DragCursor {
4837
}
4938

5039
pub(crate) fn end(&mut self, interface: &NativeInterfaceRef) {
51-
let ctrl = interface.unsynced_ctrl();
52-
if let Some((x, y)) = self.anchor.take() {
53-
let _ = ctrl.warp_mouse(x, y);
54-
}
5540
// An empty name restores the engine's own cursor, as Lua's bare
5641
// `SB.SetMouseCursor()` does.
57-
let _ = ctrl.set_mouse_cursor("", 1.0);
42+
let _ = interface.unsynced_ctrl().set_mouse_cursor("", 1.0);
5843
}
5944

6045
/// End an interrupted drag without moving the cursor back into the panel.
6146
/// A new press is an explicit user decision about where the pointer is, so
6247
/// preserving that location avoids turning a right-click on the map into a
6348
/// surprise cursor teleport.
6449
pub(crate) fn cancel(&mut self, interface: &NativeInterfaceRef) {
65-
self.anchor = None;
6650
let _ = interface.unsynced_ctrl().set_mouse_cursor("", 1.0);
6751
}
6852
}

‎native/src/sbc/panels/field.rs‎

Lines changed: 24 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ pub struct CommitRequest {
2020
/// Shared queue of fields asking to be committed. Field event listeners push
2121
/// here; the panel drains it each tick.
2222
pub type ChangeQueue = Rc<RefCell<Vec<CommitRequest>>>;
23-
/// Shared queue of pointer interactions (mousedown/mouseup) for drag support.
23+
/// Shared queue of field activation, numeric-gesture starts, and presentation
24+
/// updates.
2425
pub type InteractionQueue = Rc<RefCell<Vec<InteractionEvent>>>;
2526

2627
pub(crate) fn new_change_queue() -> ChangeQueue {
@@ -32,28 +33,14 @@ pub(crate) fn new_interaction_queue() -> InteractionQueue {
3233

3334
#[derive(Debug, Clone)]
3435
pub enum InteractionEvent {
35-
PointerDown {
36-
field: String,
37-
/// Engine mouse coordinates, when the RmlUi event provided them.
38-
/// Generic pointer users do not need an anchor; numeric drags do.
39-
anchor: Option<(i32, i32)>,
40-
},
41-
PointerUp {
42-
field: String,
43-
},
44-
/// RmlUi's drag, which captures the pointer: `DragEnd` arrives wherever the
45-
/// button comes up, including outside the panel.
46-
DragStart {
36+
/// A non-numeric field was activated. Ordinary controls intentionally use
37+
/// RmlUi's normal click event; only numeric drag completion bypasses hit
38+
/// testing.
39+
Click {
4740
field: String,
4841
},
49-
DragEnd {
50-
field: String,
51-
},
52-
/// Exact horizontal movement from one RmlUi drag event. RmlUi emits this
53-
/// before the pointer is put back at its drag anchor.
54-
DragMove {
42+
PointerDown {
5543
field: String,
56-
dx: f32,
5744
},
5845
/// A numeric field asks the panel-owned presentation surface to follow its
5946
/// drag. The field describes values; the surface owns all RmlUi geometry.
@@ -182,70 +169,44 @@ pub(crate) fn on_enter(
182169
Ok(())
183170
}
184171

185-
/// Listen for the events a field drag is made of.
186-
///
187-
/// The drag is bracketed by RmlUi's own `dragstart`/`dragend`, not by the
188-
/// engine's mouse callbacks. The engine hands mouse input to its RmlUi contexts
189-
/// *before* its event clients, and RmlUi consumes a press over the panel -- so
190-
/// the plugin's `mouse_press` is never called for a click on a field, never
191-
/// becomes the engine's mouse owner, and never receives `mouse_release`. A drag
192-
/// released outside the panel could not be ended at all.
193-
///
194-
/// RmlUi captures the pointer for a drag, so `dragend` arrives wherever the
195-
/// button comes up, on or off the element. That is the signal to use. (The
196-
/// element must opt in with `drag: drag` in RCSS.)
172+
/// Register the ordinary activation of a non-numeric field.
197173
///
198-
/// `mousedown`/`mouseup` still bracket a *click*, which is what opens the inline
199-
/// editor when the pointer never moved far enough to become a drag.
174+
/// The generic field helper only needs a completed click. Numeric fields use
175+
/// [`on_numeric_pointer`], where the engine owns relative pointer capture and
176+
/// the panel decides whether the gesture became a drag.
200177
pub(crate) fn on_pointer(
201178
interface: &NativeInterfaceRef,
202179
element: u64,
203180
name: String,
204181
interactions: &InteractionQueue,
205182
) -> Result<(), Error> {
206-
for (event, make) in [
207-
(
208-
"mousedown",
209-
(|field| InteractionEvent::PointerDown {
210-
field,
211-
anchor: None,
212-
}) as fn(String) -> InteractionEvent,
213-
),
214-
("mouseup", |field| InteractionEvent::PointerUp { field }),
215-
("dragstart", |field| InteractionEvent::DragStart { field }),
216-
("dragend", |field| InteractionEvent::DragEnd { field }),
217-
] {
218-
let queue = interactions.clone();
219-
let field = name.clone();
220-
interface
221-
.rml_ui()
222-
.element_add_event_listener(element, event, false, move || {
223-
queue.borrow_mut().push(make(field.clone()));
224-
})?;
225-
}
183+
let queue = interactions.clone();
184+
interface
185+
.rml_ui()
186+
.element_add_event_listener(element, "click", false, move || {
187+
queue.borrow_mut().push(InteractionEvent::Click {
188+
field: name.clone(),
189+
});
190+
})?;
226191
Ok(())
227192
}
228193

229-
/// Register numeric drag events with RmlUi's per-motion coordinates.
194+
/// Register the boundaries of an application-owned numeric gesture.
230195
///
231-
/// RmlUi captures a numeric field's drag before SBC receives normal mouse
232-
/// callbacks. Polling the engine cursor later made a small physical move look
233-
/// large when the original click landed away from the field's logical value.
234-
/// Instead, this mirrors Chili: consume the current RmlUi `drag` movement and
235-
/// immediately put the pointer back where the press began.
196+
/// RmlUi only tells us where the press happened. The engine then captures raw
197+
/// relative motion and re-pins the physical cursor; [`PanelInput`] consumes its
198+
/// typed lifecycle and deltas on update. RmlUi does not participate in ending
199+
/// this gesture, so neither release nor cancellation depends on hit testing.
236200
pub(crate) fn on_numeric_pointer(
237201
interface: &NativeInterfaceRef,
238202
context: u64,
239203
element: u64,
240204
name: String,
241205
interactions: &InteractionQueue,
242206
) -> Result<(), Error> {
243-
let anchor = Rc::new(RefCell::new(None::<(i32, i32)>));
244-
245207
{
246208
let queue = interactions.clone();
247209
let field = name.clone();
248-
let anchor = anchor.clone();
249210
let iface = *interface;
250211
interface
251212
.rml_ui()
@@ -256,91 +217,11 @@ pub(crate) fn on_numeric_pointer(
256217
let Some((x, y)) = current_rml_mouse_position(&iface) else {
257218
return;
258219
};
259-
let Some(anchor_position) = engine_mouse_position(&iface, x, y) else {
260-
return;
261-
};
262-
*anchor.borrow_mut() = Some(anchor_position);
263220
let _ = iface
264221
.rml_ui()
265222
.context_set_pointer_capture(context, x, y, true);
266223
queue.borrow_mut().push(InteractionEvent::PointerDown {
267224
field: field.clone(),
268-
anchor: Some(anchor_position),
269-
});
270-
})?;
271-
}
272-
{
273-
let queue = interactions.clone();
274-
let field = name.clone();
275-
let iface = *interface;
276-
interface
277-
.rml_ui()
278-
.element_add_event_listener(element, "mouseup", false, move || {
279-
if current_rml_mouse_button(&iface) != Some(0) {
280-
return;
281-
}
282-
let _ = iface
283-
.rml_ui()
284-
.context_set_pointer_capture(context, 0, 0, false);
285-
queue.borrow_mut().push(InteractionEvent::PointerUp {
286-
field: field.clone(),
287-
});
288-
})?;
289-
}
290-
{
291-
let queue = interactions.clone();
292-
let field = name.clone();
293-
interface
294-
.rml_ui()
295-
.element_add_event_listener(element, "dragstart", false, move || {
296-
// RmlUi dispatches `dragstart` immediately before the first
297-
// `drag`. Do not warp here: it changes the context position
298-
// before that first `drag` can read its movement. The next
299-
// listener consumes and pins this same physical motion.
300-
queue.borrow_mut().push(InteractionEvent::DragStart {
301-
field: field.clone(),
302-
});
303-
})?;
304-
}
305-
{
306-
let queue = interactions.clone();
307-
let field = name.clone();
308-
let anchor = anchor.clone();
309-
let iface = *interface;
310-
interface
311-
.rml_ui()
312-
.element_add_event_listener(element, "drag", false, move || {
313-
let Some((mouse_x, _)) = current_rml_mouse_position(&iface) else {
314-
return;
315-
};
316-
let Some((anchor_x, anchor_y)) = *anchor.borrow() else {
317-
return;
318-
};
319-
let dx = (mouse_x - anchor_x) as f32;
320-
if dx != 0.0 {
321-
queue.borrow_mut().push(InteractionEvent::DragMove {
322-
field: field.clone(),
323-
dx,
324-
});
325-
// The backend's synthetic anchor move fires `drag` once
326-
// more with zero movement. It has already restored both
327-
// pointer positions, so avoid a redundant OS cursor warp.
328-
let _ = iface.unsynced_ctrl().warp_mouse(anchor_x, anchor_y);
329-
}
330-
})?;
331-
}
332-
{
333-
let queue = interactions.clone();
334-
let field = name;
335-
let iface = *interface;
336-
interface
337-
.rml_ui()
338-
.element_add_event_listener(element, "dragend", false, move || {
339-
let _ = iface
340-
.rml_ui()
341-
.context_set_pointer_capture(context, 0, 0, false);
342-
queue.borrow_mut().push(InteractionEvent::DragEnd {
343-
field: field.clone(),
344225
});
345226
})?;
346227
}
@@ -446,9 +327,6 @@ pub trait Field {
446327
fn end_edit(&mut self, _interface: &NativeInterfaceRef) {}
447328
}
448329

449-
/// RmlUi event positions use top-origin screen coordinates; engine mouse and
450-
/// `warp_mouse` use the bottom-origin coordinates exposed by Spring's native
451-
/// input API.
452330
fn current_rml_mouse_position(interface: &NativeInterfaceRef) -> Option<(i32, i32)> {
453331
let rml = interface.rml_ui();
454332
let (event, ..) = rml.event_get_current().ok()?;
@@ -467,10 +345,3 @@ fn current_rml_mouse_button(interface: &NativeInterfaceRef) -> Option<i32> {
467345
let (button, found) = rml.event_get_parameter_int(event, "button").ok()?;
468346
found.then_some(button)
469347
}
470-
471-
/// Convert an RmlUi top-origin event position for Spring's bottom-origin mouse
472-
/// API. The relative Rml capture itself retains top-origin coordinates.
473-
fn engine_mouse_position(interface: &NativeInterfaceRef, x: i32, y: i32) -> Option<(i32, i32)> {
474-
let geometry = interface.display().get_view_geometry().ok()?;
475-
Some((x, geometry.viewSizeY - y - 1))
476-
}

0 commit comments

Comments
 (0)