@gjsify/event-bridge 0.48.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,11 +27,72 @@ attachEventControllers(gtkWidget, () => domElement);
27
27
 
28
28
  | GTK Controller | DOM Events |
29
29
  |---|---|
30
- | EventControllerMotion | pointermove, mousemove, pointer/mouse enter/leave/over/out |
31
- | GestureClick | pointer/mouse down/up, click, dblclick, contextmenu |
32
- | EventControllerScroll | wheel |
30
+ | EventControllerMotion | pointermove, mousemove, pointer/mouse enter/leave/over/out (mouse and pen) |
31
+ | GestureClick | pointer/mouse down/up, click, dblclick, contextmenu, pointercancel (mouse and pen) |
32
+ | EventControllerScroll | wheel — `deltaMode` follows `Gdk.ScrollUnit`: a wheel notch is `DOM_DELTA_LINE` (three lines), touchpad pixels are `DOM_DELTA_PIXEL` |
33
33
  | EventControllerKey | keydown, keyup |
34
34
  | EventControllerFocus | focus, focusin, blur, focusout |
35
+ | EventControllerLegacy | the touchscreen: one `PointerEvent` stream per contact (`pointerType: 'touch'`, a distinct `pointerId` per `GdkEventSequence`, `isPrimary` from the pointer-emulating contact) — pointerover/enter/down/move/up/cancel/out/leave, plus the compatibility mouse events and `click` the primary contact owes legacy code, in the order Pointer Events Level 3 prescribes for devices without hover |
36
+
37
+ `pointerType` is `'mouse'`, `'pen'` or `'touch'` from the GDK device source. Touch movement comes from the raw
38
+ event stream because `EventControllerMotion` never fires for a finger, and touch presses never come from
39
+ `GestureClick`, which strands its press on multi-finger input. No `TouchEvent`/`TouchList` is emitted — pointer
40
+ events carry every field a consumer needs to reconstruct pan and pinch, and `@gjsify/dom-events` has no
41
+ `TouchEvent`.
42
+
43
+ ## Design
44
+
45
+ Two pointer sources, deliberately kept apart. Measured on a OnePlus 6T (postmarketOS, GTK 4.22, mutter 48) with
46
+ synthetic touch through `org.gnome.Mutter.RemoteDesktop` and then a real finger (1183 rows):
47
+
48
+ - `Gtk.EventControllerMotion` emitted **0** `motion` callbacks for a finger, while the raw event stream carried every
49
+ `TOUCH_BEGIN`/`TOUCH_UPDATE`/`TOUCH_END` 1:1 (61 of 61 for a real swipe) at ~8 ms cadence. Touch movement therefore
50
+ comes from `Gtk.EventControllerLegacy`, not from the motion controller.
51
+ - `Gtk.GestureClick` strands its press on multi-finger input, and intermittently so: 5 `pressed` / 3 `released` /
52
+ 0 `cancel` over the real two-finger gestures, one `released` with `n_press = 0`. Its cause is in GTK's own code:
53
+ a second contact makes the gesture unrecognised and `end` fires with the *second* finger's sequence, which
54
+ `gtk_gesture_click_end` ignores; the first finger's later `TOUCH_END` finds nothing recognised. A touch-sourced
55
+ GestureClick signal (`isTouchEvent(get_current_event())`) is therefore ignored here in every branch — neither
56
+ a missing nor a late `released` can reach the DOM.
57
+ - A mouse or pen stays on `EventControllerMotion` + `GestureClick`: widget-local coordinates from the signal, GTK's
58
+ click counting, `pointerType` from `Gdk.InputSource` (`PEN` → `'pen'`, unmeasured here — no stylus).
59
+
60
+ The touch path (`touch-pointers.ts`, no GTK imports, unit-tested with the frame shapes the adapter extracts):
61
+
62
+ - **Identity.** `pointerId` is distinct per `GdkEventSequence`. The sequence is an opaque boxed pointer (the Wayland
63
+ backend passes the wl_touch id + 1) with no accessor, and GJS returns a fresh wrapper per call, so `sequenceKey()`
64
+ reads the `native@` address off the wrapper's `toString()` — GTK registers the type with an identity copy, so every
65
+ wrapper of one contact prints the same address (118 of 118 rows on device). A runtime that prints something else
66
+ falls back to the emulating-pointer flag — still correct for two contacts — and says so once.
67
+ - **The mouse pointer leaves when a finger lands.** On the first touch after mouse activity GTK emits
68
+ `EventControllerMotion::leave` for the mouse, so `pointerleave` (pointerId 1) plus its `mouseleave`/`mouseout` land
69
+ between the contact's `mousedown` and `mouseup`; the contact's own `mouseout`/`mouseleave` still follow `pointerup`.
70
+ Measured on device, once per mouse→touch transition; a hover consumer sees the leave twice, never a missing one.
71
+ - **`isPrimary`** is `GdkTouchEvent.get_emulating_pointer()`, evaluated only after the source is known to be a
72
+ touchscreen (mouse events read `false` too).
73
+ - **Coordinates** come from `Gdk.Event.get_position()` (surface-relative) through `surfaceToWidget()`, the same
74
+ translation gtkmain.c performs before running a controller; nothing on the path rounds (a real digitizer at
75
+ scale 3 reports thirds of a pixel).
76
+ - **Cancel.** `TOUCH_CANCEL` → `pointercancel` — a GNOME Mobile edge swipe reaches the client as begin + updates and
77
+ is then cancelled by the shell, fanning `cancel` out to every gesture; the translator ignores the fan-out because
78
+ the contact is already gone. `pointercancel` carries the last dispatched position, not the frame's. Every contact
79
+ is cancelled on a `GRAB_BROKEN` to a foreign surface and when the widget unmaps or turns insensitive — the three
80
+ places `GtkGesture` cancels its own sequences (unmeasured; the translator's `cancelAll` is unit-tested). A `begin`
81
+ for a contact still active cancels the stale one first, so a lost `TOUCH_END` heals.
82
+ - **Compatibility mouse events** follow Pointer Events Level 3 § "Mapping for devices that do not support hover"
83
+ verbatim: `mousemove, pointerover, pointerenter, mouseover, mouseenter, pointerdown, mousedown, [pointermove,
84
+ mousemove]*, pointerup, mouseup, pointerout, pointerleave, mouseout, mouseleave, click`; primary contact only;
85
+ a cancelled primary `pointerdown` suppresses mousedown/mousemove/mouseup but not the transitions or `click`;
86
+ `pointercancel` sends its `mouseup` at the window. `click` is a `PointerEvent` (PE L3) with `button`/`buttons` in
87
+ the mouse model. `detail` is 1 — GTK's `n_press` was measured untrustworthy on touch — so there is no touch
88
+ `dblclick`.
89
+ - **No gesture semantics** (pan, pinch, long-press, tap-vs-stroke) live in the bridge: a consumer rebuilds them from
90
+ the per-contact stream. No `TouchEvent`/`TouchList`: pointer events carry every field needed, `@gjsify/dom-events`
91
+ has no `TouchEvent`, and Excalibur uses pointer events exclusively when `PointerEvent` exists.
92
+
93
+ Not measured, and not claimed: pen pressure/tilt (no plumbing), `GRAB_BROKEN`, three or more fingers, double-tap
94
+ click counts, palm rejection. Pressure is *absent* on 1165 of 1165 real touch rows, so touch reports the spec
95
+ constant 0.5 while in contact.
35
96
 
36
97
  ## License
37
98
 
@@ -1 +1 @@
1
- import"./_virtual/_rolldown/runtime.js";import{gdkKeyvalToCode as e,gdkKeyvalToKey as t,gdkKeyvalToLocation as n}from"./key-map.js";import r from"gi://Gtk?version=4.0";import i from"gi://Gdk?version=4.0";import{FocusEvent as a,KeyboardEvent as o,MouseEvent as s,PointerEvent as c,WheelEvent as l}from"@gjsify/dom-events";function getGlobalEventTarget(){return globalThis.__gjsify_globalEventTarget}function extractModifiers(e){let t=e.get_current_event_state();return{shiftKey:!!(t&i.ModifierType.SHIFT_MASK),ctrlKey:!!(t&i.ModifierType.CONTROL_MASK),altKey:!!(t&i.ModifierType.ALT_MASK),metaKey:!!(t&i.ModifierType.SUPER_MASK)}}function gtkButtonToDom(e){return e===1?0:e===2?1:e===3?2:e-1}function buttonsFromModifiers(e){let t=e.get_current_event_state(),n=0;return t&i.ModifierType.BUTTON1_MASK&&(n|=1),t&i.ModifierType.BUTTON3_MASK&&(n|=2),t&i.ModifierType.BUTTON2_MASK&&(n|=4),n}function attachEventControllers(u,d,f){u.set_focusable(!0),u.set_can_focus(!0);let p={lastX:0,lastY:0,buttonsPressed:0,pressedKeys:new Set},m=new r.EventControllerMotion;m.connect(`motion`,(e,t,n)=>{let r=d();if(!r)return;let i=u.get_allocated_width(),a=u.get_allocated_height(),o=Math.max(0,Math.min(t,i)),l=Math.max(0,Math.min(n,a)),f=o-p.lastX,h=l-p.lastY,g=extractModifiers(m),_=buttonsFromModifiers(m),v={...g,clientX:o,clientY:l,offsetX:o,offsetY:l,screenX:o,screenY:l,movementX:f,movementY:h,buttons:_,button:0,bubbles:!0,cancelable:!0};r.dispatchEvent(new c(`pointermove`,{...v,pointerId:1,pointerType:`mouse`,isPrimary:!0})),r.dispatchEvent(new s(`mousemove`,v)),p.lastX=o,p.lastY=l}),m.connect(`enter`,(e,t,n)=>{let r=d();if(!r)return;p.lastX=t,p.lastY=n;let i={...extractModifiers(m),clientX:t,clientY:n,offsetX:t,offsetY:n,screenX:t,screenY:n,bubbles:!1,cancelable:!1};r.dispatchEvent(new c(`pointerenter`,{...i,pointerId:1,pointerType:`mouse`,isPrimary:!0})),r.dispatchEvent(new s(`mouseenter`,i)),r.dispatchEvent(new s(`mouseover`,{...i,bubbles:!0}))}),m.connect(`leave`,()=>{let e=d();if(!e)return;let t={...extractModifiers(m),clientX:p.lastX,clientY:p.lastY,bubbles:!1,cancelable:!1};e.dispatchEvent(new c(`pointerleave`,{...t,pointerId:1,pointerType:`mouse`,isPrimary:!0})),e.dispatchEvent(new s(`mouseleave`,t)),e.dispatchEvent(new s(`mouseout`,{...t,bubbles:!0}))}),u.add_controller(m);let h=new r.GestureClick;h.set_button(0),h.connect(`pressed`,(e,t,n,r)=>{let i=d();if(!i)return;let a=gtkButtonToDom(h.get_current_button()),o=extractModifiers(h);p.buttonsPressed|=1<<a;let l={...o,clientX:n,clientY:r,offsetX:n,offsetY:r,screenX:n,screenY:r,button:a,buttons:p.buttonsPressed,detail:t,bubbles:!0,cancelable:!0};i.dispatchEvent(new c(`pointerdown`,{...l,pointerId:1,pointerType:`mouse`,isPrimary:!0})),i.dispatchEvent(new s(`mousedown`,l)),u.grab_focus()}),h.connect(`released`,(e,t,n,r)=>{let i=d();if(!i)return;let a=gtkButtonToDom(h.get_current_button()),o=extractModifiers(h);p.buttonsPressed&=~(1<<a);let l={...o,clientX:n,clientY:r,offsetX:n,offsetY:r,screenX:n,screenY:r,button:a,buttons:p.buttonsPressed,detail:t,bubbles:!0,cancelable:!0};i.dispatchEvent(new c(`pointerup`,{...l,pointerId:1,pointerType:`mouse`,isPrimary:!0})),i.dispatchEvent(new s(`mouseup`,l)),a===0&&(i.dispatchEvent(new s(`click`,l)),t===2&&i.dispatchEvent(new s(`dblclick`,l))),a===2&&i.dispatchEvent(new s(`contextmenu`,{...l,cancelable:!0}))}),u.add_controller(h);let g=new r.EventControllerScroll({flags:r.EventControllerScrollFlags.BOTH_AXES});g.connect(`scroll`,(e,t,n)=>{let r=d();if(!r)return;let i={...extractModifiers(g),clientX:p.lastX,clientY:p.lastY,offsetX:p.lastX,offsetY:p.lastY,screenX:p.lastX,screenY:p.lastY,deltaX:t*100,deltaY:n*100,deltaZ:0,deltaMode:0,bubbles:!0,cancelable:!0};return r.dispatchEvent(new l(`wheel`,i)),!1}),u.add_controller(g);let _=new r.EventControllerKey;_.connect(`key-pressed`,(r,a,s,c)=>{let l=d();if(!l)return!1;let u=p.pressedKeys.has(a);p.pressedKeys.add(a);let m=t(a),h={key:m,code:e(a),location:n(a),repeat:u,altKey:!!(c&i.ModifierType.ALT_MASK),ctrlKey:!!(c&i.ModifierType.CONTROL_MASK),metaKey:!!(c&i.ModifierType.SUPER_MASK),shiftKey:!!(c&i.ModifierType.SHIFT_MASK),keyCode:m.length===1?m.toUpperCase().charCodeAt(0):0,which:m.length===1?m.toUpperCase().charCodeAt(0):0,bubbles:!0,cancelable:!0},g=new o(`keydown`,h);return l.dispatchEvent(g),getGlobalEventTarget()?.dispatchEvent(new o(`keydown`,h)),f?.captureKeys===!0}),_.connect(`key-released`,(r,a,s,c)=>{let l=d();if(!l)return;p.pressedKeys.delete(a);let u=t(a),f={key:u,code:e(a),location:n(a),repeat:!1,altKey:!!(c&i.ModifierType.ALT_MASK),ctrlKey:!!(c&i.ModifierType.CONTROL_MASK),metaKey:!!(c&i.ModifierType.SUPER_MASK),shiftKey:!!(c&i.ModifierType.SHIFT_MASK),keyCode:u.length===1?u.toUpperCase().charCodeAt(0):0,which:u.length===1?u.toUpperCase().charCodeAt(0):0,bubbles:!0,cancelable:!0};l.dispatchEvent(new o(`keyup`,f)),getGlobalEventTarget()?.dispatchEvent(new o(`keyup`,f))}),u.add_controller(_);let v=new r.EventControllerFocus;v.connect(`enter`,()=>{let e=d();e&&(e.dispatchEvent(new a(`focus`,{bubbles:!1,cancelable:!1})),e.dispatchEvent(new a(`focusin`,{bubbles:!0,cancelable:!1})))}),v.connect(`leave`,()=>{let e=d();e&&(p.pressedKeys.clear(),e.dispatchEvent(new a(`blur`,{bubbles:!1,cancelable:!1})),e.dispatchEvent(new a(`focusout`,{bubbles:!0,cancelable:!1})),getGlobalEventTarget()?.dispatchEvent(new a(`blur`,{bubbles:!1,cancelable:!1})))}),u.add_controller(v)}export{attachEventControllers};
1
+ import"./_virtual/_rolldown/runtime.js";import{gdkKeyvalToCode as e,gdkKeyvalToKey as t,gdkKeyvalToLocation as n}from"./key-map.js";import{TouchPointerTranslator as r}from"./touch-pointers.js";import i from"gi://Gtk?version=4.0";import a from"gi://Gdk?version=4.0";import o from"gi://Graphene?version=1.0";import{FocusEvent as s,KeyboardEvent as c,MouseEvent as l,PointerEvent as u,WheelEvent as d}from"@gjsify/dom-events";const f=new Map([[a.EventType.TOUCH_BEGIN,`begin`],[a.EventType.TOUCH_UPDATE,`update`],[a.EventType.TOUCH_END,`end`],[a.EventType.TOUCH_CANCEL,`cancel`]]);function isTouchEvent(e){return e!==null&&f.has(e.get_event_type())}function pointerTypeOf(e){let t=e?.get_device()?.get_source();return t===a.InputSource.TOUCHSCREEN?`touch`:t===a.InputSource.PEN?`pen`:`mouse`}let p=!1;function sequenceKey(e,t){if(e===null)return t?`primary`:`secondary`;let n=String(e),r=n.lastIndexOf(`native@`);return r!==-1&&n.endsWith(`]`)?n.slice(r+7,-1):(p||(p=!0,console.warn(`@gjsify/event-bridge: GdkEventSequence identity is not readable on this runtime; touch pointers are keyed by the emulating-pointer flag, so a third concurrent contact merges with the second.`)),t?`primary`:`secondary`)}function surfaceToWidget(e,t,n){let r=e.get_native(),[i,a]=r.get_surface_transform(),s=t-i,c=n-a,[l,u]=r.compute_point(e,new o.Point({x:s,y:c}));return l?[u.x,u.y]:[s,c]}function getGlobalEventTarget(){return globalThis.__gjsify_globalEventTarget}const m={shiftKey:!1,ctrlKey:!1,altKey:!1,metaKey:!1};function modifiersFromState(e){return{shiftKey:!!(e&a.ModifierType.SHIFT_MASK),ctrlKey:!!(e&a.ModifierType.CONTROL_MASK),altKey:!!(e&a.ModifierType.ALT_MASK),metaKey:!!(e&a.ModifierType.SUPER_MASK)}}function extractModifiers(e){return modifiersFromState(e.get_current_event_state())}function gtkButtonToDom(e){return e===1?0:e===2?1:e===3?2:e-1}function buttonsFromModifiers(e){let t=e.get_current_event_state(),n=0;return t&a.ModifierType.BUTTON1_MASK&&(n|=1),t&a.ModifierType.BUTTON3_MASK&&(n|=2),t&a.ModifierType.BUTTON2_MASK&&(n|=4),n}function attachEventControllers(o,p,h){o.set_focusable(!0),o.set_can_focus(!0);let g={lastX:0,lastY:0,buttonsPressed:0,pressedKeys:new Set},_=new r(p,()=>getGlobalEventTarget()),v=new i.EventControllerMotion;v.connect(`motion`,(e,t,n)=>{let r=p();if(!r)return;let i=o.get_allocated_width(),a=o.get_allocated_height(),s=Math.max(0,Math.min(t,i)),c=Math.max(0,Math.min(n,a)),d=s-g.lastX,f=c-g.lastY,m=extractModifiers(v),h=buttonsFromModifiers(v),_={...m,clientX:s,clientY:c,offsetX:s,offsetY:c,screenX:s,screenY:c,movementX:d,movementY:f,buttons:h,button:0,bubbles:!0,cancelable:!0};r.dispatchEvent(new u(`pointermove`,{..._,pointerId:1,pointerType:pointerTypeOf(v.get_current_event()),isPrimary:!0})),r.dispatchEvent(new l(`mousemove`,_)),g.lastX=s,g.lastY=c}),v.connect(`enter`,(e,t,n)=>{let r=p();if(!r)return;g.lastX=t,g.lastY=n;let i={...extractModifiers(v),clientX:t,clientY:n,offsetX:t,offsetY:n,screenX:t,screenY:n,bubbles:!1,cancelable:!1};r.dispatchEvent(new u(`pointerenter`,{...i,pointerId:1,pointerType:pointerTypeOf(v.get_current_event()),isPrimary:!0})),r.dispatchEvent(new l(`mouseenter`,i)),r.dispatchEvent(new l(`mouseover`,{...i,bubbles:!0}))}),v.connect(`leave`,()=>{let e=p();if(!e)return;let t={...extractModifiers(v),clientX:g.lastX,clientY:g.lastY,bubbles:!1,cancelable:!1};e.dispatchEvent(new u(`pointerleave`,{...t,pointerId:1,pointerType:pointerTypeOf(v.get_current_event()),isPrimary:!0})),e.dispatchEvent(new l(`mouseleave`,t)),e.dispatchEvent(new l(`mouseout`,{...t,bubbles:!0}))}),o.add_controller(v);let y=new i.GestureClick;y.set_button(0),y.connect(`pressed`,(e,t,n,r)=>{if(isTouchEvent(y.get_current_event()))return;let i=p();if(!i)return;let a=gtkButtonToDom(y.get_current_button()),s=extractModifiers(y);g.buttonsPressed|=1<<a;let c={...s,clientX:n,clientY:r,offsetX:n,offsetY:r,screenX:n,screenY:r,button:a,buttons:g.buttonsPressed,detail:t,bubbles:!0,cancelable:!0};i.dispatchEvent(new u(`pointerdown`,{...c,pointerId:1,pointerType:pointerTypeOf(y.get_current_event()),isPrimary:!0})),i.dispatchEvent(new l(`mousedown`,c)),o.grab_focus()}),y.connect(`released`,(e,t,n,r)=>{if(isTouchEvent(y.get_current_event()))return;let i=p();if(!i)return;let a=gtkButtonToDom(y.get_current_button()),o=extractModifiers(y);g.buttonsPressed&=~(1<<a);let s={...o,clientX:n,clientY:r,offsetX:n,offsetY:r,screenX:n,screenY:r,button:a,buttons:g.buttonsPressed,detail:t,bubbles:!0,cancelable:!0};i.dispatchEvent(new u(`pointerup`,{...s,pointerId:1,pointerType:pointerTypeOf(y.get_current_event()),isPrimary:!0})),i.dispatchEvent(new l(`mouseup`,s)),a===0&&(i.dispatchEvent(new l(`click`,s)),t===2&&i.dispatchEvent(new l(`dblclick`,s))),a===2&&i.dispatchEvent(new l(`contextmenu`,{...s,cancelable:!0}))}),y.connect(`cancel`,(e,t)=>{let n=y.get_last_event(t);if(isTouchEvent(n)||g.buttonsPressed===0)return;g.buttonsPressed=0;let r=p();r&&r.dispatchEvent(new u(`pointercancel`,{...extractModifiers(y),clientX:g.lastX,clientY:g.lastY,offsetX:g.lastX,offsetY:g.lastY,screenX:g.lastX,screenY:g.lastY,button:-1,buttons:0,pointerId:1,pointerType:pointerTypeOf(n),isPrimary:!0,bubbles:!0,cancelable:!1}))}),o.add_controller(y);let b=new i.EventControllerScroll({flags:i.EventControllerScrollFlags.BOTH_AXES});b.connect(`scroll`,(e,t,n)=>{let r=p();if(!r)return;let i=extractModifiers(b),o=b.get_unit()===a.ScrollUnit.WHEEL,s=o?3:1,c={...i,clientX:g.lastX,clientY:g.lastY,offsetX:g.lastX,offsetY:g.lastY,screenX:g.lastX,screenY:g.lastY,deltaX:t*s,deltaY:n*s,deltaZ:0,deltaMode:o?d.DOM_DELTA_LINE:d.DOM_DELTA_PIXEL,bubbles:!0,cancelable:!0};return r.dispatchEvent(new d(`wheel`,c)),!1}),o.add_controller(b);let x=new i.EventControllerKey;x.connect(`key-pressed`,(r,i,o,s)=>{let l=p();if(!l)return!1;let u=g.pressedKeys.has(i);g.pressedKeys.add(i);let d=t(i),f={key:d,code:e(i),location:n(i),repeat:u,altKey:!!(s&a.ModifierType.ALT_MASK),ctrlKey:!!(s&a.ModifierType.CONTROL_MASK),metaKey:!!(s&a.ModifierType.SUPER_MASK),shiftKey:!!(s&a.ModifierType.SHIFT_MASK),keyCode:d.length===1?d.toUpperCase().charCodeAt(0):0,which:d.length===1?d.toUpperCase().charCodeAt(0):0,bubbles:!0,cancelable:!0},m=new c(`keydown`,f);return l.dispatchEvent(m),getGlobalEventTarget()?.dispatchEvent(new c(`keydown`,f)),h?.captureKeys===!0}),x.connect(`key-released`,(r,i,o,s)=>{let l=p();if(!l)return;g.pressedKeys.delete(i);let u=t(i),d={key:u,code:e(i),location:n(i),repeat:!1,altKey:!!(s&a.ModifierType.ALT_MASK),ctrlKey:!!(s&a.ModifierType.CONTROL_MASK),metaKey:!!(s&a.ModifierType.SUPER_MASK),shiftKey:!!(s&a.ModifierType.SHIFT_MASK),keyCode:u.length===1?u.toUpperCase().charCodeAt(0):0,which:u.length===1?u.toUpperCase().charCodeAt(0):0,bubbles:!0,cancelable:!0};l.dispatchEvent(new c(`keyup`,d)),getGlobalEventTarget()?.dispatchEvent(new c(`keyup`,d))}),o.add_controller(x);let S=new i.EventControllerFocus;S.connect(`enter`,()=>{let e=p();e&&(e.dispatchEvent(new s(`focus`,{bubbles:!1,cancelable:!1})),e.dispatchEvent(new s(`focusin`,{bubbles:!0,cancelable:!1})))}),S.connect(`leave`,()=>{let e=p();e&&(g.pressedKeys.clear(),e.dispatchEvent(new s(`blur`,{bubbles:!1,cancelable:!1})),e.dispatchEvent(new s(`focusout`,{bubbles:!0,cancelable:!1})),getGlobalEventTarget()?.dispatchEvent(new s(`blur`,{bubbles:!1,cancelable:!1})))}),o.add_controller(S);let C=new i.EventControllerLegacy;C.connect(`event`,(e,t)=>{let n=t.get_event_type(),r=f.get(n);if(r===void 0)return n===a.EventType.GRAB_BROKEN&&_.activeCount>0&&t.get_grab_surface()!==o.get_native().get_surface()&&_.cancelAll(modifiersFromState(t.get_modifier_state())),!1;let[,i,s]=t.get_position(),[c,l]=surfaceToWidget(o,i,s),u=t.get_emulating_pointer();return _.handle({...modifiersFromState(t.get_modifier_state()),phase:r,key:sequenceKey(t.get_event_sequence(),u),x:c,y:l,isPrimary:u}),r===`begin`&&o.grab_focus(),!1}),o.add_controller(C),o.connect(`unmap`,()=>_.cancelAll(m)),o.connect(`state-flags-changed`,(e,t)=>{let n=i.StateFlags.INSENSITIVE;o.get_state_flags()&n&&!(t&n)&&_.cancelAll(m)})}export{attachEventControllers,isTouchEvent,pointerTypeOf,sequenceKey,surfaceToWidget};
@@ -0,0 +1 @@
1
+ import"./_virtual/_rolldown/runtime.js";import{MouseEvent as e,PointerEvent as t}from"@gjsify/dom-events";const n=.5;var TouchPointerTranslator=class{constructor(e,t){this.getElement=e,this.getWindowTarget=t,this.active=new Map,this.nextId=2}get activeCount(){return this.active.size}handle(e){switch(e.phase){case`begin`:this.begin(e);break;case`update`:this.update(e);break;case`end`:this.end(e);break;case`cancel`:this.cancel(e);break}}cancelAll(e){for(let[t,n]of this.active)this.cancel({...e,phase:`cancel`,key:t,x:n.lastX,y:n.lastY,isPrimary:n.isPrimary})}begin(r){this.active.has(r.key)&&this.cancel(r);let i=this.getElement();if(!i)return;let a={id:this.nextId++,isPrimary:r.isPrimary,lastX:r.x,lastY:r.y,preventMouse:!1};this.active.set(r.key,a);let o=this.pointerInit(r,a,-1,0,0),s=this.mouseInit(r,0,0);a.isPrimary&&i.dispatchEvent(new e(`mousemove`,s)),i.dispatchEvent(new t(`pointerover`,o)),i.dispatchEvent(new t(`pointerenter`,{...o,bubbles:!1,cancelable:!1})),a.isPrimary&&(i.dispatchEvent(new e(`mouseover`,s)),i.dispatchEvent(new e(`mouseenter`,{...s,bubbles:!1,cancelable:!1})));let c=new t(`pointerdown`,this.pointerInit(r,a,0,1,n));i.dispatchEvent(c),a.isPrimary&&(a.preventMouse=c.defaultPrevented,a.preventMouse||i.dispatchEvent(new e(`mousedown`,{...this.mouseInit(r,0,1),detail:1})))}update(r){let i=this.active.get(r.key);if(!i)return;let a=this.getElement();if(!a)return;let o=r.x-i.lastX,s=r.y-i.lastY;i.lastX=r.x,i.lastY=r.y,a.dispatchEvent(new t(`pointermove`,{...this.pointerInit(r,i,-1,1,n),movementX:o,movementY:s})),i.isPrimary&&!i.preventMouse&&a.dispatchEvent(new e(`mousemove`,{...this.mouseInit(r,0,1),movementX:o,movementY:s}))}end(n){let r=this.active.get(n.key);if(!r)return;this.active.delete(n.key);let i=this.getElement();if(!i)return;let a=this.pointerInit(n,r,0,0,0),o=this.mouseInit(n,0,0),s=r.isPrimary&&!r.preventMouse;i.dispatchEvent(new t(`pointerup`,a)),s&&i.dispatchEvent(new e(`mouseup`,{...o,detail:1})),this.leave(i,r,{...a,button:-1},o),r.isPrimary&&i.dispatchEvent(new t(`click`,{...a,detail:1}))}cancel(n){let r=this.active.get(n.key);if(!r)return;this.active.delete(n.key);let i=this.getElement();if(!i)return;let a={...n,x:r.lastX,y:r.lastY},o=this.pointerInit(a,r,-1,0,0),s=this.mouseInit(a,0,0);i.dispatchEvent(new t(`pointercancel`,{...o,cancelable:!1})),r.isPrimary&&!r.preventMouse&&this.getWindowTarget()?.dispatchEvent(new e(`mouseup`,{...s,detail:1})),this.leave(i,r,o,s)}leave(n,r,i,a){n.dispatchEvent(new t(`pointerout`,i)),n.dispatchEvent(new t(`pointerleave`,{...i,bubbles:!1,cancelable:!1})),r.isPrimary&&(n.dispatchEvent(new e(`mouseout`,a)),n.dispatchEvent(new e(`mouseleave`,{...a,bubbles:!1,cancelable:!1})))}mouseInit(e,t,n){return{shiftKey:e.shiftKey,ctrlKey:e.ctrlKey,altKey:e.altKey,metaKey:e.metaKey,clientX:e.x,clientY:e.y,offsetX:e.x,offsetY:e.y,screenX:e.x,screenY:e.y,button:t,buttons:n,bubbles:!0,cancelable:!0}}pointerInit(e,t,n,r,i){return{...this.mouseInit(e,n,r),pointerId:t.id,pointerType:`touch`,isPrimary:t.isPrimary,pressure:i}}};export{TouchPointerTranslator};
@@ -1,15 +1,50 @@
1
1
  import Gtk from 'gi://Gtk?version=4.0';
2
+ import Gdk from 'gi://Gdk?version=4.0';
2
3
  import { type Event as OurEvent } from '@gjsify/dom-events';
4
+ /**
5
+ * True for an event GDK synthesised from a touchscreen contact. A synthetic `emit()` on a
6
+ * controller has no current event and reads as a mouse, which is what the tests and the
7
+ * node-gi fixture drive.
8
+ */
9
+ export declare function isTouchEvent(event: Gdk.Event | null): boolean;
10
+ /** The DOM pointerType a GDK device source maps to; no event means a synthetic mouse. */
11
+ export declare function pointerTypeOf(event: Gdk.Event | null): 'mouse' | 'pen' | 'touch';
12
+ /**
13
+ * A key equal for every event of one contact and distinct across concurrent contacts.
14
+ *
15
+ * `GdkEventSequence` is an opaque boxed pointer (the Wayland backend passes the wl_touch id + 1;
16
+ * measured `0x2a1`/`0x2a2` for two fingers on mutter 48) with no accessor at all, and GJS
17
+ * creates a fresh wrapper for every call, so two wrappers of the
18
+ * same contact are never `===`. The pointer is still legible: GJS prints every wrapper as
19
+ * `[boxed instance wrapper GIName:Gdk.EventSequence jsobj@0x… native@0x…]`, and the `native@`
20
+ * address is the sequence itself; GTK registers the type with an identity copy, so every wrapper
21
+ * of one contact prints the same address. A runtime that prints something else (node-gi) falls
22
+ * back to the emulating-pointer flag, which still separates two concurrent contacts — measured
23
+ * 330/249 true/false over real two-finger episodes — and says so once instead of silently merging
24
+ * fingers. A null sequence is GTK's own "the pointer" sequence, not a legibility problem, so it
25
+ * takes the flag key quietly.
26
+ */
27
+ export declare function sequenceKey(sequence: Gdk.EventSequence | null, isPrimary: boolean): string;
28
+ /**
29
+ * GDK positions are surface-relative. GTK hands every controller widget-local coordinates,
30
+ * but `EventControllerLegacy` drops them from its signal, so this repeats the translation
31
+ * gtkmain.c performs before it runs a controller: subtract the native's surface transform,
32
+ * then map through the widget tree with `compute_point`.
33
+ */
34
+ export declare function surfaceToWidget(widget: Gtk.Widget, surfaceX: number, surfaceY: number): [number, number];
3
35
  /**
4
36
  * Attach GTK event controllers to a widget and dispatch standard DOM events
5
37
  * on the HTMLElement returned by `getElement()`.
6
38
  *
7
- * Controllers attached:
39
+ * Controllers attached, in this order (the node-gi fixture reads them back by add order):
8
40
  * - EventControllerMotion → pointermove/mousemove, pointerenter/mouseenter, pointerleave/mouseleave
9
- * - GestureClick → pointerdown/mousedown, pointerup/mouseup, click, dblclick, contextmenu
41
+ * - GestureClick → pointerdown/mousedown, pointerup/mouseup, click, dblclick, contextmenu,
42
+ * pointercancel — mouse and pen only
10
43
  * - EventControllerScroll → wheel
11
44
  * - EventControllerKey → keydown, keyup
12
45
  * - EventControllerFocus → focus/focusin, blur/focusout
46
+ * - EventControllerLegacy → the touchscreen: one PointerEvent stream per contact with a distinct
47
+ * pointerId, plus the compatibility mouse events the primary contact owes legacy code
13
48
  *
14
49
  * @param widget The GTK widget to attach controllers to
15
50
  * @param getElement Returns the HTMLElement to dispatch events on (may be null before init)
@@ -0,0 +1,58 @@
1
+ import { type Event as OurEvent } from '@gjsify/dom-events';
2
+ export type TouchPhase = 'begin' | 'update' | 'end' | 'cancel';
3
+ export interface ModifierKeys {
4
+ shiftKey: boolean;
5
+ ctrlKey: boolean;
6
+ altKey: boolean;
7
+ metaKey: boolean;
8
+ }
9
+ /** One raw contact frame as the GTK adapter extracted it — widget-local and unrounded. */
10
+ export interface TouchFrame extends ModifierKeys {
11
+ phase: TouchPhase;
12
+ /** Equal for every frame of one contact, distinct across concurrent contacts. */
13
+ key: string;
14
+ x: number;
15
+ y: number;
16
+ /** GDK's pointer-emulating contact, which is the spec's primary pointer. */
17
+ isPrimary: boolean;
18
+ }
19
+ export interface DispatchTarget {
20
+ dispatchEvent(event: OurEvent): boolean;
21
+ }
22
+ /**
23
+ * Translates a stream of touch frames into the pointer events plus compatibility mouse events the
24
+ * spec prescribes for a device without hover: every pointer event is followed by its mouse
25
+ * counterpart, only the primary contact produces mouse events, and a cancelled primary
26
+ * `pointerdown` suppresses mousedown/mousemove/mouseup — never the transition events, never `click`.
27
+ *
28
+ * Idempotent by construction: a frame for a contact that is not active (a late or repeated end, a
29
+ * cancel already fanned out by GTK) emits nothing, and a `begin` for a contact still active first
30
+ * cancels the stale one — a lost `TOUCH_END` therefore heals on the next contact instead of
31
+ * leaving a pointer pressed for the life of the widget.
32
+ */
33
+ export declare class TouchPointerTranslator {
34
+ private readonly getElement;
35
+ /** The spec's "at the window" target, where `pointercancel` sends its `mouseup`. */
36
+ private readonly getWindowTarget;
37
+ private readonly active;
38
+ private nextId;
39
+ constructor(getElement: () => DispatchTarget | null,
40
+ /** The spec's "at the window" target, where `pointercancel` sends its `mouseup`. */
41
+ getWindowTarget: () => DispatchTarget | undefined);
42
+ /** Number of contacts currently down. */
43
+ get activeCount(): number;
44
+ handle(frame: TouchFrame): void;
45
+ /**
46
+ * Every contact ends in `pointercancel` — for a grab that moved to another surface, or a widget
47
+ * that unmapped or turned insensitive under the finger.
48
+ */
49
+ cancelAll(modifiers: ModifierKeys): void;
50
+ private begin;
51
+ private update;
52
+ private end;
53
+ private cancel;
54
+ /** The out/leave pair a device without hover fires after pointerup and pointercancel alike. */
55
+ private leave;
56
+ private mouseInit;
57
+ private pointerInit;
58
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/event-bridge",
3
- "version": "0.48.0",
3
+ "version": "0.50.0",
4
4
  "description": "Bridge GTK event controllers to standard DOM events (MouseEvent, KeyboardEvent, etc.)",
5
5
  "type": "module",
6
6
  "module": "lib/esm/index.js",
@@ -33,15 +33,16 @@
33
33
  "bridge"
34
34
  ],
35
35
  "dependencies": {
36
- "@girs/gdk-4.0": "4.6.0",
37
- "@girs/gjs": "4.6.0",
38
- "@girs/glib-2.0": "4.6.0",
39
- "@girs/gtk-4.0": "4.6.0",
40
- "@gjsify/dom-events": "^0.48.0"
36
+ "@girs/gdk-4.0": "5.0.0",
37
+ "@girs/gjs": "5.0.0",
38
+ "@girs/glib-2.0": "5.0.0",
39
+ "@girs/graphene-1.0": "5.0.0",
40
+ "@girs/gtk-4.0": "5.0.0",
41
+ "@gjsify/dom-events": "^0.50.0"
41
42
  },
42
43
  "devDependencies": {
43
- "@gjsify/cli": "^0.48.0",
44
- "@gjsify/unit": "^0.48.0",
44
+ "@gjsify/cli": "^0.50.0",
45
+ "@gjsify/unit": "^0.50.0",
45
46
  "@types/node": "^25.9.2",
46
47
  "typescript": "^6.0.3"
47
48
  },