@gtkx/testing 0.21.0 → 1.0.0-rc.2

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 (216) hide show
  1. package/README.md +137 -34
  2. package/dist/act.d.ts +19 -0
  3. package/dist/act.d.ts.map +1 -0
  4. package/dist/act.js +56 -0
  5. package/dist/act.js.map +1 -0
  6. package/dist/bound-queries.d.ts +33 -0
  7. package/dist/bound-queries.d.ts.map +1 -0
  8. package/dist/bound-queries.js +2 -0
  9. package/dist/bound-queries.js.map +1 -0
  10. package/dist/build-queries.d.ts +16 -0
  11. package/dist/build-queries.d.ts.map +1 -0
  12. package/dist/build-queries.js +115 -0
  13. package/dist/build-queries.js.map +1 -0
  14. package/dist/cleanup-registry.d.ts +5 -0
  15. package/dist/cleanup-registry.d.ts.map +1 -0
  16. package/dist/cleanup-registry.js +21 -0
  17. package/dist/cleanup-registry.js.map +1 -0
  18. package/dist/config.d.ts +20 -52
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +17 -46
  21. package/dist/config.js.map +1 -1
  22. package/dist/editable.d.ts +8 -0
  23. package/dist/editable.d.ts.map +1 -0
  24. package/dist/editable.js +26 -0
  25. package/dist/editable.js.map +1 -0
  26. package/dist/errors.d.ts +30 -0
  27. package/dist/errors.d.ts.map +1 -0
  28. package/dist/errors.js +135 -0
  29. package/dist/errors.js.map +1 -0
  30. package/dist/fire-event.d.ts +8 -25
  31. package/dist/fire-event.d.ts.map +1 -1
  32. package/dist/fire-event.js +11 -30
  33. package/dist/fire-event.js.map +1 -1
  34. package/dist/frame-sync.d.ts +4 -0
  35. package/dist/frame-sync.d.ts.map +1 -0
  36. package/dist/frame-sync.js +37 -0
  37. package/dist/frame-sync.js.map +1 -0
  38. package/dist/index.d.ts +17 -12
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +11 -7
  41. package/dist/index.js.map +1 -1
  42. package/dist/matchers.d.ts +59 -0
  43. package/dist/matchers.d.ts.map +1 -0
  44. package/dist/matchers.js +117 -0
  45. package/dist/matchers.js.map +1 -0
  46. package/dist/pretty-widget.d.ts +24 -26
  47. package/dist/pretty-widget.d.ts.map +1 -1
  48. package/dist/pretty-widget.js +111 -117
  49. package/dist/pretty-widget.js.map +1 -1
  50. package/dist/queries.d.ts +190 -180
  51. package/dist/queries.d.ts.map +1 -1
  52. package/dist/queries.js +291 -320
  53. package/dist/queries.js.map +1 -1
  54. package/dist/register-test-hooks.d.ts +2 -0
  55. package/dist/register-test-hooks.d.ts.map +1 -0
  56. package/dist/register-test-hooks.js +16 -0
  57. package/dist/register-test-hooks.js.map +1 -0
  58. package/dist/render-hook.d.ts +15 -35
  59. package/dist/render-hook.d.ts.map +1 -1
  60. package/dist/render-hook.js +9 -45
  61. package/dist/render-hook.js.map +1 -1
  62. package/dist/render.d.ts +13 -42
  63. package/dist/render.d.ts.map +1 -1
  64. package/dist/render.js +138 -121
  65. package/dist/render.js.map +1 -1
  66. package/dist/role-helpers.d.ts +19 -53
  67. package/dist/role-helpers.d.ts.map +1 -1
  68. package/dist/role-helpers.js +48 -63
  69. package/dist/role-helpers.js.map +1 -1
  70. package/dist/screen.d.ts +7 -63
  71. package/dist/screen.d.ts.map +1 -1
  72. package/dist/screen.js +24 -110
  73. package/dist/screen.js.map +1 -1
  74. package/dist/screenshot.d.ts +21 -23
  75. package/dist/screenshot.d.ts.map +1 -1
  76. package/dist/screenshot.js +98 -50
  77. package/dist/screenshot.js.map +1 -1
  78. package/dist/suggestions.d.ts +27 -0
  79. package/dist/suggestions.d.ts.map +1 -0
  80. package/dist/suggestions.js +61 -0
  81. package/dist/suggestions.js.map +1 -0
  82. package/dist/traversal.d.ts +12 -10
  83. package/dist/traversal.d.ts.map +1 -1
  84. package/dist/traversal.js +36 -15
  85. package/dist/traversal.js.map +1 -1
  86. package/dist/types.d.ts +115 -169
  87. package/dist/types.d.ts.map +1 -1
  88. package/dist/types.js.map +1 -1
  89. package/dist/user-event/adjustment.d.ts +10 -0
  90. package/dist/user-event/adjustment.d.ts.map +1 -0
  91. package/dist/user-event/adjustment.js +35 -0
  92. package/dist/user-event/adjustment.js.map +1 -0
  93. package/dist/user-event/click.d.ts +8 -0
  94. package/dist/user-event/click.d.ts.map +1 -0
  95. package/dist/user-event/click.js +73 -0
  96. package/dist/user-event/click.js.map +1 -0
  97. package/dist/user-event/controller.d.ts +8 -0
  98. package/dist/user-event/controller.d.ts.map +1 -0
  99. package/dist/user-event/controller.js +31 -0
  100. package/dist/user-event/controller.js.map +1 -0
  101. package/dist/user-event/dispatch.d.ts +7 -0
  102. package/dist/user-event/dispatch.d.ts.map +1 -0
  103. package/dist/user-event/dispatch.js +11 -0
  104. package/dist/user-event/dispatch.js.map +1 -0
  105. package/dist/user-event/event-wrapper.d.ts +4 -0
  106. package/dist/user-event/event-wrapper.d.ts.map +1 -0
  107. package/dist/user-event/event-wrapper.js +59 -0
  108. package/dist/user-event/event-wrapper.js.map +1 -0
  109. package/dist/user-event/gesture.d.ts +35 -0
  110. package/dist/user-event/gesture.d.ts.map +1 -0
  111. package/dist/user-event/gesture.js +138 -0
  112. package/dist/user-event/gesture.js.map +1 -0
  113. package/dist/user-event/index.d.ts +51 -0
  114. package/dist/user-event/index.d.ts.map +1 -0
  115. package/dist/user-event/index.js +42 -0
  116. package/dist/user-event/index.js.map +1 -0
  117. package/dist/user-event/keyboard.d.ts +10 -0
  118. package/dist/user-event/keyboard.d.ts.map +1 -0
  119. package/dist/user-event/keyboard.js +204 -0
  120. package/dist/user-event/keyboard.js.map +1 -0
  121. package/dist/user-event/pointer.d.ts +10 -0
  122. package/dist/user-event/pointer.d.ts.map +1 -0
  123. package/dist/user-event/pointer.js +30 -0
  124. package/dist/user-event/pointer.js.map +1 -0
  125. package/dist/user-event/selection.d.ts +5 -0
  126. package/dist/user-event/selection.d.ts.map +1 -0
  127. package/dist/user-event/selection.js +105 -0
  128. package/dist/user-event/selection.js.map +1 -0
  129. package/dist/user-event/state.d.ts +7 -0
  130. package/dist/user-event/state.d.ts.map +1 -0
  131. package/dist/user-event/state.js +3 -0
  132. package/dist/user-event/state.js.map +1 -0
  133. package/dist/user-event/text.d.ts +18 -0
  134. package/dist/user-event/text.d.ts.map +1 -0
  135. package/dist/user-event/text.js +113 -0
  136. package/dist/user-event/text.js.map +1 -0
  137. package/dist/wait-for.d.ts +16 -36
  138. package/dist/wait-for.d.ts.map +1 -1
  139. package/dist/wait-for.js +74 -70
  140. package/dist/wait-for.js.map +1 -1
  141. package/dist/widget-accessible-properties.d.ts +34 -0
  142. package/dist/widget-accessible-properties.d.ts.map +1 -0
  143. package/dist/widget-accessible-properties.js +260 -0
  144. package/dist/widget-accessible-properties.js.map +1 -0
  145. package/dist/within.d.ts +10 -26
  146. package/dist/within.d.ts.map +1 -1
  147. package/dist/within.js +16 -25
  148. package/dist/within.js.map +1 -1
  149. package/package.json +23 -12
  150. package/src/act.ts +77 -0
  151. package/src/bound-queries.ts +37 -0
  152. package/src/build-queries.ts +226 -0
  153. package/src/cleanup-registry.ts +27 -0
  154. package/src/config.ts +35 -62
  155. package/src/editable.ts +37 -0
  156. package/src/errors.ts +191 -0
  157. package/src/fire-event.ts +13 -38
  158. package/src/frame-sync.ts +51 -0
  159. package/src/index.ts +58 -15
  160. package/src/matchers.ts +251 -0
  161. package/src/pretty-widget.ts +163 -130
  162. package/src/queries.ts +485 -396
  163. package/src/register-test-hooks.ts +19 -0
  164. package/src/render-hook.tsx +38 -45
  165. package/src/render.tsx +192 -155
  166. package/src/role-helpers.ts +65 -72
  167. package/src/screen.ts +26 -114
  168. package/src/screenshot.ts +128 -56
  169. package/src/suggestions.ts +106 -0
  170. package/src/traversal.ts +56 -22
  171. package/src/types.ts +144 -172
  172. package/src/user-event/adjustment.ts +62 -0
  173. package/src/user-event/click.ts +99 -0
  174. package/src/user-event/controller.ts +58 -0
  175. package/src/user-event/dispatch.ts +25 -0
  176. package/src/user-event/event-wrapper.ts +80 -0
  177. package/src/user-event/gesture.ts +239 -0
  178. package/src/user-event/index.ts +81 -0
  179. package/src/user-event/keyboard.ts +290 -0
  180. package/src/user-event/pointer.ts +46 -0
  181. package/src/user-event/selection.ts +159 -0
  182. package/src/user-event/state.ts +8 -0
  183. package/src/user-event/text.ts +172 -0
  184. package/src/wait-for.ts +117 -78
  185. package/src/widget-accessible-properties.ts +388 -0
  186. package/src/within.ts +29 -27
  187. package/dist/bind-queries.d.ts +0 -14
  188. package/dist/bind-queries.d.ts.map +0 -1
  189. package/dist/bind-queries.js +0 -29
  190. package/dist/bind-queries.js.map +0 -1
  191. package/dist/error-builder.d.ts +0 -28
  192. package/dist/error-builder.d.ts.map +0 -1
  193. package/dist/error-builder.js +0 -81
  194. package/dist/error-builder.js.map +0 -1
  195. package/dist/timing.d.ts +0 -18
  196. package/dist/timing.d.ts.map +0 -1
  197. package/dist/timing.js +0 -18
  198. package/dist/timing.js.map +0 -1
  199. package/dist/user-event.d.ts +0 -138
  200. package/dist/user-event.d.ts.map +0 -1
  201. package/dist/user-event.js +0 -411
  202. package/dist/user-event.js.map +0 -1
  203. package/dist/widget-text.d.ts +0 -69
  204. package/dist/widget-text.d.ts.map +0 -1
  205. package/dist/widget-text.js +0 -177
  206. package/dist/widget-text.js.map +0 -1
  207. package/dist/widget.d.ts +0 -3
  208. package/dist/widget.d.ts.map +0 -1
  209. package/dist/widget.js +0 -13
  210. package/dist/widget.js.map +0 -1
  211. package/src/bind-queries.ts +0 -52
  212. package/src/error-builder.ts +0 -102
  213. package/src/timing.ts +0 -17
  214. package/src/user-event.ts +0 -492
  215. package/src/widget-text.ts +0 -202
  216. package/src/widget.ts +0 -15
