@@ -34,6 +34,9 @@ pub(crate) fn new_interaction_queue() -> InteractionQueue {
3434pub enum InteractionEvent {
3535 PointerDown {
3636 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 ) > ,
3740 } ,
3841 PointerUp {
3942 field : String ,
@@ -46,6 +49,26 @@ pub enum InteractionEvent {
4649 DragEnd {
4750 field : String ,
4851 } ,
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 {
55+ field : String ,
56+ dx : f32 ,
57+ } ,
58+ /// A numeric field asks the panel-owned presentation surface to follow its
59+ /// drag. The field describes values; the surface owns all RmlUi geometry.
60+ NumericDragPresentation ( NumericDragPresentation ) ,
61+ }
62+
63+ #[ derive( Debug , Clone ) ]
64+ pub struct NumericDragPresentation {
65+ pub element : u64 ,
66+ pub title : String ,
67+ pub value : String ,
68+ pub min : Option < String > ,
69+ pub max : Option < String > ,
70+ /// A bounded range normalised to 0..1. `None` means no fill.
71+ pub progress : Option < f32 > ,
4972}
5073
5174/// One field as the control channel sees it: what it is called, what it holds,
@@ -183,7 +206,10 @@ pub(crate) fn on_pointer(
183206 for ( event, make) in [
184207 (
185208 "mousedown" ,
186- ( |field| InteractionEvent :: PointerDown { field } ) as fn ( String ) -> InteractionEvent ,
209+ ( |field| InteractionEvent :: PointerDown {
210+ field,
211+ anchor : None ,
212+ } ) as fn ( String ) -> InteractionEvent ,
187213 ) ,
188214 ( "mouseup" , |field| InteractionEvent :: PointerUp { field } ) ,
189215 ( "dragstart" , |field| InteractionEvent :: DragStart { field } ) ,
@@ -200,6 +226,127 @@ pub(crate) fn on_pointer(
200226 Ok ( ( ) )
201227}
202228
229+ /// Register numeric drag events with RmlUi's per-motion coordinates.
230+ ///
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.
236+ pub ( crate ) fn on_numeric_pointer (
237+ interface : & NativeInterfaceRef ,
238+ context : u64 ,
239+ element : u64 ,
240+ name : String ,
241+ interactions : & InteractionQueue ,
242+ ) -> Result < ( ) , Error > {
243+ let anchor = Rc :: new ( RefCell :: new ( None :: < ( i32 , i32 ) > ) ) ;
244+
245+ {
246+ let queue = interactions. clone ( ) ;
247+ let field = name. clone ( ) ;
248+ let anchor = anchor. clone ( ) ;
249+ let iface = * interface;
250+ interface
251+ . rml_ui ( )
252+ . element_add_event_listener ( element, "mousedown" , false , move || {
253+ if current_rml_mouse_button ( & iface) != Some ( 0 ) {
254+ return ;
255+ }
256+ let Some ( ( x, y) ) = current_rml_mouse_position ( & iface) else {
257+ return ;
258+ } ;
259+ let Some ( anchor_position) = engine_mouse_position ( & iface, x, y) else {
260+ return ;
261+ } ;
262+ * anchor. borrow_mut ( ) = Some ( anchor_position) ;
263+ let _ = iface
264+ . rml_ui ( )
265+ . context_set_pointer_capture ( context, x, y, true ) ;
266+ queue. borrow_mut ( ) . push ( InteractionEvent :: PointerDown {
267+ 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 ( ) ,
344+ } ) ;
345+ } ) ?;
346+ }
347+ Ok ( ( ) )
348+ }
349+
203350// ── DOM helpers ────────────────────────────────────────────────────
204351
205352pub ( crate ) use crate :: sbc:: rml:: { element_by_id, escape_rml} ;
@@ -298,3 +445,32 @@ pub trait Field {
298445 /// End edit mode — switch back to display, without committing.
299446 fn end_edit ( & mut self , _interface : & NativeInterfaceRef ) { }
300447}
448+
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.
452+ fn current_rml_mouse_position ( interface : & NativeInterfaceRef ) -> Option < ( i32 , i32 ) > {
453+ let rml = interface. rml_ui ( ) ;
454+ let ( event, ..) = rml. event_get_current ( ) . ok ( ) ?;
455+ let ( x, has_x) = rml. event_get_parameter_int ( event, "mouse_x" ) . ok ( ) ?;
456+ let ( y, has_y) = rml. event_get_parameter_int ( event, "mouse_y" ) . ok ( ) ?;
457+ if !has_x || !has_y {
458+ return None ;
459+ }
460+ Some ( ( x, y) )
461+ }
462+
463+ /// RmlUi mouse buttons are zero-based: left, right, then middle.
464+ fn current_rml_mouse_button ( interface : & NativeInterfaceRef ) -> Option < i32 > {
465+ let rml = interface. rml_ui ( ) ;
466+ let ( event, ..) = rml. event_get_current ( ) . ok ( ) ?;
467+ let ( button, found) = rml. event_get_parameter_int ( event, "button" ) . ok ( ) ?;
468+ found. then_some ( button)
469+ }
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