@gtkx/testing 1.0.0-rc.4 → 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 (182) hide show
  1. package/README.md +5 -5
  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/build-queries.d.ts +27 -10
  7. package/dist/build-queries.d.ts.map +1 -1
  8. package/dist/build-queries.js +22 -24
  9. package/dist/build-queries.js.map +1 -1
  10. package/dist/cleanup-registry.d.ts +1 -1
  11. package/dist/cleanup-registry.d.ts.map +1 -1
  12. package/dist/cleanup-registry.js.map +1 -1
  13. package/dist/errors.d.ts +10 -1
  14. package/dist/errors.d.ts.map +1 -1
  15. package/dist/errors.js +10 -1
  16. package/dist/errors.js.map +1 -1
  17. package/dist/fire-event.d.ts +35 -6
  18. package/dist/fire-event.d.ts.map +1 -1
  19. package/dist/fire-event.js +23 -7
  20. package/dist/fire-event.js.map +1 -1
  21. package/dist/frame-sync.d.ts +2 -1
  22. package/dist/frame-sync.d.ts.map +1 -1
  23. package/dist/frame-sync.js +15 -5
  24. package/dist/frame-sync.js.map +1 -1
  25. package/dist/harness-window.d.ts +4 -0
  26. package/dist/harness-window.d.ts.map +1 -0
  27. package/dist/harness-window.js +6 -0
  28. package/dist/harness-window.js.map +1 -0
  29. package/dist/index.d.ts +14 -7
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +9 -5
  32. package/dist/index.js.map +1 -1
  33. package/dist/internal.d.ts +4 -0
  34. package/dist/internal.d.ts.map +1 -0
  35. package/dist/internal.js +4 -0
  36. package/dist/internal.js.map +1 -0
  37. package/dist/matchers.d.ts +88 -46
  38. package/dist/matchers.d.ts.map +1 -1
  39. package/dist/matchers.js +156 -18
  40. package/dist/matchers.js.map +1 -1
  41. package/dist/pretty-format.d.ts +2 -0
  42. package/dist/pretty-format.d.ts.map +1 -0
  43. package/dist/pretty-format.js +2 -0
  44. package/dist/pretty-format.js.map +1 -0
  45. package/dist/pretty-widget.d.ts +5 -2
  46. package/dist/pretty-widget.d.ts.map +1 -1
  47. package/dist/pretty-widget.js +73 -56
  48. package/dist/pretty-widget.js.map +1 -1
  49. package/dist/production.d.ts +8 -0
  50. package/dist/production.d.ts.map +1 -0
  51. package/dist/production.js +34 -0
  52. package/dist/production.js.map +1 -0
  53. package/dist/queries.d.ts +98 -56
  54. package/dist/queries.d.ts.map +1 -1
  55. package/dist/queries.js +123 -83
  56. package/dist/queries.js.map +1 -1
  57. package/dist/query-helpers.d.ts +44 -0
  58. package/dist/query-helpers.d.ts.map +1 -0
  59. package/dist/query-helpers.js +57 -0
  60. package/dist/query-helpers.js.map +1 -0
  61. package/dist/render-children.d.ts +9 -0
  62. package/dist/render-children.d.ts.map +1 -0
  63. package/dist/render-children.js +11 -0
  64. package/dist/render-children.js.map +1 -0
  65. package/dist/render.d.ts.map +1 -1
  66. package/dist/render.js +11 -9
  67. package/dist/render.js.map +1 -1
  68. package/dist/role-helpers.d.ts +12 -3
  69. package/dist/role-helpers.d.ts.map +1 -1
  70. package/dist/role-helpers.js +13 -4
  71. package/dist/role-helpers.js.map +1 -1
  72. package/dist/role-naming.d.ts +5 -0
  73. package/dist/role-naming.d.ts.map +1 -0
  74. package/dist/role-naming.js +96 -0
  75. package/dist/role-naming.js.map +1 -0
  76. package/dist/screenshot.d.ts +12 -20
  77. package/dist/screenshot.d.ts.map +1 -1
  78. package/dist/screenshot.js +183 -91
  79. package/dist/screenshot.js.map +1 -1
  80. package/dist/traversal.d.ts +4 -1
  81. package/dist/traversal.d.ts.map +1 -1
  82. package/dist/traversal.js +25 -6
  83. package/dist/traversal.js.map +1 -1
  84. package/dist/types.d.ts +26 -14
  85. package/dist/types.d.ts.map +1 -1
  86. package/dist/types.js.map +1 -1
  87. package/dist/user-event/adjustment.d.ts +3 -0
  88. package/dist/user-event/adjustment.d.ts.map +1 -1
  89. package/dist/user-event/adjustment.js +23 -2
  90. package/dist/user-event/adjustment.js.map +1 -1
  91. package/dist/user-event/click.d.ts +43 -8
  92. package/dist/user-event/click.d.ts.map +1 -1
  93. package/dist/user-event/click.js +175 -44
  94. package/dist/user-event/click.js.map +1 -1
  95. package/dist/user-event/column-header-click.d.ts +4 -0
  96. package/dist/user-event/column-header-click.d.ts.map +1 -0
  97. package/dist/user-event/column-header-click.js +65 -0
  98. package/dist/user-event/column-header-click.js.map +1 -0
  99. package/dist/user-event/event-wrapper.d.ts.map +1 -1
  100. package/dist/user-event/event-wrapper.js +2 -1
  101. package/dist/user-event/event-wrapper.js.map +1 -1
  102. package/dist/user-event/index.d.ts +51 -10
  103. package/dist/user-event/index.d.ts.map +1 -1
  104. package/dist/user-event/index.js +43 -26
  105. package/dist/user-event/index.js.map +1 -1
  106. package/dist/user-event/indexed-children.d.ts +10 -0
  107. package/dist/user-event/indexed-children.d.ts.map +1 -0
  108. package/dist/user-event/indexed-children.js +50 -0
  109. package/dist/user-event/indexed-children.js.map +1 -0
  110. package/dist/user-event/keyboard.d.ts.map +1 -1
  111. package/dist/user-event/keyboard.js +33 -4
  112. package/dist/user-event/keyboard.js.map +1 -1
  113. package/dist/user-event/list-row-click.d.ts +4 -0
  114. package/dist/user-event/list-row-click.d.ts.map +1 -0
  115. package/dist/user-event/list-row-click.js +39 -0
  116. package/dist/user-event/list-row-click.js.map +1 -0
  117. package/dist/user-event/native-click.d.ts +6 -0
  118. package/dist/user-event/native-click.d.ts.map +1 -0
  119. package/dist/user-event/native-click.js +32 -0
  120. package/dist/user-event/native-click.js.map +1 -0
  121. package/dist/user-event/pointer.d.ts +1 -1
  122. package/dist/user-event/pointer.d.ts.map +1 -1
  123. package/dist/user-event/pointer.js +33 -9
  124. package/dist/user-event/pointer.js.map +1 -1
  125. package/dist/user-event/selection.d.ts +11 -8
  126. package/dist/user-event/selection.d.ts.map +1 -1
  127. package/dist/user-event/selection.js +49 -38
  128. package/dist/user-event/selection.js.map +1 -1
  129. package/dist/user-event/text.d.ts +6 -2
  130. package/dist/user-event/text.d.ts.map +1 -1
  131. package/dist/user-event/text.js +68 -18
  132. package/dist/user-event/text.js.map +1 -1
  133. package/dist/widget-accessible-properties.d.ts +14 -26
  134. package/dist/widget-accessible-properties.d.ts.map +1 -1
  135. package/dist/widget-accessible-properties.js +135 -190
  136. package/dist/widget-accessible-properties.js.map +1 -1
  137. package/dist/widget-getters.d.ts +8 -0
  138. package/dist/widget-getters.d.ts.map +1 -0
  139. package/dist/widget-getters.js +19 -0
  140. package/dist/widget-getters.js.map +1 -0
  141. package/dist/within.d.ts +3 -2
  142. package/dist/within.d.ts.map +1 -1
  143. package/dist/within.js +3 -2
  144. package/dist/within.js.map +1 -1
  145. package/package.json +12 -7
  146. package/src/accessible-native.ts +333 -0
  147. package/src/build-queries.ts +47 -41
  148. package/src/cleanup-registry.ts +1 -1
  149. package/src/errors.ts +12 -1
  150. package/src/fire-event.ts +61 -8
  151. package/src/frame-sync.ts +21 -5
  152. package/src/harness-window.ts +9 -0
  153. package/src/index.ts +18 -11
  154. package/src/internal.ts +11 -0
  155. package/src/matchers.ts +428 -70
  156. package/src/pretty-format.ts +1 -0
  157. package/src/pretty-widget.ts +97 -70
  158. package/src/production.ts +52 -0
  159. package/src/queries.ts +127 -89
  160. package/src/query-helpers.ts +97 -0
  161. package/src/render-children.ts +25 -0
  162. package/src/render.tsx +16 -11
  163. package/src/role-helpers.ts +14 -4
  164. package/src/role-naming.ts +99 -0
  165. package/src/screenshot.ts +272 -103
  166. package/src/traversal.ts +44 -6
  167. package/src/types.ts +28 -16
  168. package/src/user-event/adjustment.ts +29 -2
  169. package/src/user-event/click.ts +247 -52
  170. package/src/user-event/column-header-click.ts +90 -0
  171. package/src/user-event/event-wrapper.ts +3 -1
  172. package/src/user-event/index.ts +100 -37
  173. package/src/user-event/indexed-children.ts +75 -0
  174. package/src/user-event/keyboard.ts +52 -4
  175. package/src/user-event/list-row-click.ts +57 -0
  176. package/src/user-event/native-click.ts +46 -0
  177. package/src/user-event/pointer.ts +48 -9
  178. package/src/user-event/selection.ts +67 -41
  179. package/src/user-event/text.ts +92 -21
  180. package/src/widget-accessible-properties.ts +183 -230
  181. package/src/widget-getters.ts +32 -0
  182. package/src/within.ts +3 -2