@@ -0,0 +1,290 @@
1
+ import * as Gdk from "@gtkx/gi/gdk";
2
+ import * as Gtk from "@gtkx/gi/gtk";
3
+ import type { UserEventState } from "./state.js";
4
+ import { getEditableDelegate } from "../editable.js";
5
+ import { fireEvent } from "../fire-event.js";
6
+ import { getOrCreateControllers } from "./controller.js";
7
+ import { wrapEvent } from "./event-wrapper.js";
8
+
9
+ /** Options for {@link tab}: when `shift` is set, move focus backward instead of forward. */
10
+ type TabOptions = {
11
+ shift?: boolean;
12
+ };
13
+
14
+ type KeyAction = { keyval: number; press: boolean };
15
+ type ParseStep = { actions: KeyAction[]; next: number };
16
+
17
+ const KEY_MAP: Record<string, number> = {
18
+ Enter: Gdk.KEY_Return,
19
+ Tab: Gdk.KEY_Tab,
20
+ Escape: Gdk.KEY_Escape,
21
+ Backspace: Gdk.KEY_BackSpace,
22
+ Delete: Gdk.KEY_Delete,
23
+ ArrowUp: Gdk.KEY_Up,
24
+ ArrowDown: Gdk.KEY_Down,
25
+ ArrowLeft: Gdk.KEY_Left,
26
+ ArrowRight: Gdk.KEY_Right,
27
+ Home: Gdk.KEY_Home,
28
+ End: Gdk.KEY_End,
29
+ PageUp: Gdk.KEY_Page_Up,
30
+ PageDown: Gdk.KEY_Page_Down,
31
+ Space: Gdk.KEY_space,
32
+ Shift: Gdk.KEY_Shift_L,
33
+ Control: Gdk.KEY_Control_L,
34
+ Alt: Gdk.KEY_Alt_L,
35
+ Meta: Gdk.KEY_Meta_L,
36
+ F1: Gdk.KEY_F1,
37
+ F2: Gdk.KEY_F2,
38
+ F3: Gdk.KEY_F3,
39
+ F4: Gdk.KEY_F4,
40
+ F5: Gdk.KEY_F5,
41
+ F6: Gdk.KEY_F6,
42
+ F7: Gdk.KEY_F7,
43
+ F8: Gdk.KEY_F8,
44
+ F9: Gdk.KEY_F9,
45
+ F10: Gdk.KEY_F10,
46
+ F11: Gdk.KEY_F11,
47
+ F12: Gdk.KEY_F12,
48
+ };
49
+
50
+ const MODIFIER_KEYVAL_TO_MASK: Record<number, number> = {
51
+ [Gdk.KEY_Shift_L]: Gdk.ModifierType.SHIFT_MASK,
52
+ [Gdk.KEY_Shift_R]: Gdk.ModifierType.SHIFT_MASK,
53
+ [Gdk.KEY_Control_L]: Gdk.ModifierType.CONTROL_MASK,
54
+ [Gdk.KEY_Control_R]: Gdk.ModifierType.CONTROL_MASK,
55
+ [Gdk.KEY_Alt_L]: Gdk.ModifierType.ALT_MASK,
56
+ [Gdk.KEY_Alt_R]: Gdk.ModifierType.ALT_MASK,
57
+ [Gdk.KEY_Meta_L]: Gdk.ModifierType.META_MASK,
58
+ [Gdk.KEY_Meta_R]: Gdk.ModifierType.META_MASK,
59
+ };
60
+
61
+ const tab = (widget: Gtk.Widget, options?: TabOptions): Promise<void> =>
62
+ wrapEvent(widget, () => {
63
+ const direction = options?.shift ? Gtk.DirectionType.TAB_BACKWARD : Gtk.DirectionType.TAB_FORWARD;
64
+ const root = widget.getRoot();
65
+
66
+ if (root) {
67
+ root.childFocus(direction);
68
+ }
69
+ });
70
+
71
+ const parseKeyToken = (token: string): { keyval: number; press: boolean; release: boolean } => {
72
+ let keyName = token;
73
+ let isPress = true;
74
+ let isRelease = true;
75
+
76
+ if (keyName.startsWith("/")) {
77
+ keyName = keyName.slice(1);
78
+ isPress = false;
79
+ } else if (keyName.endsWith(">")) {
80
+ keyName = keyName.slice(0, -1);
81
+ isRelease = false;
82
+ }
83
+
84
+ const keyval = KEY_MAP[keyName];
85
+
86
+ if (keyval === undefined) {
87
+ throw new Error(`Unknown key: {${keyName}}`);
88
+ }
89
+
90
+ return { keyval, press: isPress, release: isRelease };
91
+ };
92
+
93
+ const parseCharAt = (input: string, i: number): ParseStep => {
94
+ const keyval = input.codePointAt(i) ?? 0;
95
+
96
+ return {
97
+ actions: [
98
+ { keyval, press: true },
99
+ { keyval, press: false },
100
+ ],
101
+ next: i + 1,
102
+ };
103
+ };
104
+
105
+ const parseBraceAt = (input: string, i: number): ParseStep | null => {
106
+ const endBrace = input.indexOf("}", i);
107
+
108
+ if (endBrace === -1) {
109
+ return null;
110
+ }
111
+
112
+ const { keyval, press, release } = parseKeyToken(input.slice(i + 1, endBrace));
113
+ const actions: KeyAction[] = [];
114
+
115
+ if (press) {
116
+ actions.push({ keyval, press: true });
117
+ }
118
+
119
+ if (release) {
120
+ actions.push({ keyval, press: false });
121
+ }
122
+
123
+ return { actions, next: endBrace + 1 };
124
+ };
125
+
126
+ const parseStepAt = (input: string, i: number): ParseStep | null =>
127
+ input[i] === "{" ? parseBraceAt(input, i) : parseCharAt(input, i);
128
+
129
+ const parseKeyboardInput = (input: string): KeyAction[] => {
130
+ const actions: KeyAction[] = [];
131
+ let i = 0;
132
+
133
+ while (i < input.length) {
134
+ const step = parseStepAt(input, i);
135
+
136
+ if (step === null) {
137
+ break;
138
+ }
139
+
140
+ actions.push(...step.actions);
141
+ i = step.next;
142
+ }
143
+
144
+ return actions;
145
+ };
146
+
147
+ const updateModifierState = (state: UserEventState, action: KeyAction): void => {
148
+ const mask = MODIFIER_KEYVAL_TO_MASK[action.keyval];
149
+
150
+ if (!mask) {
151
+ return;
152
+ }
153
+
154
+ if (action.press) {
155
+ state.modifierState |= mask;
156
+ } else {
157
+ state.modifierState &= ~mask;
158
+ }
159
+ };
160
+
161
+ const isTriggerMatch = (
162
+ trigger: Gtk.ShortcutTrigger | null,
163
+ keyval: number,
164
+ modifiers: Gdk.ModifierType,
165
+ ): boolean => {
166
+ if (trigger instanceof Gtk.KeyvalTrigger) {
167
+ return trigger.getKeyval() === keyval && trigger.getModifiers() === modifiers;
168
+ }
169
+
170
+ if (trigger instanceof Gtk.AlternativeTrigger) {
171
+ return (
172
+ isTriggerMatch(trigger.getFirst(), keyval, modifiers) ||
173
+ isTriggerMatch(trigger.getSecond(), keyval, modifiers)
174
+ );
175
+ }
176
+
177
+ return false;
178
+ };
179
+
180
+ const didActivateShortcut = (
181
+ shortcut: Gtk.Shortcut,
182
+ widget: Gtk.Widget,
183
+ keyval: number,
184
+ modifiers: Gdk.ModifierType,
185
+ ): boolean => {
186
+ if (!isTriggerMatch(shortcut.getTrigger(), keyval, modifiers)) {
187
+ return false;
188
+ }
189
+
190
+ const action = shortcut.getAction();
191
+
192
+ if (action instanceof Gtk.SignalAction && action.getSignalName() === "move-focus") {
193
+ return false;
194
+ }
195
+
196
+ return action?.activate(0 as Gtk.ShortcutActionFlags, widget, shortcut.getArguments()) ?? false;
197
+ };
198
+
199
+ const didActivateMatchingShortcut = (
200
+ controller: Gtk.ShortcutController,
201
+ widget: Gtk.Widget,
202
+ keyval: number,
203
+ modifiers: number,
204
+ ): boolean => {
205
+ const count = controller.getNItems();
206
+
207
+ for (let j = 0; j < count; j++) {
208
+ const shortcut = controller.getItem(j);
209
+
210
+ if (!(shortcut instanceof Gtk.Shortcut)) {
211
+ continue;
212
+ }
213
+
214
+ if (didActivateShortcut(shortcut, widget, keyval, modifiers)) {
215
+ return true;
216
+ }
217
+ }
218
+
219
+ return false;
220
+ };
221
+
222
+ const didDispatchShortcutsOnWidget = (widget: Gtk.Widget, keyval: number, modifiers: number): boolean => {
223
+ const controllers = widget.observeControllers();
224
+
225
+ for (let i = 0; i < controllers.getNItems(); i++) {
226
+ const controller = controllers.getItem(i);
227
+
228
+ if (
229
+ controller instanceof Gtk.ShortcutController &&
230
+ didActivateMatchingShortcut(controller, widget, keyval, modifiers)
231
+ ) {
232
+ return true;
233
+ }
234
+ }
235
+
236
+ return false;
237
+ };
238
+
239
+ const didDispatchShortcuts = (widget: Gtk.Widget, keyval: number, modifiers: number): boolean => {
240
+ const delegate = getEditableDelegate(widget);
241
+
242
+ if (delegate && didDispatchShortcutsOnWidget(delegate, keyval, modifiers)) {
243
+ return true;
244
+ }
245
+
246
+ for (let current: Gtk.Widget | null = widget; current; current = current.getParent()) {
247
+ if (didDispatchShortcutsOnWidget(current, keyval, modifiers)) {
248
+ return true;
249
+ }
250
+ }
251
+
252
+ return false;
253
+ };
254
+
255
+ const handleKeyPress = async (widget: Gtk.Widget, keyval: number, modifiers: number): Promise<void> => {
256
+ const isHandled = didDispatchShortcuts(widget, keyval, modifiers);
257
+
258
+ if (!isHandled && keyval === Gdk.KEY_Return && widget instanceof Gtk.Editable) {
259
+ await fireEvent(widget, "activate");
260
+ }
261
+ };
262
+
263
+ const applyKeyAction = async (
264
+ widget: Gtk.Widget,
265
+ controllers: Gtk.EventControllerKey[],
266
+ state: UserEventState,
267
+ action: KeyAction,
268
+ ): Promise<void> => {
269
+ updateModifierState(state, action);
270
+ const signalName = action.press ? "key-pressed" : "key-released";
271
+
272
+ for (const controller of controllers) {
273
+ controller.emit(signalName, action.keyval, 0, state.modifierState);
274
+ }
275
+
276
+ if (action.press) {
277
+ await handleKeyPress(widget, action.keyval, state.modifierState);
278
+ }
279
+ };
280
+
281
+ const keyboard = (state: UserEventState, widget: Gtk.Widget, input: string): Promise<void> =>
282
+ wrapEvent(widget, async () => {
283
+ const controllers = getOrCreateControllers(widget, Gtk.EventControllerKey);
284
+
285
+ for (const action of parseKeyboardInput(input)) {
286
+ await applyKeyAction(widget, controllers, state, action);
287
+ }
288
+ });
289
+
290
+ export { tab, keyboard, type TabOptions };
@@ -0,0 +1,46 @@
1
+ import * as Gtk from "@gtkx/gi/gtk";
2
+ import type { UserEventState } from "./state.js";
3
+ import { emitPress, emitRelease } from "./click.js";
4
+ import { getOrCreateControllers } from "./controller.js";
5
+ import { wrapEvent } from "./event-wrapper.js";
6
+
7
+ /**
8
+ * A pointer action token: a full click (`click`, `[MouseLeft]`), a button press (`down`,
9
+ * `[MouseLeft>]`), or a button release (`up`, `[/MouseLeft]`).
10
+ */
11
+ type PointerInput = "click" | "down" | "up" | "[MouseLeft]" | "[MouseLeft>]" | "[/MouseLeft]";
12
+
13
+ const PRESS_INPUTS: Set<PointerInput> = new Set(["[MouseLeft>]", "down"]);
14
+ const RELEASE_INPUTS: Set<PointerInput> = new Set(["[/MouseLeft]", "up"]);
15
+ const CLICK_INPUTS: Set<PointerInput> = new Set(["[MouseLeft]", "click"]);
16
+
17
+ const applyPointerInput = (widget: Gtk.Widget, state: UserEventState, input: PointerInput): void => {
18
+ const controllers = getOrCreateControllers(widget, Gtk.GestureClick);
19
+
20
+ if (CLICK_INPUTS.has(input)) {
21
+ emitPress(widget, controllers, 1);
22
+ emitRelease(widget, controllers, 1);
23
+ state.mouseLeftDown = false;
24
+
25
+ return;
26
+ }
27
+
28
+ if (PRESS_INPUTS.has(input) && !state.mouseLeftDown) {
29
+ emitPress(widget, controllers, 1);
30
+ state.mouseLeftDown = true;
31
+
32
+ return;
33
+ }
34
+
35
+ if (RELEASE_INPUTS.has(input) && state.mouseLeftDown) {
36
+ emitRelease(widget, controllers, 1);
37
+ state.mouseLeftDown = false;
38
+ }
39
+ };
40
+
41
+ const pointer = (state: UserEventState, widget: Gtk.Widget, input: PointerInput): Promise<void> =>
42
+ wrapEvent(widget, () => {
43
+ applyPointerInput(widget, state, input);
44
+ });
45
+
46
+ export { pointer, type PointerInput };
@@ -0,0 +1,159 @@
1
+ import * as Gtk from "@gtkx/gi/gtk";
2
+ import { formatRoleList } from "../role-helpers.js";
3
+ import { wrapEvent } from "./event-wrapper.js";
4
+
5
+ type CollectionWidget = Gtk.ListView | Gtk.GridView | Gtk.ColumnView;
6
+ type ListBoxRowAction = (listBox: Gtk.ListBox, row: Gtk.ListBoxRow) => void;
7
+
8
+ const SELECTABLE_ROLES: Set<Gtk.AccessibleRole> = new Set([Gtk.AccessibleRole.COMBO_BOX, Gtk.AccessibleRole.LIST]);
9
+
10
+ const isSelectable = (widget: Gtk.Widget): boolean => {
11
+ return SELECTABLE_ROLES.has(widget.getAccessibleRole());
12
+ };
13
+
14
+ const selectListViewItems = (
15
+ selectionModel: Gtk.SelectionModel,
16
+ positions: number[],
17
+ isExclusive: boolean,
18
+ ): void => {
19
+ if (positions.length === 0) {
20
+ selectionModel.unselectRange(0, selectionModel.getNItems());
21
+
22
+ return;
23
+ }
24
+
25
+ const [first] = positions;
26
+
27
+ if (isExclusive && first !== undefined && positions.length === 1) {
28
+ selectionModel.selectItem(first, true);
29
+
30
+ return;
31
+ }
32
+
33
+ const nItems = selectionModel.getNItems();
34
+ const selected = Gtk.Bitset.newEmpty();
35
+ const mask = Gtk.Bitset.newRange(0, nItems);
36
+
37
+ for (const pos of positions) {
38
+ selected.add(pos);
39
+ }
40
+
41
+ selectionModel.setSelection(selected, mask);
42
+ };
43
+
44
+ const isListView = (widget: Gtk.Widget): widget is CollectionWidget =>
45
+ widget instanceof Gtk.ListView || widget instanceof Gtk.GridView || widget instanceof Gtk.ColumnView;
46
+
47
+ const selectComboBoxOption = (widget: Gtk.Widget, valueArray: number[]): void => {
48
+ if (valueArray.length > 1) {
49
+ throw new Error("Cannot select multiple options: ComboBox only supports single selection");
50
+ }
51
+
52
+ const [selection] = valueArray;
53
+
54
+ if (selection === undefined) {
55
+ return;
56
+ }
57
+
58
+ if (widget instanceof Gtk.DropDown) {
59
+ widget.setSelected(selection);
60
+ } else if (widget instanceof Gtk.ComboBox) {
61
+ widget.setActive(selection);
62
+ }
63
+ };
64
+
65
+ const selectListBoxRow = (listBox: Gtk.ListBox, row: Gtk.ListBoxRow): void => {
66
+ listBox.selectRow(row);
67
+ row.activate();
68
+ };
69
+
70
+ const unselectListBoxRow = (listBox: Gtk.ListBox, row: Gtk.ListBoxRow): void => {
71
+ listBox.unselectRow(row);
72
+ };
73
+
74
+ const applyListBoxRows = (listBox: Gtk.ListBox, valueArray: number[], apply: ListBoxRowAction): void => {
75
+ for (const value of valueArray) {
76
+ const row = listBox.getRowAtIndex(value);
77
+
78
+ if (!row) {
79
+ continue;
80
+ }
81
+
82
+ apply(listBox, row);
83
+ }
84
+ };
85
+
86
+ const requireSelectionModel = (
87
+ widget: CollectionWidget,
88
+ verb: string,
89
+ ): Gtk.SelectionModel => {
90
+ const selectionModel = widget.getModel();
91
+
92
+ if (selectionModel === null) {
93
+ throw new Error(`Cannot ${verb} options: list view has no selection model`);
94
+ }
95
+
96
+ return selectionModel;
97
+ };
98
+
99
+ const selectInListView = (widget: CollectionWidget, valueArray: number[]): void => {
100
+ const selectionModel = requireSelectionModel(widget, "select");
101
+ const isMultiSelection = selectionModel instanceof Gtk.MultiSelection;
102
+ selectListViewItems(selectionModel, valueArray, !isMultiSelection);
103
+ };
104
+
105
+ const selectByRole = (widget: Gtk.Widget, valueArray: number[]): void => {
106
+ if (!isSelectable(widget)) {
107
+ throw new Error(`Cannot select options: expected selectable widget (${formatRoleList(SELECTABLE_ROLES)})`);
108
+ }
109
+
110
+ const role = widget.getAccessibleRole();
111
+
112
+ if (role === Gtk.AccessibleRole.COMBO_BOX) {
113
+ selectComboBoxOption(widget, valueArray);
114
+ } else if (widget instanceof Gtk.ListBox) {
115
+ applyListBoxRows(widget, valueArray, selectListBoxRow);
116
+ }
117
+ };
118
+
119
+ const runSelectionEvent = (
120
+ widget: Gtk.Widget,
121
+ values: number | number[],
122
+ inListView: (view: Gtk.ListView | Gtk.GridView | Gtk.ColumnView, valueArray: number[]) => void,
123
+ byRole: (widget: Gtk.Widget, valueArray: number[]) => void,
124
+ ): Promise<void> =>
125
+ wrapEvent(widget, () => {
126
+ const valueArray = Array.isArray(values) ? values : [values];
127
+
128
+ if (isListView(widget)) {
129
+ inListView(widget, valueArray);
130
+
131
+ return;
132
+ }
133
+
134
+ byRole(widget, valueArray);
135
+ });
136
+
137
+ const selectOptions = (widget: Gtk.Widget, values: number | number[]): Promise<void> =>
138
+ runSelectionEvent(widget, values, selectInListView, selectByRole);
139
+
140
+ const deselectInListView = (widget: Gtk.ListView | Gtk.GridView | Gtk.ColumnView, valueArray: number[]): void => {
141
+ const selectionModel = requireSelectionModel(widget, "deselect");
142
+
143
+ for (const pos of valueArray) {
144
+ selectionModel.unselectItem(pos);
145
+ }
146
+ };
147
+
148
+ const deselectByRole = (widget: Gtk.Widget, valueArray: number[]): void => {
149
+ if (!(widget instanceof Gtk.ListBox)) {
150
+ throw new TypeError("Cannot deselect options: only ListBox supports deselection");
151
+ }
152
+
153
+ applyListBoxRows(widget, valueArray, unselectListBoxRow);
154
+ };
155
+
156
+ const deselectOptions = (widget: Gtk.Widget, values: number | number[]): Promise<void> =>
157
+ runSelectionEvent(widget, values, deselectInListView, deselectByRole);
158
+
159
+ export { selectOptions, deselectOptions };
@@ -0,0 +1,8 @@
1
+ type UserEventState = {
2
+ modifierState: number;
3
+ mouseLeftDown: boolean;
4
+ };
5
+
6
+ const createInitialState = (): UserEventState => ({ modifierState: 0, mouseLeftDown: false });
7
+
8
+ export { createInitialState, type UserEventState };
@@ -0,0 +1,172 @@
1
+ import * as Gdk from "@gtkx/gi/gdk";
2
+ import * as GObject from "@gtkx/gi/gobject";
3
+ import * as Gtk from "@gtkx/gi/gtk";
4
+ import { EDITABLE_ROLES, type EditableTarget, getEditableDelegate, isEditable } from "../editable.js";
5
+ import { formatRoleList } from "../role-helpers.js";
6
+ import { wrapEvent } from "./event-wrapper.js";
7
+
8
+ /** Options for {@link type}. */
9
+ type TypeOptions = {
10
+ /** Do not focus the widget before typing. */
11
+ skipClick?: boolean | undefined;
12
+ /** Select from this offset before typing, replacing the selected range with the typed text. */
13
+ initialSelectionStart?: number | undefined;
14
+ /** End offset of the initial selection (defaults to the start offset). */
15
+ initialSelectionEnd?: number | undefined;
16
+ };
17
+
18
+ const EDITABLE_REQUIRED = `expected editable widget (${formatRoleList(EDITABLE_ROLES)})`;
19
+
20
+ const insertEditableText = (widget: EditableTarget, text: string): void => {
21
+ if (widget instanceof Gtk.TextView) {
22
+ widget.emit("insert-at-cursor", text);
23
+
24
+ return;
25
+ }
26
+
27
+ const target = getEditableDelegate(widget) ?? widget;
28
+
29
+ if (target instanceof Gtk.Text) {
30
+ target.emit("insert-at-cursor", text);
31
+
32
+ return;
33
+ }
34
+
35
+ const position = widget.getPosition();
36
+ const newPosition = widget.insertText(text, text.length, position);
37
+ widget.setPosition(newPosition);
38
+ };
39
+
40
+ const readSelection = (widget: EditableTarget): string => {
41
+ if (widget instanceof Gtk.TextView) {
42
+ const buffer = widget.getBuffer();
43
+ const [hasSelection, start, end] = buffer.getSelectionBounds();
44
+
45
+ return hasSelection ? buffer.getText(start, end, true) : "";
46
+ }
47
+
48
+ const [hasSelection, start, end] = widget.getSelectionBounds();
49
+
50
+ return hasSelection ? widget.getChars(start, end) : "";
51
+ };
52
+
53
+ const deleteSelection = (widget: EditableTarget): void => {
54
+ if (widget instanceof Gtk.TextView) {
55
+ widget.getBuffer().deleteSelection(false, true);
56
+
57
+ return;
58
+ }
59
+
60
+ widget.deleteSelection();
61
+ };
62
+
63
+ const setEditableText = (widget: EditableTarget, text: string): void => {
64
+ if (widget instanceof Gtk.TextView) {
65
+ widget.getBuffer().setText(text, text.length);
66
+
67
+ return;
68
+ }
69
+
70
+ widget.setText(text);
71
+ };
72
+
73
+ const applyTextViewSelection = (widget: Gtk.TextView, start: number, end: number): void => {
74
+ const buffer = widget.getBuffer();
75
+ buffer.selectRange(buffer.getIterAtOffset(start), buffer.getIterAtOffset(end));
76
+
77
+ if (end !== start) {
78
+ deleteSelection(widget);
79
+ }
80
+ };
81
+
82
+ const applyEditableSelection = (widget: Gtk.Editable, start: number, end: number): void => {
83
+ widget.selectRegion(start, end);
84
+
85
+ if (end !== start) {
86
+ widget.deleteSelection();
87
+ }
88
+
89
+ widget.setPosition(start);
90
+ };
91
+
92
+ const applyInitialSelection = (widget: EditableTarget, options: TypeOptions): void => {
93
+ if (options.initialSelectionStart === undefined) {
94
+ return;
95
+ }
96
+
97
+ const start = options.initialSelectionStart;
98
+ const end = options.initialSelectionEnd ?? start;
99
+
100
+ if (widget instanceof Gtk.TextView) {
101
+ applyTextViewSelection(widget, start, end);
102
+
103
+ return;
104
+ }
105
+
106
+ applyEditableSelection(widget, start, end);
107
+ };
108
+
109
+ const writeClipboardText = (widget: Gtk.Widget, text: string): void => {
110
+ const value = GObject.buildValue(GObject.TYPE_STRING, (v) => {
111
+ v.setString(text);
112
+ });
113
+
114
+ widget.getClipboard().set(value);
115
+ };
116
+
117
+ const resetClipboard = (): void => {
118
+ Gdk.Display.getDefault()?.getClipboard().setContent(null);
119
+ };
120
+
121
+ const runEditableEvent = (
122
+ widget: Gtk.Widget,
123
+ failure: string,
124
+ action: (editable: EditableTarget) => void,
125
+ ): Promise<void> =>
126
+ wrapEvent(widget, () => {
127
+ if (!isEditable(widget)) {
128
+ throw new Error(`${failure}: ${EDITABLE_REQUIRED}`);
129
+ }
130
+
131
+ action(widget);
132
+ });
133
+
134
+ const type = (widget: Gtk.Widget, text: string, options?: TypeOptions): Promise<void> =>
135
+ runEditableEvent(widget, "Cannot type into element", (editable) => {
136
+ if (!options?.skipClick) {
137
+ editable.grabFocus();
138
+ }
139
+
140
+ applyInitialSelection(editable, options ?? {});
141
+ insertEditableText(editable, text);
142
+ });
143
+
144
+ const clear = (widget: Gtk.Widget): Promise<void> =>
145
+ runEditableEvent(widget, "Cannot clear element", (editable) => {
146
+ setEditableText(editable, "");
147
+ });
148
+
149
+ const copy = (widget: Gtk.Widget): Promise<void> =>
150
+ runEditableEvent(widget, "Cannot copy", (editable) => {
151
+ writeClipboardText(editable, readSelection(editable));
152
+ });
153
+
154
+ const cut = (widget: Gtk.Widget): Promise<void> =>
155
+ runEditableEvent(widget, "Cannot cut", (editable) => {
156
+ writeClipboardText(editable, readSelection(editable));
157
+ deleteSelection(editable);
158
+ });
159
+
160
+ const paste = async (widget: Gtk.Widget, text?: string): Promise<void> => {
161
+ if (!isEditable(widget)) {
162
+ throw new Error(`Cannot paste: ${EDITABLE_REQUIRED}`);
163
+ }
164
+
165
+ const content = text ?? (await widget.getClipboard().readTextAsync(null)) ?? "";
166
+
167
+ await wrapEvent(widget, () => {
168
+ insertEditableText(widget, content);
169
+ });
170
+ };
171
+
172
+ export { resetClipboard, type, clear, copy, cut, paste, type TypeOptions };