@gtkx/testing 2.0.0-beta.1 → 2.0.0-beta.10

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 (112) hide show
  1. package/README.md +43 -40
  2. package/dist/accessible-native.d.ts +1 -1
  3. package/dist/accessible-native.d.ts.map +1 -1
  4. package/dist/accessible-native.js +29 -8
  5. package/dist/accessible-native.js.map +1 -1
  6. package/dist/build-queries.d.ts +9 -9
  7. package/dist/build-queries.d.ts.map +1 -1
  8. package/dist/build-queries.js +2 -1
  9. package/dist/build-queries.js.map +1 -1
  10. package/dist/fire-event.d.ts +3 -3
  11. package/dist/fire-event.d.ts.map +1 -1
  12. package/dist/fire-event.js +14 -4
  13. package/dist/fire-event.js.map +1 -1
  14. package/dist/matchers.d.ts +52 -56
  15. package/dist/matchers.d.ts.map +1 -1
  16. package/dist/matchers.js +4 -16
  17. package/dist/matchers.js.map +1 -1
  18. package/dist/queries.d.ts +7 -7
  19. package/dist/queries.d.ts.map +1 -1
  20. package/dist/queries.js +18 -10
  21. package/dist/queries.js.map +1 -1
  22. package/dist/query-helpers.d.ts +2 -2
  23. package/dist/query-helpers.d.ts.map +1 -1
  24. package/dist/query-helpers.js +10 -3
  25. package/dist/query-helpers.js.map +1 -1
  26. package/dist/render.d.ts.map +1 -1
  27. package/dist/render.js +4 -0
  28. package/dist/render.js.map +1 -1
  29. package/dist/role-helpers.d.ts +1 -1
  30. package/dist/role-helpers.d.ts.map +1 -1
  31. package/dist/role-helpers.js +2 -1
  32. package/dist/role-helpers.js.map +1 -1
  33. package/dist/screenshot.d.ts +1 -1
  34. package/dist/screenshot.d.ts.map +1 -1
  35. package/dist/screenshot.js +25 -5
  36. package/dist/screenshot.js.map +1 -1
  37. package/dist/suggestions.d.ts +1 -1
  38. package/dist/suggestions.d.ts.map +1 -1
  39. package/dist/suggestions.js +3 -1
  40. package/dist/suggestions.js.map +1 -1
  41. package/dist/traversal.d.ts +4 -4
  42. package/dist/traversal.d.ts.map +1 -1
  43. package/dist/traversal.js +25 -7
  44. package/dist/traversal.js.map +1 -1
  45. package/dist/types.d.ts +10 -10
  46. package/dist/types.d.ts.map +1 -1
  47. package/dist/types.js.map +1 -1
  48. package/dist/user-event/click.d.ts +17 -4
  49. package/dist/user-event/click.d.ts.map +1 -1
  50. package/dist/user-event/click.js +28 -9
  51. package/dist/user-event/click.js.map +1 -1
  52. package/dist/user-event/controller.d.ts +5 -5
  53. package/dist/user-event/controller.d.ts.map +1 -1
  54. package/dist/user-event/controller.js +5 -2
  55. package/dist/user-event/controller.js.map +1 -1
  56. package/dist/user-event/index.d.ts +22 -22
  57. package/dist/user-event/index.d.ts.map +1 -1
  58. package/dist/user-event/index.js +28 -25
  59. package/dist/user-event/index.js.map +1 -1
  60. package/dist/user-event/keyboard.d.ts.map +1 -1
  61. package/dist/user-event/keyboard.js +23 -2
  62. package/dist/user-event/keyboard.js.map +1 -1
  63. package/dist/user-event/list-row-click.d.ts +1 -1
  64. package/dist/user-event/list-row-click.d.ts.map +1 -1
  65. package/dist/user-event/list-row-click.js +2 -1
  66. package/dist/user-event/list-row-click.js.map +1 -1
  67. package/dist/user-event/pointer.d.ts.map +1 -1
  68. package/dist/user-event/pointer.js +9 -11
  69. package/dist/user-event/pointer.js.map +1 -1
  70. package/dist/user-event/selection.d.ts +2 -2
  71. package/dist/user-event/selection.d.ts.map +1 -1
  72. package/dist/user-event/selection.js +8 -2
  73. package/dist/user-event/selection.js.map +1 -1
  74. package/dist/wait-for.d.ts +2 -2
  75. package/dist/wait-for.d.ts.map +1 -1
  76. package/dist/wait-for.js +6 -4
  77. package/dist/wait-for.js.map +1 -1
  78. package/dist/widget-accessible-properties.d.ts +4 -3
  79. package/dist/widget-accessible-properties.d.ts.map +1 -1
  80. package/dist/widget-accessible-properties.js +24 -17
  81. package/dist/widget-accessible-properties.js.map +1 -1
  82. package/dist/widget-getters.d.ts +7 -9
  83. package/dist/widget-getters.d.ts.map +1 -1
  84. package/dist/widget-getters.js.map +1 -1
  85. package/dist/widget-target.d.ts +4 -0
  86. package/dist/widget-target.d.ts.map +1 -0
  87. package/dist/widget-target.js +9 -0
  88. package/dist/widget-target.js.map +1 -0
  89. package/package.json +13 -10
  90. package/src/accessible-native.ts +45 -9
  91. package/src/build-queries.ts +60 -30
  92. package/src/fire-event.ts +32 -8
  93. package/src/matchers.ts +63 -85
  94. package/src/queries.ts +64 -30
  95. package/src/query-helpers.ts +17 -9
  96. package/src/render.tsx +7 -2
  97. package/src/role-helpers.ts +3 -1
  98. package/src/screenshot.ts +48 -5
  99. package/src/suggestions.ts +3 -1
  100. package/src/traversal.ts +35 -11
  101. package/src/types.ts +14 -10
  102. package/src/user-event/click.ts +37 -17
  103. package/src/user-event/controller.ts +8 -5
  104. package/src/user-event/index.ts +133 -47
  105. package/src/user-event/keyboard.ts +32 -3
  106. package/src/user-event/list-row-click.ts +2 -2
  107. package/src/user-event/pointer.ts +11 -15
  108. package/src/user-event/selection.ts +12 -4
  109. package/src/wait-for.ts +9 -6
  110. package/src/widget-accessible-properties.ts +39 -25
  111. package/src/widget-getters.ts +11 -13
  112. package/src/widget-target.ts +11 -0
