@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
|
|
package/lib/esm/event-bridge.js
CHANGED
|
@@ -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
|
|
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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/event-bridge",
|
|
3
|
-
"version": "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": "
|
|
37
|
-
"@girs/gjs": "
|
|
38
|
-
"@girs/glib-2.0": "
|
|
39
|
-
"@girs/
|
|
40
|
-
"@
|
|
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.
|
|
44
|
-
"@gjsify/unit": "^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
|
},
|