@gtkx/testing 1.0.0-rc.3 → 1.0.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.
Files changed (215) hide show
  1. package/README.md +8 -9
  2. package/dist/accessible-native.d.ts +16 -0
  3. package/dist/accessible-native.d.ts.map +1 -0
  4. package/dist/accessible-native.js +206 -0
  5. package/dist/accessible-native.js.map +1 -0
  6. package/dist/act.d.ts +2 -0
  7. package/dist/act.d.ts.map +1 -1
  8. package/dist/act.js.map +1 -1
  9. package/dist/build-queries.d.ts +27 -10
  10. package/dist/build-queries.d.ts.map +1 -1
  11. package/dist/build-queries.js +39 -30
  12. package/dist/build-queries.js.map +1 -1
  13. package/dist/cleanup-registry.d.ts +1 -1
  14. package/dist/cleanup-registry.d.ts.map +1 -1
  15. package/dist/cleanup-registry.js.map +1 -1
  16. package/dist/config.d.ts +1 -0
  17. package/dist/config.d.ts.map +1 -1
  18. package/dist/config.js +3 -3
  19. package/dist/config.js.map +1 -1
  20. package/dist/errors.d.ts +10 -1
  21. package/dist/errors.d.ts.map +1 -1
  22. package/dist/errors.js +12 -3
  23. package/dist/errors.js.map +1 -1
  24. package/dist/fire-event.d.ts +35 -6
  25. package/dist/fire-event.d.ts.map +1 -1
  26. package/dist/fire-event.js +23 -7
  27. package/dist/fire-event.js.map +1 -1
  28. package/dist/frame-sync.d.ts +2 -1
  29. package/dist/frame-sync.d.ts.map +1 -1
  30. package/dist/frame-sync.js +15 -5
  31. package/dist/frame-sync.js.map +1 -1
  32. package/dist/harness-window.d.ts +4 -0
  33. package/dist/harness-window.d.ts.map +1 -0
  34. package/dist/harness-window.js +6 -0
  35. package/dist/harness-window.js.map +1 -0
  36. package/dist/index.d.ts +14 -7
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +9 -5
  39. package/dist/index.js.map +1 -1
  40. package/dist/internal.d.ts +4 -0
  41. package/dist/internal.d.ts.map +1 -0
  42. package/dist/internal.js +4 -0
  43. package/dist/internal.js.map +1 -0
  44. package/dist/matchers.d.ts +125 -39
  45. package/dist/matchers.d.ts.map +1 -1
  46. package/dist/matchers.js +156 -18
  47. package/dist/matchers.js.map +1 -1
  48. package/dist/pretty-format.d.ts +2 -0
  49. package/dist/pretty-format.d.ts.map +1 -0
  50. package/dist/pretty-format.js +2 -0
  51. package/dist/pretty-format.js.map +1 -0
  52. package/dist/pretty-widget.d.ts +7 -3
  53. package/dist/pretty-widget.d.ts.map +1 -1
  54. package/dist/pretty-widget.js +74 -57
  55. package/dist/pretty-widget.js.map +1 -1
  56. package/dist/production.d.ts +8 -0
  57. package/dist/production.d.ts.map +1 -0
  58. package/dist/production.js +34 -0
  59. package/dist/production.js.map +1 -0
  60. package/dist/queries.d.ts +99 -56
  61. package/dist/queries.d.ts.map +1 -1
  62. package/dist/queries.js +123 -83
  63. package/dist/queries.js.map +1 -1
  64. package/dist/query-helpers.d.ts +44 -0
  65. package/dist/query-helpers.d.ts.map +1 -0
  66. package/dist/query-helpers.js +57 -0
  67. package/dist/query-helpers.js.map +1 -0
  68. package/dist/render-children.d.ts +9 -0
  69. package/dist/render-children.d.ts.map +1 -0
  70. package/dist/render-children.js +11 -0
  71. package/dist/render-children.js.map +1 -0
  72. package/dist/render.d.ts.map +1 -1
  73. package/dist/render.js +13 -11
  74. package/dist/render.js.map +1 -1
  75. package/dist/role-helpers.d.ts +12 -3
  76. package/dist/role-helpers.d.ts.map +1 -1
  77. package/dist/role-helpers.js +13 -4
  78. package/dist/role-helpers.js.map +1 -1
  79. package/dist/role-naming.d.ts +5 -0
  80. package/dist/role-naming.d.ts.map +1 -0
  81. package/dist/role-naming.js +96 -0
  82. package/dist/role-naming.js.map +1 -0
  83. package/dist/screenshot.d.ts +12 -20
  84. package/dist/screenshot.d.ts.map +1 -1
  85. package/dist/screenshot.js +183 -91
  86. package/dist/screenshot.js.map +1 -1
  87. package/dist/suggestions.d.ts +3 -0
  88. package/dist/suggestions.d.ts.map +1 -1
  89. package/dist/suggestions.js.map +1 -1
  90. package/dist/traversal.d.ts +9 -1
  91. package/dist/traversal.d.ts.map +1 -1
  92. package/dist/traversal.js +26 -6
  93. package/dist/traversal.js.map +1 -1
  94. package/dist/types.d.ts +67 -17
  95. package/dist/types.d.ts.map +1 -1
  96. package/dist/types.js.map +1 -1
  97. package/dist/user-event/adjustment.d.ts +16 -0
  98. package/dist/user-event/adjustment.d.ts.map +1 -1
  99. package/dist/user-event/adjustment.js +34 -2
  100. package/dist/user-event/adjustment.js.map +1 -1
  101. package/dist/user-event/click.d.ts +45 -3
  102. package/dist/user-event/click.d.ts.map +1 -1
  103. package/dist/user-event/click.js +177 -39
  104. package/dist/user-event/click.js.map +1 -1
  105. package/dist/user-event/column-header-click.d.ts +4 -0
  106. package/dist/user-event/column-header-click.d.ts.map +1 -0
  107. package/dist/user-event/column-header-click.js +65 -0
  108. package/dist/user-event/column-header-click.js.map +1 -0
  109. package/dist/user-event/controller.d.ts +29 -0
  110. package/dist/user-event/controller.d.ts.map +1 -1
  111. package/dist/user-event/controller.js +28 -0
  112. package/dist/user-event/controller.js.map +1 -1
  113. package/dist/user-event/event-wrapper.d.ts.map +1 -1
  114. package/dist/user-event/event-wrapper.js +2 -1
  115. package/dist/user-event/event-wrapper.js.map +1 -1
  116. package/dist/user-event/gesture.d.ts +51 -0
  117. package/dist/user-event/gesture.d.ts.map +1 -1
  118. package/dist/user-event/gesture.js +47 -4
  119. package/dist/user-event/gesture.js.map +1 -1
  120. package/dist/user-event/index.d.ts +70 -2
  121. package/dist/user-event/index.d.ts.map +1 -1
  122. package/dist/user-event/index.js +43 -26
  123. package/dist/user-event/index.js.map +1 -1
  124. package/dist/user-event/indexed-children.d.ts +10 -0
  125. package/dist/user-event/indexed-children.d.ts.map +1 -0
  126. package/dist/user-event/indexed-children.js +50 -0
  127. package/dist/user-event/indexed-children.js.map +1 -0
  128. package/dist/user-event/keyboard.d.ts +4 -2
  129. package/dist/user-event/keyboard.d.ts.map +1 -1
  130. package/dist/user-event/keyboard.js +46 -16
  131. package/dist/user-event/keyboard.js.map +1 -1
  132. package/dist/user-event/list-row-click.d.ts +4 -0
  133. package/dist/user-event/list-row-click.d.ts.map +1 -0
  134. package/dist/user-event/list-row-click.js +39 -0
  135. package/dist/user-event/list-row-click.js.map +1 -0
  136. package/dist/user-event/native-click.d.ts +6 -0
  137. package/dist/user-event/native-click.d.ts.map +1 -0
  138. package/dist/user-event/native-click.js +32 -0
  139. package/dist/user-event/native-click.js.map +1 -0
  140. package/dist/user-event/pointer.d.ts +1 -1
  141. package/dist/user-event/pointer.d.ts.map +1 -1
  142. package/dist/user-event/pointer.js +37 -13
  143. package/dist/user-event/pointer.js.map +1 -1
  144. package/dist/user-event/selection.d.ts +18 -0
  145. package/dist/user-event/selection.d.ts.map +1 -1
  146. package/dist/user-event/selection.js +56 -30
  147. package/dist/user-event/selection.js.map +1 -1
  148. package/dist/user-event/state.d.ts +1 -1
  149. package/dist/user-event/state.d.ts.map +1 -1
  150. package/dist/user-event/state.js +1 -1
  151. package/dist/user-event/state.js.map +1 -1
  152. package/dist/user-event/text.d.ts +34 -3
  153. package/dist/user-event/text.d.ts.map +1 -1
  154. package/dist/user-event/text.js +94 -17
  155. package/dist/user-event/text.js.map +1 -1
  156. package/dist/wait-for.d.ts +2 -0
  157. package/dist/wait-for.d.ts.map +1 -1
  158. package/dist/wait-for.js.map +1 -1
  159. package/dist/widget-accessible-properties.d.ts +14 -10
  160. package/dist/widget-accessible-properties.d.ts.map +1 -1
  161. package/dist/widget-accessible-properties.js +135 -173
  162. package/dist/widget-accessible-properties.js.map +1 -1
  163. package/dist/widget-getters.d.ts +8 -0
  164. package/dist/widget-getters.d.ts.map +1 -0
  165. package/dist/widget-getters.js +19 -0
  166. package/dist/widget-getters.js.map +1 -0
  167. package/dist/within.d.ts +3 -2
  168. package/dist/within.d.ts.map +1 -1
  169. package/dist/within.js +3 -2
  170. package/dist/within.js.map +1 -1
  171. package/package.json +13 -8
  172. package/src/accessible-native.ts +333 -0
  173. package/src/act.ts +2 -0
  174. package/src/build-queries.ts +66 -48
  175. package/src/cleanup-registry.ts +1 -1
  176. package/src/config.ts +4 -3
  177. package/src/errors.ts +14 -3
  178. package/src/fire-event.ts +61 -8
  179. package/src/frame-sync.ts +21 -5
  180. package/src/harness-window.ts +9 -0
  181. package/src/index.ts +18 -11
  182. package/src/internal.ts +11 -0
  183. package/src/matchers.ts +466 -64
  184. package/src/pretty-format.ts +1 -0
  185. package/src/pretty-widget.ts +100 -72
  186. package/src/production.ts +52 -0
  187. package/src/queries.ts +128 -89
  188. package/src/query-helpers.ts +97 -0
  189. package/src/render-children.ts +25 -0
  190. package/src/render.tsx +18 -13
  191. package/src/role-helpers.ts +14 -4
  192. package/src/role-naming.ts +99 -0
  193. package/src/screenshot.ts +272 -103
  194. package/src/suggestions.ts +3 -0
  195. package/src/traversal.ts +49 -6
  196. package/src/types.ts +69 -19
  197. package/src/user-event/adjustment.ts +42 -2
  198. package/src/user-event/click.ts +248 -46
  199. package/src/user-event/column-header-click.ts +90 -0
  200. package/src/user-event/controller.ts +29 -0
  201. package/src/user-event/event-wrapper.ts +3 -1
  202. package/src/user-event/gesture.ts +57 -6
  203. package/src/user-event/index.ts +119 -29
  204. package/src/user-event/indexed-children.ts +75 -0
  205. package/src/user-event/keyboard.ts +70 -20
  206. package/src/user-event/list-row-click.ts +57 -0
  207. package/src/user-event/native-click.ts +46 -0
  208. package/src/user-event/pointer.ts +52 -13
  209. package/src/user-event/selection.ts +74 -33
  210. package/src/user-event/state.ts +2 -2
  211. package/src/user-event/text.ts +121 -23
  212. package/src/wait-for.ts +2 -0
  213. package/src/widget-accessible-properties.ts +183 -214
  214. package/src/widget-getters.ts +32 -0
  215. package/src/within.ts +3 -2