@@ -3,6 +3,7 @@ import { sortStringsBy } from "@gtkx/utils";
3
3
  import { type Container, traverse } from "./traversal.js";
4
4
  import { getWidgetAccessibleName, getWidgetLevel } from "./widget-accessible-properties.js";
5
5
  import { getTypeTag } from "./widget-getters.js";
6
+ import { requireWidget } from "./widget-target.js";
6
7
 
7
8
  const ROLE_NAMES_BY_VALUE = enumNamesByValue(Gtk.AccessibleRole);
8
9
 
@@ -125,6 +126,7 @@ const logRoles = (container: Container): void => {
125
126
  *
126
127
  * @param widget The widget to read the level from.
127
128
  */
128
- const computeHeadingLevel = (widget: Gtk.Widget): number | undefined => getWidgetLevel(widget) ?? undefined;
129
+ const computeHeadingLevel = (widget: Gtk.Accessible): number | undefined =>
130
+ getWidgetLevel(requireWidget(widget)) ?? undefined;
129
131
 
130
132
  export { computeHeadingLevel, formatRole, formatRoleList, getRoles, prettyRoles, logRoles };
package/src/screenshot.ts CHANGED
@@ -9,6 +9,7 @@ import { getConfig } from "./config.js";
9
9
  import { now } from "./timers.js";
10
10
  import { descendants } from "./traversal.js";
11
11
  import { waitFor } from "./wait-for.js";
12
+ import { requireWidget } from "./widget-target.js";
12
13
  import { activeToplevel, mappedToplevels } from "./window-state.js";
13
14
 
14
15
  type FrameProbe = {
@@ -31,6 +32,20 @@ type CaptureOutcome =
31
32
  { status: "captured"; result: ScreenshotResult } |
32
33
  { status: "stalled"; failure: CaptureFailure };
33
34
 
35
+ type BackgroundSnapshot = {
36
+ renderBackground(...args: [context: object, x: number, y: number, width: number, height: number]): void;
37
+ };
38
+
39
+ type StyledWidget = {
40
+ getStyleContext(): object;
41
+ };
42
+
43
+ type RenderSize = {
44
+ scale: number;
45
+ width: number;
46
+ height: number;
47
+ };
48
+
34
49
  const DEFAULT_SCREENSHOT_INTERVAL = 10;
35
50
  const PRESENTATION_PROBE_MS = 250;
36
51
 
@@ -179,16 +194,43 @@ const getChildrenNode = (widget: Gtk.Widget, scale: number): Gsk.RenderNode | nu
179
194
  return snapshot.toNode();
180
195
  };
181
196
 
197
+ const addWindowBackground = (
198
+ widget: Gtk.Widget,
199
+ node: Gsk.RenderNode,
200
+ { scale, width, height }: RenderSize,
201
+ ): Gsk.RenderNode => {
202
+ if (!(widget instanceof Gtk.Window)) {
203
+ return node;
204
+ }
205
+
206
+ const snapshot = new Gtk.Snapshot();
207
+ const backgroundSnapshot: BackgroundSnapshot = snapshot;
208
+ const styledWidget: StyledWidget = widget;
209
+ snapshot.save();
210
+ snapshot.scale(scale, scale);
211
+ backgroundSnapshot.renderBackground(styledWidget.getStyleContext(), 0, 0, width, height);
212
+ snapshot.restore();
213
+ snapshot.appendNode(node);
214
+
215
+ return snapshot.toNode() ?? node;
216
+ };
217
+
218
+ const getFallbackNode = (widget: Gtk.Widget, size: RenderSize): Gsk.RenderNode | null => {
219
+ const content = getChildrenNode(widget, size.scale);
220
+
221
+ return content === null ? null : addWindowBackground(widget, content, size);
222
+ };
223
+
182
224
  const getRenderNode = (widget: Gtk.Widget, scale: number, width: number, height: number): Gsk.RenderNode | null => {
183
- const presented = getPaintableNode(widget, scale, width, height);
225
+ const content = getPaintableNode(widget, scale, width, height);
184
226
 
185
- if (presented) {
186
- return presented;
227
+ if (content) {
228
+ return content;
187
229
  }
188
230
 
189
231
  allocateRoot(widget);
190
232
 
191
- return getPaintableNode(widget, scale, width, height) ?? getChildrenNode(widget, scale);
233
+ return getPaintableNode(widget, scale, width, height) ?? getFallbackNode(widget, { scale, width, height });
192
234
  };
193
235
 
194
236
  const renderToPng = (widget: Gtk.Widget, node: Gsk.RenderNode, width: number, height: number): ScreenshotResult => {
@@ -301,7 +343,8 @@ const captureUntilPaintable = (
301
343
  * @param options Optional scale, timeout, retry interval, and output path.
302
344
  * @returns The base64-encoded PNG data along with its mime type and dimensions.
303
345
  */
304
- const screenshot = async (widget: Gtk.Widget, options?: ScreenshotOptions): Promise<ScreenshotResult> => {
346
+ const screenshot = async (target: Gtk.Accessible, options?: ScreenshotOptions): Promise<ScreenshotResult> => {
347
+ const widget = requireWidget(target);
305
348
  const scale = options?.scale ?? 1;
306
349
 
307
350
  if (!Number.isFinite(scale) || scale <= 0) {
@@ -9,6 +9,7 @@ import {
9
9
  getWidgetOwnLabel,
10
10
  getWidgetPlaceholderText,
11
11
  } from "./widget-accessible-properties.js";
12
+ import { requireWidget } from "./widget-target.js";
12
13
 
13
14
  /** The query variant a suggestion targets. */
14
15
  type Variant = "get" | "getAll" | "query" | "queryAll" | "find" | "findAll";
@@ -75,10 +76,11 @@ const textSuggestion = (queryName: Method, variant: Variant, value: string | nul
75
76
  * @returns The best available suggestion, or undefined when no query applies.
76
77
  */
77
78
  const getSuggestedQuery = (
78
- widget: Gtk.Widget,
79
+ target: Gtk.Accessible,
79
80
  variant: Variant = "get",
80
81
  method?: Method,
81
82
  ): Suggestion | undefined => {
83
+ const widget = requireWidget(target);
82
84
  const builders: Record<Method, () => Suggestion | undefined> = {
83
85
  Role: () => roleSuggestion(widget, variant),
84
86
  LabelText: () =>
package/src/traversal.ts CHANGED
@@ -1,25 +1,41 @@
1
1
  import * as Gtk from "@gtkx/gi/gtk";
2
+ import { getClassType, getInstanceType, typeIsA } from "@gtkx/runtime";
2
3
 
3
4
  /**
4
5
  * A scope that resolves to a single root widget: the widget itself, the widget a controller or
5
6
  * layout manager is attached to, or a list item's or header's child.
6
7
  */
7
- type QueryContainer = Gtk.Widget | Gtk.EventController | Gtk.LayoutManager | Gtk.ListItem | Gtk.ListHeader;
8
+ type QueryContainer = Gtk.Accessible | Gtk.EventController | Gtk.LayoutManager | Gtk.ListItem | Gtk.ListHeader;
8
9
  /**
9
10
  * A scope that queries and traversal can run against: a widget, an event
10
11
  * controller, a layout manager, a list item or header, an application, or the
11
12
  * sentinel representing all current toplevel windows.
12
13
  */
13
14
  type Container = QueryContainer | Gtk.Application | typeof TOPLEVELS;
14
- type WidgetClass<T extends Gtk.Widget> = abstract new (...args: never[]) => T;
15
+ type WidgetClass<T extends object> = abstract new (...args: never[]) => T;
15
16
 
16
17
  /** Container sentinel that widens a query to every toplevel window currently open. */
17
18
  const TOPLEVELS: unique symbol = Symbol("gtkx.toplevels");
19
+ const nativeWidgetTypes: Map<bigint, boolean> = new Map();
18
20
 
19
21
  const isApplication = (container: Container): container is Gtk.Application => container instanceof Gtk.Application;
20
22
  const isAnyWidget = (): boolean => true;
21
23
  const isOnScreen = (widget: Gtk.Widget): boolean => widget.getMapped();
22
24
 
25
+ const isNativeWidget = (widget: Gtk.Widget): boolean => {
26
+ const type = getInstanceType(widget);
27
+ const cached = nativeWidgetTypes.get(type);
28
+
29
+ if (cached !== undefined) {
30
+ return cached;
31
+ }
32
+
33
+ const isNative = typeIsA(type, getClassType(Gtk.Native));
34
+ nativeWidgetTypes.set(type, isNative);
35
+
36
+ return isNative;
37
+ };
38
+
23
39
  const traverseWidgetTree = function* (
24
40
  root: Gtk.Widget,
25
41
  isIncluded: (widget: Gtk.Widget) => boolean,
@@ -60,7 +76,7 @@ const ancestors = function* (widget: Gtk.Widget): Generator<Gtk.Widget> {
60
76
  }
61
77
  };
62
78
 
63
- const ancestorFor = <T extends Gtk.Widget>(widget: Gtk.Widget, type: WidgetClass<T>): T | null => {
79
+ const ancestorFor = <T extends object>(widget: Gtk.Widget, type: WidgetClass<T>): T | null => {
64
80
  for (const ancestor of ancestors(widget)) {
65
81
  if (ancestor instanceof type) {
66
82
  return ancestor;
@@ -70,15 +86,23 @@ const ancestorFor = <T extends Gtk.Widget>(widget: Gtk.Widget, type: WidgetClass
70
86
  return null;
71
87
  };
72
88
 
73
- const relationCandidates = (widget: Gtk.Widget): Gtk.Accessible[] => {
74
- const pool: Gtk.Accessible[] = [...widget.listMnemonicLabels(), ...descendants(widget)];
75
- const root = widget.getRoot();
89
+ const mappedWidgets = function* (widget: Gtk.Widget, isParentMapped: boolean): Generator<Gtk.Widget> {
90
+ const isMapped = (isParentMapped || isNativeWidget(widget)) && isOnScreen(widget);
76
91
 
77
- if (root instanceof Gtk.Widget) {
78
- pool.push(...traverseWidgetTree(root, isAnyWidget));
92
+ if (isMapped) {
93
+ yield widget;
79
94
  }
80
95
 
81
- return [...new Set(pool)];
96
+ for (const child of children(widget)) {
97
+ yield* mappedWidgets(child, isMapped);
98
+ }
99
+ };
100
+
101
+ const relationCandidates = (widget: Gtk.Widget): Iterable<Gtk.Accessible>[] => {
102
+ const root = widget.getRoot();
103
+ const tree: Iterable<Gtk.Accessible> = root instanceof Gtk.Widget ? traverseWidgetTree(root, isAnyWidget) : [];
104
+
105
+ return [widget.listMnemonicLabels(), descendants(widget), tree];
82
106
  };
83
107
 
84
108
  const resolveRoot = (container: QueryContainer): Gtk.Widget | null => {
@@ -98,7 +122,7 @@ const resolveRoot = (container: QueryContainer): Gtk.Widget | null => {
98
122
  return container.getChild();
99
123
  }
100
124
 
101
- return null;
125
+ throw new TypeError("Query container must resolve to a Gtk.Widget");
102
126
  };
103
127
 
104
128
  const roots = function* (container: Container): Generator<Gtk.Widget> {
@@ -117,7 +141,7 @@ const roots = function* (container: Container): Generator<Gtk.Widget> {
117
141
 
118
142
  const traverse = function* (container: Container): Generator<Gtk.Widget> {
119
143
  for (const root of roots(container)) {
120
- yield* traverseWidgetTree(root, isOnScreen);
144
+ yield* mappedWidgets(root, true);
121
145
  }
122
146
  };
123
147
 
package/src/types.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type * as Gtk from "@gtkx/gi/gtk";
2
- import type { RootElement } from "@gtkx/react";
2
+ import type { CaughtErrorInfo, RootElement } from "@gtkx/react";
3
3
  import type { ComponentType, ErrorInfo, ReactNode } from "react";
4
4
  import type { PrettyWidgetOptions } from "./pretty-widget.js";
5
5
  import type { Container } from "./traversal.js";
@@ -15,9 +15,11 @@ type Matcher = string | number | RegExp | MatcherFunction;
15
15
  type NormalizerFn = (text: string) => string;
16
16
  /**
17
17
  * A widget class usable as a query's `as` constraint, such as `Gtk.Button`. Abstract classes and
18
- * generated GInterface pseudo-classes are accepted.
18
+ * generated GInterface pseudo-classes that extend `Gtk.Accessible` are accepted.
19
19
  */
20
- type WidgetType<T extends Gtk.Widget = Gtk.Widget> = abstract new (...args: never[]) => T;
20
+ type WidgetType<T extends Gtk.Accessible = Gtk.Accessible> = abstract new (
21
+ ...args: never[]
22
+ ) => T;
21
23
 
22
24
  /** Options controlling the default text normalizer. */
23
25
  type NormalizerOptions = {
@@ -40,7 +42,7 @@ type WaitForOptions = {
40
42
  };
41
43
 
42
44
  /** Options controlling text matching and, for asynchronous queries, polling behavior. */
43
- type MatcherOptions<T extends Gtk.Widget = Gtk.Widget> = {
45
+ type MatcherOptions<T extends Gtk.Accessible = Gtk.Accessible> = {
44
46
  /** When true (the default), require an exact match; when false, match case-insensitively as a substring. */
45
47
  exact?: boolean | undefined;
46
48
  /** Custom normalizer replacing the default; cannot be combined with `trim` or `collapseWhitespace`. */
@@ -74,7 +76,7 @@ type ByRoleValue = {
74
76
  };
75
77
 
76
78
  /** Options for role queries: an accessible name matcher plus accessible state and value constraints. */
77
- type ByRoleOptions<T extends Gtk.Widget = Gtk.Widget> = MatcherOptions<T> & {
79
+ type ByRoleOptions<T extends Gtk.Accessible = Gtk.Accessible> = MatcherOptions<T> & {
78
80
  /** Matcher for the widget's accessible name. */
79
81
  name?: Matcher | undefined;
80
82
  /** Required checked state; a mixed check button reads as neither, so it matches neither value. */
@@ -116,7 +118,7 @@ type BoundQuery<Q extends Query> = Q extends (container: Container, ...args: inf
116
118
  type BoundCustomQueries<Q extends QueryMap> = { [K in keyof Q]: BoundQuery<Q[K]> };
117
119
 
118
120
  /** What each variant of a query family yields for the widget type it matched. */
119
- type QueryFamilyReturns<T extends Gtk.Widget> = {
121
+ type QueryFamilyReturns<T extends Gtk.Accessible> = {
120
122
  /** The single match, or null when nothing matched; throws when more than one matched. */
121
123
  queryBy: T | null;
122
124
  /** Every match, empty when nothing matched. */
@@ -135,7 +137,7 @@ type QueryFamilyReturns<T extends Gtk.Widget> = {
135
137
  type QueryKind = "role" | "text" | "name" | "value";
136
138
 
137
139
  /** The arguments a query of the given kind takes after the family's leading ones. */
138
- type QueryArgs<Kind extends QueryKind, T extends Gtk.Widget> = Kind extends "role"
140
+ type QueryArgs<Kind extends QueryKind, T extends Gtk.Accessible> = Kind extends "role"
139
141
  ? [role: Gtk.AccessibleRole, options?: ByRoleOptions<T>]
140
142
  : Kind extends "name"
141
143
  ? [name: Matcher, options?: MatcherOptions<T>]
@@ -148,7 +150,9 @@ type QueryArgs<Kind extends QueryKind, T extends Gtk.Widget> = Kind extends "rol
148
150
  * ahead of the family's own arguments.
149
151
  */
150
152
  type QueryFamily<Suffix extends string, Kind extends QueryKind, Head extends unknown[]> = {
151
- [K in keyof QueryFamilyReturns<Gtk.Widget> as `${K & string}${Suffix}`]: <T extends Gtk.Widget = Gtk.Widget>(
153
+ [K in keyof QueryFamilyReturns<Gtk.Widget> as `${K & string}${Suffix}`]: <
154
+ T extends Gtk.Accessible = Gtk.Widget,
155
+ >(
152
156
  ...args: [...Head, ...QueryArgs<Kind, T>]
153
157
  ) => QueryFamilyReturns<T>[K];
154
158
  };
@@ -175,7 +179,7 @@ type RenderOptions<Q extends QueryMap = Record<never, never>> = {
175
179
  * out of reach of a pointer and a keyboard, so every `userEvent` helper aimed at it rejects
176
180
  * after `actionabilityTimeout`; `fireEvent` drives such a tree instead.
177
181
  */
178
- container?: Gtk.Widget | RootElement | undefined;
182
+ container?: Gtk.Accessible | RootElement | undefined;
179
183
  /** Root of the subtree that bound queries search. */
180
184
  baseElement?: Container | undefined;
181
185
  /** Component wrapped around the rendered element, such as a context provider. */
@@ -185,7 +189,7 @@ type RenderOptions<Q extends QueryMap = Record<never, never>> = {
185
189
  /** Enable widget animations during the test. */
186
190
  areAnimationsEnabled?: boolean | undefined;
187
191
  /** Called for errors caught by React error boundaries. */
188
- onCaughtError?: ((error: unknown, errorInfo: ErrorInfo) => void) | undefined;
192
+ onCaughtError?: ((error: unknown, errorInfo: CaughtErrorInfo) => void) | undefined;
189
193
  /** Called for errors React recovered from automatically. */
190
194
  onRecoverableError?: ((error: unknown, errorInfo: ErrorInfo) => void) | undefined;
191
195
  /** Custom queries to bind to the rendered result. */
@@ -11,6 +11,7 @@ type PressPoint = { x: number; y: number };
11
11
  type ClickPhase = "pressed" | "released";
12
12
  type ClickSite = { gestures: Gtk.GestureClick[]; point: PressPoint };
13
13
  type ClickScope = { isClicked: boolean; isInternalAllowed: boolean };
14
+ type DirectClick = { sites: ClickSite[]; outcome: NativeClick | null };
14
15
 
15
16
  type ClickTarget = {
16
17
  widget: Gtk.Widget;
@@ -59,15 +60,6 @@ const emitGesture = (
59
60
  }
60
61
  };
61
62
 
62
- const emitClickPhase = (
63
- widget: Gtk.Widget,
64
- controllers: Gtk.GestureClick[],
65
- nPress: number,
66
- phase: ClickPhase,
67
- ): void => {
68
- emitGesture(getCenterPoint(widget), controllers, nPress, phase);
69
- };
70
-
71
63
  const emitSitePhase = (sites: ClickSite[], nPress: number, phase: ClickPhase): void => {
72
64
  for (const site of sites) {
73
65
  emitGesture(site.point, site.gestures, nPress, phase);
@@ -97,14 +89,23 @@ const clickGestures = (widget: Gtk.Widget): Gtk.GestureClick[] =>
97
89
  const getAuthoredClickGestures = (widget: Gtk.Widget): Gtk.GestureClick[] =>
98
90
  clickGestures(widget).filter((gesture) => hasClickHandlers(gesture));
99
91
 
100
- const isClaimingTarget = (widget: Gtk.Widget): boolean =>
101
- widget instanceof Gtk.Button || containerFor(widget) !== null;
102
-
103
92
  const gestureTargetFor = (widget: Gtk.Widget, isInternalAllowed: boolean): ClickTarget | null => {
104
93
  const gestures = clickGestures(widget);
105
94
 
106
- if (isClaimingTarget(widget)) {
107
- return { widget, container: containerFor(widget), gestures, isClaiming: true, native: null };
95
+ if (widget instanceof Gtk.Button) {
96
+ return { widget, container: null, gestures, isClaiming: true, native: null };
97
+ }
98
+
99
+ const container = containerFor(widget);
100
+
101
+ if (container !== null) {
102
+ return {
103
+ widget,
104
+ container,
105
+ gestures: gestures.filter((gesture) => hasClickHandlers(gesture)),
106
+ isClaiming: true,
107
+ native: null,
108
+ };
108
109
  }
109
110
 
110
111
  const authored = gestures.filter((gesture) => hasClickHandlers(gesture));
@@ -205,7 +206,7 @@ const applyContainerOutcome = (target: ClickTarget, nPress: number): void => {
205
206
  }
206
207
 
207
208
  if (isActivatedByClick(container, nPress)) {
208
- widget.activate();
209
+ Gtk.Widget.prototype.activate.call(widget);
209
210
  }
210
211
 
211
212
  if (isSelectionReplacedByClick(container, nPress)) {
@@ -223,6 +224,24 @@ const applyClickOutcome = (target: ClickTarget, nPress: number): void => {
223
224
  target.native(target.widget, nPress);
224
225
  };
225
226
 
227
+ const applyContainerClick: NativeClick = (widget, nPress) => {
228
+ const container = containerFor(widget);
229
+
230
+ if (container !== null) {
231
+ applyContainerOutcome({ widget, container, gestures: [], isClaiming: true, native: null }, nPress);
232
+ }
233
+ };
234
+
235
+ const directClickFor = (widget: Gtk.Widget): DirectClick => {
236
+ const native = nativeClickFor(widget, true);
237
+ const container = native === null ? containerFor(widget) : null;
238
+ const gestures =
239
+ native !== null || container !== null ? getAuthoredClickGestures(widget) : clickGestures(widget);
240
+ const target: ClickTarget = { widget, container, gestures, isClaiming: true, native };
241
+
242
+ return { sites: targetSites(widget, target), outcome: native ?? (container === null ? null : applyContainerClick) };
243
+ };
244
+
226
245
  const applyClickOutcomes = (targets: ClickTarget[], nPress: number): void => {
227
246
  for (const target of targets) {
228
247
  applyClickOutcome(target, nPress);
@@ -246,7 +265,7 @@ const tryActivate = async (widget: Gtk.Widget): Promise<boolean> => {
246
265
  let isActivated = false;
247
266
 
248
267
  await wrapEvent(widget, () => {
249
- isActivated = widget.activate();
268
+ isActivated = Gtk.Widget.prototype.activate.call(widget);
250
269
  });
251
270
 
252
271
  return isActivated;
@@ -266,4 +285,5 @@ const dblClick = (widget: Gtk.Widget): Promise<void> => deliverClick(widget, 2);
266
285
  /** Delivers a triple-click gesture. */
267
286
  const tripleClick = (widget: Gtk.Widget): Promise<void> => deliverClick(widget, 3);
268
287
 
269
- export { clickGestures, emitClickPhase, getAuthoredClickGestures, click, dblClick, tripleClick };
288
+ export { directClickFor, emitSitePhase, click, dblClick, tripleClick };
289
+ export type { DirectClick };
@@ -1,4 +1,5 @@
1
1
  import type * as Gtk from "@gtkx/gi/gtk";
2
+ import { requireWidget } from "../widget-target.js";
2
3
 
3
4
  /** An event controller class that can be constructed without arguments, such as `Gtk.GestureClick`. */
4
5
  type ControllerConstructor<T extends Gtk.EventController> = new () => T;
@@ -11,9 +12,10 @@ type ControllerConstructor<T extends Gtk.EventController> = new () => T;
11
12
  * @returns The matching controllers, or an empty array when the widget has none.
12
13
  */
13
14
  const queryAllControllers = <T extends Gtk.EventController>(
14
- widget: Gtk.Widget,
15
+ target: Gtk.Accessible,
15
16
  controllerType: ControllerConstructor<T>,
16
17
  ): T[] => {
18
+ const widget = requireWidget(target);
17
19
  const controllers = widget.observeControllers();
18
20
  const nItems = controllers.getNItems();
19
21
  const matches: T[] = [];
@@ -37,7 +39,7 @@ const queryAllControllers = <T extends Gtk.EventController>(
37
39
  * @returns The first matching controller, or null when the widget has none.
38
40
  */
39
41
  const queryController = <T extends Gtk.EventController>(
40
- widget: Gtk.Widget,
42
+ widget: Gtk.Accessible,
41
43
  controllerType: ControllerConstructor<T>,
42
44
  ): T | null => queryAllControllers(widget, controllerType)[0] ?? null;
43
45
 
@@ -49,7 +51,7 @@ const queryController = <T extends Gtk.EventController>(
49
51
  * @throws When the widget has no controller of that type.
50
52
  */
51
53
  const getAllControllers = <T extends Gtk.EventController>(
52
- widget: Gtk.Widget,
54
+ widget: Gtk.Accessible,
53
55
  controllerType: ControllerConstructor<T>,
54
56
  ): T[] => {
55
57
  const controllers = queryAllControllers(widget, controllerType);
@@ -69,7 +71,7 @@ const getAllControllers = <T extends Gtk.EventController>(
69
71
  * @throws When the widget has no controller of that type.
70
72
  */
71
73
  const getController = <T extends Gtk.EventController>(
72
- widget: Gtk.Widget,
74
+ widget: Gtk.Accessible,
73
75
  controllerType: ControllerConstructor<T>,
74
76
  ): T => {
75
77
  const [controller] = getAllControllers(widget, controllerType);
@@ -82,9 +84,10 @@ const getController = <T extends Gtk.EventController>(
82
84
  };
83
85
 
84
86
  const getOrCreateControllers = <T extends Gtk.EventController>(
85
- widget: Gtk.Widget,
87
+ target: Gtk.Accessible,
86
88
  controllerType: ControllerConstructor<T>,
87
89
  ): T[] => {
90
+ const widget = requireWidget(target);
88
91
  const existing = queryAllControllers(widget, controllerType);
89
92
 
90
93
  if (existing.length > 0) {