package/src/types.ts CHANGED
@@ -55,13 +55,19 @@ type MatcherOptions<T extends Gtk.Widget = Gtk.Widget> = {
55
55
  as?: WidgetType<T> | undefined;
56
56
  } & WaitForOptions;
57
57
 
58
- /** Constraints on a widget's numeric range value used by role queries. */
58
+ /**
59
+ * Constraints on a widget's numeric range value used by role queries. The numbers come from the
60
+ * accessibility tree and match within 0.001, the resolution GTK keeps them to.
61
+ */
59
62
  type ByRoleValue = {
60
63
  /** The current value. */
61
64
  now?: number | undefined;
62
65
  /** The lower bound of the value's range. */
63
66
  min?: number | undefined;
64
- /** The upper bound of the value's range. */
67
+ /**
68
+ * The highest value the widget can reach, which on a paged widget such as a scrollbar is its
69
+ * adjustment's upper bound less one page.
70
+ */
65
71
  max?: number | undefined;
66
72
  /** Matcher for the value's textual representation. */
67
73
  text?: Matcher | undefined;
@@ -87,7 +93,7 @@ type ByRoleOptions<T extends Gtk.Widget = Gtk.Widget> = MatcherOptions<T> & {
87
93
  description?: Matcher | undefined;
88
94
  /** Constraints on the widget's range value, each checked only when given. */
89
95
  value?: ByRoleValue | undefined;
90
- /** When true, include widgets excluded from the accessibility tree. */
96
+ /** When true, include widgets excluded from the accessibility tree. Widgets that are not mapped stay excluded. */
91
97
  hidden?: boolean | undefined;
92
98
  };
93
99
 
@@ -187,8 +193,11 @@ type DebugUtilities = {
187
193
  debug: (element?: Container | Container[], options?: PrettyWidgetOptions) => void;
188
194
  /** Prints the accessible roles found in the scope, with the widgets carrying each one. */
189
195
  logRoles: () => void;
190
- /** Captures a window, writes the image to a temporary file, and logs its path. */
191
- screenshot: (selector?: WindowSelector, options?: ScreenshotOptions) => Promise<ScreenshotResult>;
196
+ /**
197
+ * Captures what is on screen, which is the active toplevel window: GTK4 exposes no position for
198
+ * a toplevel, so windows cannot be composited into a single image.
199
+ */
200
+ screenshot: (options?: ScreenshotOptions) => Promise<ScreenshotResult>;
192
201
  };
193
202
 
194
203
  /** A captured screenshot: base64-encoded image data, its MIME type, and pixel dimensions. */
@@ -203,26 +212,27 @@ type ScreenshotResult = {
203
212
  height: number;
204
213
  };
205
214
 
206
- /** Options for capturing a screenshot: the poll timeout and interval, plus a rendering scale factor. */
215
+ /**
216
+ * Options for capturing a screenshot: the poll timeout and interval, a rendering scale factor, and
217
+ * a file to write the PNG to.
218
+ */
207
219
  type ScreenshotOptions = Pick<WaitForOptions, "timeout" | "interval"> & {
208
220
  /** Device scale factor applied when rendering. */
209
- scale?: number;
221
+ scale?: number | undefined;
222
+ /** File the PNG is written to, with its parent directories created as needed. */
223
+ path?: string | undefined;
210
224
  };
211
225
 
212
- /**
213
- * Selects the window to screenshot by index, or by title (exact string or regular expression);
214
- * undefined targets the default window.
215
- */
216
- type WindowSelector = number | string | RegExp | undefined;
217
-
218
226
  /**
219
227
  * Options for {@link renderHook}: an optional wrapper and the initial props (required unless the
220
228
  * props type permits undefined).
221
229
  */
222
230
  type RenderHookOptions<Props> = {
231
+ /** Component wrapped around the one calling the hook, such as a context provider. */
223
232
  wrapper?: WrapperComponent;
224
233
  } & (undefined extends Props
225
234
  ? {
235
+ /** Props the hook is invoked with until `rerender` is given new ones. */
226
236
  initialProps?: Props;
227
237
  }
228
238
  : {
@@ -234,8 +244,11 @@ type RenderHookOptions<Props> = {
234
244
  * props and to unmount.
235
245
  */
236
246
  type RenderHookResult<Result, Props> = {
237
- /** Holds the most recent value returned by the hook under `current`. */
238
- result: { current: Result };
247
+ /** Stable object the hook's return value is written to on every render. */
248
+ result: {
249
+ /** The hook's latest return value. */
250
+ current: Result;
251
+ };
239
252
  /** Re-invokes the hook, keeping the previous props when none are given. */
240
253
  rerender: (newProps?: Props) => Promise<void>;
241
254
  /** Unmounts the component that calls the hook. */
@@ -260,7 +273,6 @@ export {
260
273
  type DebugUtilities,
261
274
  type ScreenshotResult,
262
275
  type ScreenshotOptions,
263
- type WindowSelector,
264
276
  type RenderHookOptions,
265
277
  type RenderHookResult,
266
278
  };
@@ -1,4 +1,5 @@
1
1
  import * as Gtk from "@gtkx/gi/gtk";
2
+ import { scheduleNextFrame } from "../frame-sync.js";
2
3
  import { wrapEvent } from "./event-wrapper.js";
3
4
 
4
5
  /** A scroll distance in pixels along each axis. */
@@ -44,22 +45,47 @@ const resolveScrollAdjustments = (widget: Gtk.Widget): ScrollAdjustments | null
44
45
  return null;
45
46
  };
46
47
 
48
+ const stepToward = (adjustment: Gtk.Adjustment, target: number, step: number): number => {
49
+ const value = adjustment.getValue();
50
+ adjustment.setValue(value < target ? Math.min(value + step, target) : Math.max(value - step, target));
51
+
52
+ return adjustment.getValue();
53
+ };
54
+
55
+ const rampTo = (adjustment: Gtk.Adjustment, target: number, step: number): void => {
56
+ let value = adjustment.getValue();
57
+
58
+ while (value !== target) {
59
+ const moved = stepToward(adjustment, target, step);
60
+
61
+ if (moved === value) {
62
+ return;
63
+ }
64
+
65
+ value = moved;
66
+ }
67
+ };
68
+
47
69
  const applyScrollDelta = (adjustment: Gtk.Adjustment | null, delta: number): void => {
48
70
  if (!adjustment || delta === 0) {
49
71
  return;
50
72
  }
51
73
 
52
- adjustment.setValue(adjustment.getValue() + delta);
74
+ const pageSize = adjustment.getPageSize();
75
+ rampTo(adjustment, adjustment.getValue() + delta, pageSize > 0 ? pageSize : Math.abs(delta));
53
76
  };
54
77
 
55
78
  /**
56
79
  * Adds the delta to the adjustments of the widget itself, or of its nearest Gtk.ScrolledWindow or
57
80
  * Gtk.Scrollable ancestor.
58
81
  *
82
+ * Each adjustment advances in viewport-sized steps rather than one jump, so virtualized views such
83
+ * as Gtk.ListView, Gtk.GridView and Gtk.ColumnView re-anchor onto the rows the new offset shows.
84
+ *
59
85
  * @throws When neither the widget nor any of its ancestors is scrollable.
60
86
  */
61
87
  const scroll = (widget: Gtk.Widget, delta: ScrollDelta): Promise<void> =>
62
- wrapEvent(widget, () => {
88
+ wrapEvent(widget, async () => {
63
89
  const adjustments = resolveScrollAdjustments(widget);
64
90
 
65
91
  if (!adjustments) {
@@ -70,6 +96,7 @@ const scroll = (widget: Gtk.Widget, delta: ScrollDelta): Promise<void> =>
70
96
 
71
97
  applyScrollDelta(adjustments.horizontal, delta.x ?? 0);
72
98
  applyScrollDelta(adjustments.vertical, delta.y ?? 0);
99
+ await scheduleNextFrame(widget);
73
100
  });
74
101
 
75
102
  export { slide, scroll, type ScrollDelta };
@@ -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,46 +34,205 @@ 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());
88
+
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
+ }
49
104
 
50
- current = current.getParent();
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
 
118
+ const clickTargetFor = (widget: Gtk.Widget, scope: ClickScope): ClickTarget | null => {
119
+ if (isClickTransparent(widget)) {
120
+ return null;
121
+ }
122
+
123
+ const native = nativeClickFor(widget, scope.isClicked);
124
+
125
+ if (native !== null) {
126
+ return { widget, container: null, gestures: getAuthoredClickGestures(widget), isClaiming: true, native };
127
+ }
128
+
129
+ return gestureTargetFor(widget, scope.isInternalAllowed);
130
+ };
131
+
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
+ };
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)) {
179
+ return;
180
+ }
181
+
182
+ selectContainerChild(container, child);
183
+
184
+ if (isMultipleSelection(container)) {
185
+ unselectOtherChildren(container, child);
186
+ }
187
+ };
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) {
199
+ return;
200
+ }
201
+
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
+
215
+ return;
216
+ }
217
+
218
+ target.native(target.widget, nPress);
219
+ };
220
+
221
+ const applyClickOutcomes = (targets: ClickTarget[], nPress: number): void => {
222
+ for (const target of targets) {
223
+ applyClickOutcome(target, nPress);
224
+ }
225
+ };
226
+
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 */
56
236
  const tryActivate = async (widget: Gtk.Widget): Promise<boolean> => {
57
237
  if (widget.getAccessibleRole() === Gtk.AccessibleRole.LABEL) {
58
238
  return false;
@@ -68,39 +248,54 @@ const tryActivate = async (widget: Gtk.Widget): Promise<boolean> => {
68
248
  };
69
249
 
70
250
  /**
71
- * Presses and releases a Gtk.Button, flips a Gtk.Switch, and otherwise activates the widget,
72
- * falling back to a click gesture on the nearest Gtk.Button or click-gesture ancestor when
73
- * activation does nothing. A widget with the label role is never activated.
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.
74
275
  */
75
276
  const click = async (widget: Gtk.Widget): Promise<void> => {
76
- if (widget instanceof Gtk.Button) {
77
- await emitClickSequence(widget, widget, 1);
78
-
79
- return;
80
- }
81
-
82
- if (widget instanceof Gtk.Switch) {
83
- await wrapEvent(widget, () => {
84
- widget.setActive(!widget.getActive());
85
- });
86
-
87
- return;
88
- }
89
-
90
- if (await tryActivate(widget)) {
277
+ if (!isSelfClickTarget(widget) && (await tryActivate(widget))) {
91
278
  return;
92
279
  }
93
280
 
94
- const target = findClickableAncestor(widget);
95
-
96
- if (target) {
97
- await emitClickSequence(widget, target, 1);
98
- }
281
+ await deliverClick(widget, 1);
99
282
  };
100
283
 
101
- /** Emits a two-press click gesture at the widget's center, adding a Gtk.GestureClick when it has none. */
102
- const dblClick = (widget: Gtk.Widget): Promise<void> => emitClickSequence(widget, widget, 2);
103
- /** Emits a three-press click gesture at the widget's center, adding a Gtk.GestureClick when it has none. */
104
- const tripleClick = (widget: Gtk.Widget): Promise<void> => emitClickSequence(widget, widget, 3);
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);
105
300
 
106
- 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 };
@@ -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