@@ -1,8 +1,29 @@
1
+ import * as Gdk from "@gtkx/gi/gdk";
1
2
  import * as Gtk from "@gtkx/gi/gtk";
2
- import { getOrCreateControllers, queryController } from "./controller.js";
3
+ import { hasSignalListener } from "@gtkx/runtime/internal";
4
+ import { callBooleanGetter, getCallableMethod, hasWidgetMethod } from "../widget-getters.js";
5
+ import { queryAllControllers } from "./controller.js";
3
6
  import { wrapEvent } from "./event-wrapper.js";
7
+ import { hasIndexedChildren, selectContainerChild, SELECTED_PROBE, unselectOtherChildren } from "./indexed-children.js";
8
+ import { isClickTransparent, type NativeClick, nativeClickFor } from "./native-click.js";
4
9
 
5
- const getPressPoint = (widget: Gtk.Widget): { x: number; y: number } => {
10
+ type PressPoint = { x: number; y: number };
11
+ type ClickPhase = "pressed" | "released";
12
+ type ClickSite = { gestures: Gtk.GestureClick[]; point: PressPoint };
13
+ type ClickScope = { isClicked: boolean; isInternalAllowed: boolean };
14
+
15
+ type ClickTarget = {
16
+ widget: Gtk.Widget;
17
+ container: Gtk.Widget | null;
18
+ gestures: Gtk.GestureClick[];
19
+ isClaiming: boolean;
20
+ native: NativeClick | null;
21
+ };
22
+
23
+ const CLICK_SIGNALS = ["pressed", "released"];
24
+ const PRIMARY_BUTTONS: Set<number> = new Set([0, Gdk.BUTTON_PRIMARY]);
25
+
26
+ const getCenterPoint = (widget: Gtk.Widget): PressPoint => {
6
27
  const width = widget.getWidth();
7
28
  const height = widget.getHeight();
8
29
 
@@ -13,87 +34,268 @@ const getPressPoint = (widget: Gtk.Widget): { x: number; y: number } => {
13
34
  return { x: 0, y: 0 };
14
35
  };
15
36
 
16
- const emitGesture = (widget: Gtk.Widget, controllers: Gtk.GestureClick[], nPress: number, signal: string): void => {
17
- const { x, y } = getPressPoint(widget);
37
+ const getClickPoint = (clicked: Gtk.Widget, carrier: Gtk.Widget): PressPoint => {
38
+ if (clicked === carrier) {
39
+ return getCenterPoint(clicked);
40
+ }
41
+
42
+ const [isComputed, bounds] = clicked.computeBounds(carrier);
18
43
 
44
+ if (!isComputed) {
45
+ return getCenterPoint(carrier);
46
+ }
47
+
48
+ return { x: bounds.getX() + bounds.getWidth() / 2, y: bounds.getY() + bounds.getHeight() / 2 };
49
+ };
50
+
51
+ const emitGesture = (point: PressPoint, controllers: Gtk.GestureClick[], nPress: number, signal: string): void => {
19
52
  for (const controller of controllers) {
20
- controller.emit(signal, nPress, x, y);
53
+ controller.emit(signal, nPress, point.x, point.y);
21
54
  }
22
55
  };
23
56
 
24
- const emitPress = (widget: Gtk.Widget, controllers: Gtk.GestureClick[], nPress: number): void => {
25
- emitGesture(widget, controllers, nPress, "pressed");
57
+ const emitClickPhase = (
58
+ widget: Gtk.Widget,
59
+ controllers: Gtk.GestureClick[],
60
+ nPress: number,
61
+ phase: ClickPhase,
62
+ ): void => {
63
+ emitGesture(getCenterPoint(widget), controllers, nPress, phase);
26
64
  };
27
65
 
28
- const emitRelease = (widget: Gtk.Widget, controllers: Gtk.GestureClick[], nPress: number): void => {
29
- emitGesture(widget, controllers, nPress, "released");
66
+ const emitSitePhase = (sites: ClickSite[], nPress: number, phase: ClickPhase): void => {
67
+ for (const site of sites) {
68
+ emitGesture(site.point, site.gestures, nPress, phase);
69
+ }
30
70
  };
31
71
 
32
- const emitClickSequence = (widget: Gtk.Widget, target: Gtk.Widget, nPress: number): Promise<void> =>
33
- wrapEvent(widget, () => {
34
- const controllers = getOrCreateControllers(target, Gtk.GestureClick);
72
+ const emitClickSequence = (sites: ClickSite[], nPress: number): void => {
73
+ for (let i = 1; i <= nPress; i++) {
74
+ emitSitePhase(sites, i, "pressed");
75
+ emitSitePhase(sites, i, "released");
76
+ }
77
+ };
35
78
 
36
- for (let i = 1; i <= nPress; i++) {
37
- emitPress(target, controllers, i);
38
- emitRelease(target, controllers, i);
39
- }
40
- });
79
+ const containerFor = (widget: Gtk.Widget): Gtk.Widget | null => {
80
+ const parent = widget.getParent();
41
81
 
42
- const findClickableAncestor = (widget: Gtk.Widget): Gtk.Widget | null => {
43
- let current = widget.getParent();
82
+ return parent !== null && hasWidgetMethod(widget, SELECTED_PROBE) && hasIndexedChildren(parent) ? parent : null;
83
+ };
44
84
 
45
- while (current) {
46
- if (current instanceof Gtk.Button || queryController(current, Gtk.GestureClick) !== null) {
47
- return current;
48
- }
85
+ const hasClickHandlers = (gesture: Gtk.GestureClick): boolean => hasSignalListener(gesture, CLICK_SIGNALS);
86
+ const isInternalGesture = (gesture: Gtk.GestureClick): boolean => !hasSignalListener(gesture);
87
+ const isPrimaryGesture = (gesture: Gtk.GestureClick): boolean => PRIMARY_BUTTONS.has(gesture.getButton());
49
88
 
50
- current = current.getParent();
89
+ const clickGestures = (widget: Gtk.Widget): Gtk.GestureClick[] =>
90
+ queryAllControllers(widget, Gtk.GestureClick).filter((gesture) => isPrimaryGesture(gesture));
91
+
92
+ const getAuthoredClickGestures = (widget: Gtk.Widget): Gtk.GestureClick[] =>
93
+ clickGestures(widget).filter((gesture) => hasClickHandlers(gesture));
94
+
95
+ const isClaimingTarget = (widget: Gtk.Widget): boolean =>
96
+ widget instanceof Gtk.Button || containerFor(widget) !== null;
97
+
98
+ const gestureTargetFor = (widget: Gtk.Widget, isInternalAllowed: boolean): ClickTarget | null => {
99
+ const gestures = clickGestures(widget);
100
+
101
+ if (isClaimingTarget(widget)) {
102
+ return { widget, container: containerFor(widget), gestures, isClaiming: true, native: null };
103
+ }
104
+
105
+ const authored = gestures.filter((gesture) => hasClickHandlers(gesture));
106
+
107
+ if (authored.length > 0) {
108
+ return { widget, container: null, gestures: authored, isClaiming: false, native: null };
109
+ }
110
+
111
+ if (isInternalAllowed && gestures.some((gesture) => isInternalGesture(gesture))) {
112
+ return { widget, container: null, gestures, isClaiming: true, native: null };
51
113
  }
52
114
 
53
115
  return null;
54
116
  };
55
117
 
56
- const tryActivate = async (widget: Gtk.Widget): Promise<boolean> => {
57
- if (widget.getAccessibleRole() === Gtk.AccessibleRole.LABEL) {
58
- return false;
118
+ const clickTargetFor = (widget: Gtk.Widget, scope: ClickScope): ClickTarget | null => {
119
+ if (isClickTransparent(widget)) {
120
+ return null;
59
121
  }
60
122
 
61
- let isActivated = false;
123
+ const native = nativeClickFor(widget, scope.isClicked);
62
124
 
63
- await wrapEvent(widget, () => {
64
- isActivated = widget.activate();
65
- });
125
+ if (native !== null) {
126
+ return { widget, container: null, gestures: getAuthoredClickGestures(widget), isClaiming: true, native };
127
+ }
66
128
 
67
- return isActivated;
129
+ return gestureTargetFor(widget, scope.isInternalAllowed);
68
130
  };
69
131
 
70
- const click = async (widget: Gtk.Widget): Promise<void> => {
71
- if (widget instanceof Gtk.Button) {
72
- await emitClickSequence(widget, widget, 1);
132
+ const isSelfClickTarget = (widget: Gtk.Widget): boolean =>
133
+ isClickTransparent(widget) || clickTargetFor(widget, { isClicked: true, isInternalAllowed: false }) !== null;
134
+
135
+ const nextClickWidget = (current: Gtk.Widget, target: ClickTarget | null): Gtk.Widget | null =>
136
+ target?.isClaiming === true ? null : current.getParent();
137
+
138
+ const collectClickTargets = (widget: Gtk.Widget): ClickTarget[] => {
139
+ const targets: ClickTarget[] = [];
140
+ let current: Gtk.Widget | null = widget;
141
+
142
+ while (current !== null) {
143
+ const isClicked = current === widget;
144
+ const scope = { isClicked, isInternalAllowed: !isClicked && targets.length === 0 };
145
+ const target = clickTargetFor(current, scope);
146
+ const next = nextClickWidget(current, target);
147
+
148
+ if (target !== null) {
149
+ targets.push(target);
150
+ }
151
+
152
+ current = next;
153
+ }
154
+
155
+ return targets;
156
+ };
157
+
158
+ const targetSites = (clicked: Gtk.Widget, target: ClickTarget): ClickSite[] => {
159
+ const { container } = target;
160
+ const site = { gestures: target.gestures, point: getClickPoint(clicked, target.widget) };
161
+
162
+ if (container === null) {
163
+ return [site];
164
+ }
165
+
166
+ return [site, { gestures: getAuthoredClickGestures(container), point: getClickPoint(clicked, container) }];
167
+ };
73
168
 
169
+ const isSingleClickActivating = (container: Gtk.Widget): boolean =>
170
+ callBooleanGetter(container, "getActivateOnSingleClick") ?? true;
171
+
172
+ const isMultipleSelection = (container: Gtk.Widget): boolean =>
173
+ getCallableMethod<[], Gtk.SelectionMode>(container, "getSelectionMode")?.() === Gtk.SelectionMode.MULTIPLE;
174
+
175
+ const isChildSelectable = (child: Gtk.Widget): boolean => callBooleanGetter(child, "getSelectable") ?? true;
176
+
177
+ const replaceContainerSelection = (container: Gtk.Widget, child: Gtk.Widget): void => {
178
+ if (!isChildSelectable(child)) {
74
179
  return;
75
180
  }
76
181
 
77
- if (widget instanceof Gtk.Switch) {
78
- await wrapEvent(widget, () => {
79
- widget.setActive(!widget.getActive());
80
- });
182
+ selectContainerChild(container, child);
183
+
184
+ if (isMultipleSelection(container)) {
185
+ unselectOtherChildren(container, child);
186
+ }
187
+ };
81
188
 
189
+ const isActivatedByClick = (container: Gtk.Widget, nPress: number): boolean =>
190
+ isSingleClickActivating(container) || nPress > 1;
191
+
192
+ const isSelectionReplacedByClick = (container: Gtk.Widget, nPress: number): boolean =>
193
+ nPress > 1 || !isSingleClickActivating(container);
194
+
195
+ const applyContainerOutcome = (target: ClickTarget, nPress: number): void => {
196
+ const { container, widget } = target;
197
+
198
+ if (container === null || widget.getParent() !== container) {
82
199
  return;
83
200
  }
84
201
 
85
- if (await tryActivate(widget)) {
202
+ if (isActivatedByClick(container, nPress)) {
203
+ widget.activate();
204
+ }
205
+
206
+ if (isSelectionReplacedByClick(container, nPress)) {
207
+ replaceContainerSelection(container, widget);
208
+ }
209
+ };
210
+
211
+ const applyClickOutcome = (target: ClickTarget, nPress: number): void => {
212
+ if (target.native === null) {
213
+ applyContainerOutcome(target, nPress);
214
+
86
215
  return;
87
216
  }
88
217
 
89
- const target = findClickableAncestor(widget);
218
+ target.native(target.widget, nPress);
219
+ };
90
220
 
91
- if (target) {
92
- await emitClickSequence(widget, target, 1);
221
+ const applyClickOutcomes = (targets: ClickTarget[], nPress: number): void => {
222
+ for (const target of targets) {
223
+ applyClickOutcome(target, nPress);
93
224
  }
94
225
  };
95
226
 
96
- const dblClick = (widget: Gtk.Widget): Promise<void> => emitClickSequence(widget, widget, 2);
97
- const tripleClick = (widget: Gtk.Widget): Promise<void> => emitClickSequence(widget, widget, 3);
227
+ const deliverClick = (widget: Gtk.Widget, nPress: number): Promise<void> =>
228
+ wrapEvent(widget, () => {
229
+ const targets = collectClickTargets(widget);
230
+ const sites = targets.flatMap((target) => targetSites(widget, target));
231
+ emitClickSequence(sites, nPress);
232
+ applyClickOutcomes(targets, nPress);
233
+ });
234
+
235
+ /* eslint-disable-next-line unicorn/consistent-boolean-name -- the boolean reports whether activation succeeded */
236
+ const tryActivate = async (widget: Gtk.Widget): Promise<boolean> => {
237
+ if (widget.getAccessibleRole() === Gtk.AccessibleRole.LABEL) {
238
+ return false;
239
+ }
240
+
241
+ let isActivated = false;
242
+
243
+ await wrapEvent(widget, () => {
244
+ isActivated = widget.activate();
245
+ });
246
+
247
+ return isActivated;
248
+ };
249
+
250
+ /**
251
+ * Activates a widget that neither claims the click nor carries a click gesture of its own, and
252
+ * otherwise delivers a press and release. The press travels outwards from the clicked widget
253
+ * through every widget carrying a click gesture with a pressed or released handler, the way GTK
254
+ * hands the same press to each of them, and stops at the first Gtk.Button or indexed child of a
255
+ * list box or flow box, which claims it. A gesture GTK attached itself takes the press only when no
256
+ * widget below it handles one, and stops it there, so an expander or a notebook tab nested in a row
257
+ * opens instead of activating the row. Coordinates are the clicked widget's position in each
258
+ * carrier, so a gesture the container of an indexed child carries reads the child's position rather
259
+ * than the container's center. An indexed child that the press reaches is then activated, or
260
+ * exclusively selected when its container does not activate on a single click. A widget with the
261
+ * label role is never activated, but does consume the click when it carries a gesture of its own.
262
+ *
263
+ * The press also stops at a widget whose click GTK4 implements in C on a gesture it attached
264
+ * itself, reading a GdkEvent that off-screen synthesis cannot produce. Gestures that widget carries
265
+ * of its own still take the press and release, the way GTK hands them the press before its own
266
+ * gesture claims it, and the same outcome is then applied through the public action GTK's own
267
+ * handler invokes: a list, grid, or column-view row is focused and selected, and activated as well
268
+ * on a second press or when its view activates on a single click; an expandable tree expander
269
+ * toggles its expansion, once per press, and only when it is the widget clicked, so a click on its
270
+ * child falls through to the enclosing row; a column header sorts by its column. Sorting goes
271
+ * through Gtk.ColumnView.sortByColumn, so the primary sort column and order match what a pointer
272
+ * produces while previously sorted columns are dropped rather than kept as secondary keys. A
273
+ * column-view cell and the row that carries the column headers stand in the way of the click rather
274
+ * than taking it, so a click on either reaches the row or the view behind it.
275
+ */
276
+ const click = async (widget: Gtk.Widget): Promise<void> => {
277
+ if (!isSelfClickTarget(widget) && (await tryActivate(widget))) {
278
+ return;
279
+ }
280
+
281
+ await deliverClick(widget, 1);
282
+ };
283
+
284
+ /**
285
+ * Delivers a two-press click gesture the way {@link click} delivers a single press, without trying
286
+ * activation first. A list box row or flow box child the presses reach, clicked directly or through
287
+ * a descendant, is activated and exclusively selected, as GTK's double-click path does whether or
288
+ * not the container activates on a single click. A widget whose click GTK4 implements itself
289
+ * receives that outcome once per press, so a list, grid, or column-view row is selected and then
290
+ * activated by the second press, a tree expander ends back where it started, and a column header
291
+ * sorts and then inverts its order.
292
+ */
293
+ const dblClick = (widget: Gtk.Widget): Promise<void> => deliverClick(widget, 2);
294
+ /**
295
+ * Delivers a three-press click gesture the same way a double click is delivered, applying the same
296
+ * outcome to a list box row or flow box child the presses reach, and applying the outcome GTK4
297
+ * implements itself once per press to a row, tree expander, or column header.
298
+ */
299
+ const tripleClick = (widget: Gtk.Widget): Promise<void> => deliverClick(widget, 3);
98
300
 
99
- export { emitPress, emitRelease, click, dblClick, tripleClick };
301
+ export { clickGestures, emitClickPhase, getAuthoredClickGestures, click, dblClick, tripleClick };
@@ -0,0 +1,90 @@
1
+ import * as Gtk from "@gtkx/gi/gtk";
2
+ import { children } from "../traversal.js";
3
+ import { getWidgetTextContent } from "../widget-accessible-properties.js";
4
+
5
+ const columnViewFor = (header: Gtk.Widget): Gtk.ColumnView | null => {
6
+ let current: Gtk.Widget | null = header.getParent();
7
+
8
+ while (current !== null) {
9
+ if (current instanceof Gtk.ColumnView) {
10
+ return current;
11
+ }
12
+
13
+ current = current.getParent();
14
+ }
15
+
16
+ return null;
17
+ };
18
+
19
+ const columnsFor = (view: Gtk.ColumnView): Gtk.ColumnViewColumn[] => {
20
+ const model = view.getColumns();
21
+ const columns: Gtk.ColumnViewColumn[] = [];
22
+
23
+ for (let index = 0; index < model.getNItems(); index++) {
24
+ const column = model.getItem(index);
25
+
26
+ if (column instanceof Gtk.ColumnViewColumn) {
27
+ columns.push(column);
28
+ }
29
+ }
30
+
31
+ return columns;
32
+ };
33
+
34
+ const headerIndexFor = (header: Gtk.Widget): number => {
35
+ const parent = header.getParent();
36
+
37
+ return parent === null ? -1 : [...children(parent)].indexOf(header);
38
+ };
39
+
40
+ const titledColumnFor = (columns: Gtk.ColumnViewColumn[], title: string | null): Gtk.ColumnViewColumn | null => {
41
+ const matches = columns.filter((column) => column.getTitle() === title);
42
+
43
+ return matches.length === 1 ? matches[0] ?? null : null;
44
+ };
45
+
46
+ const columnFor = (header: Gtk.Widget, view: Gtk.ColumnView): Gtk.ColumnViewColumn | null => {
47
+ const columns = columnsFor(view);
48
+ const picked = columns[headerIndexFor(header)] ?? null;
49
+ const title = getWidgetTextContent(header);
50
+
51
+ if (picked !== null && picked.getTitle() === title) {
52
+ return picked;
53
+ }
54
+
55
+ return titledColumnFor(columns, title) ?? picked;
56
+ };
57
+
58
+ const sortableColumnFor = (header: Gtk.Widget, view: Gtk.ColumnView): Gtk.ColumnViewColumn | null => {
59
+ const column = columnFor(header, view);
60
+
61
+ return column !== null && column.getSorter() !== null ? column : null;
62
+ };
63
+
64
+ const invertedSortDirection = (order: Gtk.SortType): Gtk.SortType =>
65
+ order === Gtk.SortType.ASCENDING ? Gtk.SortType.DESCENDING : Gtk.SortType.ASCENDING;
66
+
67
+ const nextSortDirection = (view: Gtk.ColumnView, column: Gtk.ColumnViewColumn): Gtk.SortType => {
68
+ const sorter = view.getSorter();
69
+
70
+ if (!(sorter instanceof Gtk.ColumnViewSorter) || sorter.getPrimarySortColumn() !== column) {
71
+ return Gtk.SortType.ASCENDING;
72
+ }
73
+
74
+ return invertedSortDirection(sorter.getPrimarySortOrder());
75
+ };
76
+
77
+ const applyHeaderClick = (header: Gtk.Widget, nPress: number): void => {
78
+ const view = columnViewFor(header);
79
+ const column = view === null ? null : sortableColumnFor(header, view);
80
+
81
+ if (view === null || column === null) {
82
+ return;
83
+ }
84
+
85
+ for (let press = 1; press <= nPress; press++) {
86
+ view.sortByColumn(column, nextSortDirection(view, column));
87
+ }
88
+ };
89
+
90
+ export { applyHeaderClick };
@@ -1,7 +1,15 @@
1
1
  import type * as Gtk from "@gtkx/gi/gtk";
2
2
 
3
+ /** An event controller class that can be constructed without arguments, such as `Gtk.GestureClick`. */
3
4
  type ControllerConstructor<T extends Gtk.EventController> = new () => T;
4
5
 
6
+ /**
7
+ * Returns every controller of the given type attached to a widget, in the order GTK4 reports them.
8
+ *
9
+ * @param widget The widget whose controllers are inspected.
10
+ * @param controllerType The controller class to match.
11
+ * @returns The matching controllers, or an empty array when the widget has none.
12
+ */
5
13
  const queryAllControllers = <T extends Gtk.EventController>(
6
14
  widget: Gtk.Widget,
7
15
  controllerType: ControllerConstructor<T>,
@@ -21,11 +29,25 @@ const queryAllControllers = <T extends Gtk.EventController>(
21
29
  return matches;
22
30
  };
23
31
 
32
+ /**
33
+ * Returns the first controller of the given type attached to a widget.
34
+ *
35
+ * @param widget The widget whose controllers are inspected.
36
+ * @param controllerType The controller class to match.
37
+ * @returns The first matching controller, or null when the widget has none.
38
+ */
24
39
  const queryController = <T extends Gtk.EventController>(
25
40
  widget: Gtk.Widget,
26
41
  controllerType: ControllerConstructor<T>,
27
42
  ): T | null => queryAllControllers(widget, controllerType)[0] ?? null;
28
43
 
44
+ /**
45
+ * Returns every controller of the given type attached to a widget.
46
+ *
47
+ * @param widget The widget whose controllers are inspected.
48
+ * @param controllerType The controller class to match.
49
+ * @throws When the widget has no controller of that type.
50
+ */
29
51
  const getAllControllers = <T extends Gtk.EventController>(
30
52
  widget: Gtk.Widget,
31
53
  controllerType: ControllerConstructor<T>,
@@ -39,6 +61,13 @@ const getAllControllers = <T extends Gtk.EventController>(
39
61
  return controllers;
40
62
  };
41
63
 
64
+ /**
65
+ * Returns the first controller of the given type attached to a widget.
66
+ *
67
+ * @param widget The widget whose controllers are inspected.
68
+ * @param controllerType The controller class to match.
69
+ * @throws When the widget has no controller of that type.
70
+ */
42
71
  const getController = <T extends Gtk.EventController>(
43
72
  widget: Gtk.Widget,
44
73
  controllerType: ControllerConstructor<T>,
@@ -14,7 +14,9 @@ const canDisplayDeliverActivation = (window: Gtk.Window): boolean =>
14
14
  window.getDisplay().getDefaultSeat() !== null;
15
15
 
16
16
  const findWindowActionabilityFailure = (widget: Gtk.Widget, root: Gtk.Window): string | null => {
17
- if (root.getAllocatedWidth() === 0) {
17
+ const [isComputed, allocation] = root.computeBounds(root);
18
+
19
+ if (!isComputed || allocation.getWidth() === 0) {
18
20
  return WINDOW_NOT_ALLOCATED;
19
21
  }
20
22
 
@@ -9,13 +9,17 @@ type DropContent = string | number | boolean | GObject.Value;
9
9
 
10
10
  /** Options for a drop: the drop coordinates within the target. */
11
11
  type DropOptions = {
12
+ /** Horizontal drop coordinate within the target, in pixels. */
12
13
  x?: number;
14
+ /** Vertical drop coordinate within the target, in pixels. */
13
15
  y?: number;
14
16
  };
15
17
 
16
18
  /** A drag offset relative to the drag start point. */
17
19
  type DragOffset = {
20
+ /** Horizontal distance from the drag start point, in pixels. */
18
21
  x: number;
22
+ /** Vertical distance from the drag start point, in pixels. */
19
23
  y: number;
20
24
  };
21
25
 
@@ -24,9 +28,13 @@ type DragOffset = {
24
28
  * explicit intermediate offsets emitted before the final one.
25
29
  */
26
30
  type DragOptions = {
31
+ /** Horizontal coordinate the drag begins at; defaults to 0. */
27
32
  startX?: number;
33
+ /** Vertical coordinate the drag begins at; defaults to 0. */
28
34
  startY?: number;
35
+ /** Number of evenly interpolated drag updates, the last of which is the requested offset; defaults to 2. */
29
36
  steps?: number;
37
+ /** Explicit intermediate offsets emitted before the requested one, used instead of `steps`. */
30
38
  offsets?: DragOffset[];
31
39
  };
32
40
 
@@ -37,8 +45,8 @@ type DragInstancePatch = {
37
45
 
38
46
  type SavedDragState = {
39
47
  instance: DragInstancePatch;
40
- ownsStartPoint: boolean;
41
- ownsOffset: boolean;
48
+ hasOwnStartPoint: boolean;
49
+ hasOwnOffset: boolean;
42
50
  previousStartPoint: DragInstancePatch["getStartPoint"];
43
51
  previousOffset: DragInstancePatch["getOffset"];
44
52
  };
@@ -65,32 +73,56 @@ const buildDropValue = (content: DropContent): GObject.Value => {
65
73
  });
66
74
  };
67
75
 
76
+ /** Emits `enter` at the widget's origin on its motion controllers, adding one when it has none. */
68
77
  const hover = (widget: Gtk.Widget): Promise<void> =>
69
78
  dispatchOnOrCreateControllers(widget, Gtk.EventControllerMotion, (controller) => {
70
79
  controller.emit("enter", 0, 0);
71
80
  });
72
81
 
82
+ /** Emits `leave` on the widget's motion controllers, adding one when it has none. */
73
83
  const unhover = (widget: Gtk.Widget): Promise<void> =>
74
84
  dispatchOnOrCreateControllers(widget, Gtk.EventControllerMotion, (controller) => {
75
85
  controller.emit("leave");
76
86
  });
77
87
 
88
+ /**
89
+ * Emits `angle-changed` on the widget's rotate gestures.
90
+ *
91
+ * @param angle Current angle, in radians.
92
+ * @param deltaAngle Difference from the angle the gesture started at; defaults to `angle`.
93
+ * @throws When the widget has no Gtk.GestureRotate.
94
+ */
78
95
  const rotate = (widget: Gtk.Widget, angle: number, deltaAngle: number = angle): Promise<void> =>
79
96
  dispatchOnControllers(widget, Gtk.GestureRotate, (controller) => {
80
97
  controller.emit("angle-changed", angle, deltaAngle);
81
98
  },
82
99
  );
83
100
 
101
+ /**
102
+ * Emits `scale-changed` with the given scale delta on the widget's zoom gestures.
103
+ *
104
+ * @throws When the widget has no Gtk.GestureZoom.
105
+ */
84
106
  const zoom = (widget: Gtk.Widget, scale: number): Promise<void> =>
85
107
  dispatchOnControllers(widget, Gtk.GestureZoom, (controller) => {
86
108
  controller.emit("scale-changed", scale);
87
109
  });
88
110
 
111
+ /**
112
+ * Emits `swipe` with the given per-axis velocity, in pixels per second, on the widget's swipe gestures.
113
+ *
114
+ * @throws When the widget has no Gtk.GestureSwipe.
115
+ */
89
116
  const swipe = (widget: Gtk.Widget, velocityX: number, velocityY: number): Promise<void> =>
90
117
  dispatchOnControllers(widget, Gtk.GestureSwipe, (controller) => {
91
118
  controller.emit("swipe", velocityX, velocityY);
92
119
  });
93
120
 
121
+ /**
122
+ * Emits `pressed` at the given point in widget coordinates on the widget's long-press gestures.
123
+ *
124
+ * @throws When the widget has no Gtk.GestureLongPress.
125
+ */
94
126
  const longPress = (widget: Gtk.Widget, x = 0, y = 0): Promise<void> =>
95
127
  dispatchOnControllers(widget, Gtk.GestureLongPress, (controller) => {
96
128
  controller.emit("pressed", x, y);
@@ -99,13 +131,13 @@ const longPress = (widget: Gtk.Widget, x = 0, y = 0): Promise<void> =>
99
131
  const restoreDragState = (saved: SavedDragState): void => {
100
132
  const { instance } = saved;
101
133
 
102
- if (saved.ownsStartPoint) {
134
+ if (saved.hasOwnStartPoint) {
103
135
  instance.getStartPoint = saved.previousStartPoint;
104
136
  } else {
105
137
  delete instance.getStartPoint;
106
138
  }
107
139
 
108
- if (saved.ownsOffset) {
140
+ if (saved.hasOwnOffset) {
109
141
  instance.getOffset = saved.previousOffset;
110
142
  } else {
111
143
  delete instance.getOffset;
@@ -117,8 +149,8 @@ const patchDragState = (controller: Gtk.GestureDrag, start: DragOffset, offset:
117
149
 
118
150
  const saved: SavedDragState = {
119
151
  instance,
120
- ownsStartPoint: Object.hasOwn(instance, "getStartPoint"),
121
- ownsOffset: Object.hasOwn(instance, "getOffset"),
152
+ hasOwnStartPoint: Object.hasOwn(instance, "getStartPoint"),
153
+ hasOwnOffset: Object.hasOwn(instance, "getOffset"),
122
154
  previousStartPoint: instance.getStartPoint,
123
155
  previousOffset: instance.getOffset,
124
156
  };
@@ -177,6 +209,14 @@ const resolveDragUpdates = (dx: number, dy: number, options: DragOptions): DragO
177
209
  return updates;
178
210
  };
179
211
 
212
+ /**
213
+ * Runs a `drag-begin`, `drag-update`, `drag-end` sequence ending at the offset `dx`, `dy` on the
214
+ * widget's drag gestures, overriding each gesture's start point and offset for the duration so
215
+ * handlers read the simulated values back.
216
+ *
217
+ * @throws When the widget is a Gtk.Range, whose slider reads pointer coordinates from the display,
218
+ * or when it has no Gtk.GestureDrag.
219
+ */
180
220
  const drag = async (widget: Gtk.Widget, dx: number, dy: number, options: DragOptions = {}): Promise<void> => {
181
221
  if (widget instanceof Gtk.Range) {
182
222
  throw new TypeError(
@@ -202,11 +242,22 @@ const emitDrop = (target: Gtk.Widget, content: DropContent, options: DropOptions
202
242
  }
203
243
  };
204
244
 
245
+ /**
246
+ * Emits `drop` with the given content on every drop target attached to the widget.
247
+ *
248
+ * @throws When the widget has no Gtk.DropTarget.
249
+ */
205
250
  const drop = (widget: Gtk.Widget, content: DropContent, options: DropOptions = {}): Promise<void> =>
206
251
  wrapEvent(widget, () => {
207
252
  emitDrop(widget, content, options);
208
253
  });
209
254
 
255
+ /**
256
+ * Emits `drop` with the given content on the target's drop targets, after checking that the source
257
+ * carries a drag source.
258
+ *
259
+ * @throws When the source has no Gtk.DragSource, or the target no Gtk.DropTarget.
260
+ */
210
261
  const dragAndDrop = async (
211
262
  source: Gtk.Widget,
212
263
  target: Gtk.Widget,