gdharness 1.3.35 → 1.3.36

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 (30) hide show
  1. package/build/cli.js +226 -57
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +17 -1
  3. package/build/godot/addons/gdharness_editor/reading.gd +10 -2
  4. package/build/godot/addons/gdharness_editor/scene_file.gd +12 -6
  5. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +57 -0
  6. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +16 -0
  7. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +81 -4
  8. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +3 -3
  9. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +4 -2
  10. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +59 -0
  11. package/build/godot/addons/gdharness_editor/tools/script_paths.gd +148 -0
  12. package/build/godot/addons/gdharness_editor/tools/script_paths.gd.uid +1 -0
  13. package/build/godot/addons/gdharness_runtime/reading.gd +10 -2
  14. package/build/godot/addons/gdharness_runtime/runtime_hover.gd +70 -0
  15. package/build/godot/addons/gdharness_runtime/runtime_input.gd +223 -248
  16. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +128 -5
  17. package/build/godot/addons/gdharness_runtime/runtime_screen.gd +107 -0
  18. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +15 -1
  19. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +8 -0
  20. package/build/godot/addons/gdharness_runtime/runtime_words.gd +22 -0
  21. package/build/godot/operations/animation_reads.gd +17 -1
  22. package/build/godot/operations/classdb_queries.gd +16 -0
  23. package/build/godot/operations/dependencies.gd +46 -4
  24. package/build/godot/operations/editor_tools.gd +5 -5
  25. package/build/godot/operations/gdscript_authoring.gd +7 -3
  26. package/build/godot/operations/input_actions.gd +105 -0
  27. package/build/godot/operations/project_diagnostics.gd +4 -0
  28. package/build/godot/operations/project_validation.gd +15 -0
  29. package/build/godot/operations/reading.gd +10 -2
  30. package/package.json +1 -1
@@ -46,6 +46,9 @@ var _pointer_placed: bool = false
46
46
  var _pointer_at_usec: int = 0
47
47
  var _arrival_seconds: float = 1.0 / 60.0
48
48
 
49
+ ## The buttons a press pushed into a window other than the game's holds, which Input never hears of.
50
+ var _pushed_held: int = 0
51
+
49
52
 
50
53
  func _init(host: Node, values: GdhValues) -> void:
51
54
  _host = host
@@ -94,18 +97,61 @@ static func _hover_noted(answer: Dictionary, supplied: bool) -> Dictionary:
94
97
  "This window has had no pointer enter from the operating system, and with subwindow"
95
98
  + " embedding off Godot hovers nothing in a native window until it has, so a Button there"
96
99
  + " takes no press. The runtime gave the controls under the pointer the hover the engine"
97
- + " would have: their mouse_entered and mouse_exited, and the hover a Button presses on."
100
+ + " would have: their mouse_entered and mouse_exited, and the hover a Button presses on,"
101
+ + " and told the window the pointer is in it, so a motion reaches the control under it."
98
102
  + " Viewport.gui_get_hovered_control() still answers null in this window. Said once a run;"
99
103
  + " hover_supplied marks every answer it applies to."
100
104
  )
101
105
  return answer
102
106
 
103
107
 
104
- ## Hovers what is under [param position] in the game's window, where the pointer events Input is
105
- ## given go, when the engine cannot: see runtime_hover.gd.
106
- func _follow_in_the_window(position: Vector2) -> bool:
107
- var root: Window = _host.get_tree().root
108
- return _hover.follow(root, root.get_final_transform().affine_inverse() * position)
108
+ ## Hovers what is under [param position], in [param viewport]'s pixels, when the engine cannot: see
109
+ ## runtime_hover.gd.
110
+ func _follow_in(viewport: Viewport, position: Vector2) -> bool:
111
+ return _hover.follow(viewport, viewport.get_final_transform().affine_inverse() * position)
112
+
113
+
114
+ ## Sends one event of a mouse_click, mouse_motion or mouse_drag into [param viewport]: through Input
115
+ ## for the game's own window, and pushed into any other, as a click sends one, since Input reaches
116
+ ## only the game's window. Input keeps the buttons held from the events it is given, so the ones a
117
+ ## pushed press holds are kept here for the motions and the release after it.
118
+ func _send(viewport: Viewport, event: InputEventMouse) -> void:
119
+ if viewport == _host.get_tree().root:
120
+ Input.parse_input_event(event)
121
+ return
122
+ # A press can close the window it went to, and its release then has nowhere to go.
123
+ if not is_instance_valid(viewport):
124
+ return
125
+ if event is InputEventMouseButton:
126
+ _pushed_held = event.button_mask
127
+ viewport.push_input(event)
128
+
129
+
130
+ ## The buttons held in [param viewport] before the next event sent there.
131
+ func _held_in(viewport: Viewport) -> int:
132
+ return Input.get_mouse_button_mask() if viewport == _host.get_tree().root else _pushed_held
133
+
134
+
135
+ ## The viewport whose GUI says what is under the pointer: the named node's own, which for a node in
136
+ ## an embedded window is not [param viewport], the one the events went into. Read before the events,
137
+ ## since they may free the node.
138
+ static func _own_of(aim: Dictionary, viewport: Viewport) -> Viewport:
139
+ if not aim.has("node"):
140
+ return viewport
141
+ var named: Node = aim["node"]
142
+ return named.get_viewport()
143
+
144
+
145
+ ## The path of the control under the pointer in [param own], or null, read once the events are
146
+ ## handled: a motion and a drag answered nothing of where they left the pointer, so a hover could be
147
+ ## checked only by what the game did about it.
148
+ func _hovered_path(own: Viewport) -> Variant:
149
+ if not is_instance_valid(own):
150
+ return null
151
+ var hovered: Control = _hover.hovered_in(own)
152
+ if hovered == null:
153
+ return null
154
+ return str(hovered.get_path())
109
155
 
110
156
 
111
157
  ## The movement a pointer arriving at [param position] carries, which is the distance from where
@@ -266,10 +312,17 @@ func inject_key(params: Dictionary) -> Dictionary:
266
312
  ## `pressed` is how a caller asks for one half, the way a drag is made: true holds the button down,
267
313
  ## false lets go of one being held.
268
314
  func inject_mouse_click(params: Dictionary) -> Dictionary:
315
+ var aimed: Variant = GdhMouse.aim_of(params, _host.get_tree().root)
316
+ if aimed is String:
317
+ return {"type": "error", "message": aimed}
318
+ var aim: Dictionary = aimed
269
319
  var point: Variant = GdhMouse.read_point(params, "x", "y")
270
320
  if point is String:
271
321
  return {"type": "error", "message": point}
272
- var position: Vector2 = point
322
+ var local: Vector2 = point
323
+ var placed: Dictionary = GdhMouse.carried(aim, local, _host.get_tree().root)
324
+ var viewport: Viewport = placed["viewport"]
325
+ var position: Vector2 = placed["position"]
273
326
  var button: int = GdhMouse.button_of(params.get("button", MOUSE_BUTTON_LEFT))
274
327
  if button < 0:
275
328
  return GdhMouse.no_such_button(params.get("button"))
@@ -294,50 +347,49 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
294
347
  # A button pressed somewhere puts the pointer there: the motion after a held press measures
295
348
  # its drag from the press, which is where a real pointer would be.
296
349
  var _moved: Vector2 = _arrive(position)
297
- var supplied: bool = _follow_in_the_window(position)
350
+ var supplied: bool = _follow_in(viewport, position)
351
+ var at: Array[Vector2] = [position]
352
+ var where: Dictionary = GdhMouse.where(aim, viewport, at)
353
+ var own: Viewport = _own_of(aim, viewport)
354
+ var pressing: Dictionary = _hover.press_of(aim, viewport, position) if held else {}
298
355
  # A double click's first click goes whole before it, as [method _click_button] sends one.
299
356
  if double and held and not notch:
300
- Input.parse_input_event(GdhMouse.button_event(position, button, true, false))
357
+ _send(viewport, GdhMouse.button_event(position, button, true, false, _held_in(viewport)))
301
358
  await _host.get_tree().process_frame
302
- Input.parse_input_event(GdhMouse.button_event(position, button, false, false))
359
+ _send(viewport, GdhMouse.button_event(position, button, false, false, _held_in(viewport)))
303
360
  await _host.get_tree().process_frame
304
- var press: InputEventMouseButton = GdhMouse.button_event(position, button, whole or held, double)
305
- Input.parse_input_event(press)
361
+ var press: InputEventMouseButton = GdhMouse.button_event(
362
+ position, button, whole or held, double, _held_in(viewport)
363
+ )
364
+ _send(viewport, press)
306
365
  var lasted: Variant = null
307
366
  if notch:
308
- Input.parse_input_event(GdhMouse.button_event(position, button, false, false))
367
+ _send(viewport, GdhMouse.button_event(position, button, false, false, _held_in(viewport)))
309
368
  elif whole:
310
369
  # A frame between the halves at least, as inject_action has, so a control that acts on the
311
370
  # press and one that acts on the release each get a frame to do it in.
312
371
  lasted = await GdhValues.hold(_host.get_tree(), frames)
313
- Input.parse_input_event(GdhMouse.button_event(position, button, false, false))
372
+ _send(viewport, GdhMouse.button_event(position, button, false, false, _held_in(viewport)))
314
373
  # Input hands the events on at the next frame, so the control under the pointer is read after it.
315
374
  await _host.get_tree().process_frame
316
- var hovered: Control = _hover.hovered_in(_host.get_viewport())
317
- var hovered_path: Variant = null
318
- if hovered != null:
319
- hovered_path = str(hovered.get_path())
375
+ var hovered_path: Variant = _hovered_path(own)
320
376
 
321
- var at: Array[Vector2] = [position]
322
- var said: Dictionary = _pointer_noted(
323
- _hover_noted(
324
- {
325
- "type": "input_injected",
326
- "input_type": "mouse_click",
327
- "held_frames": GdhValues.held_answer(whole and not notch, frames),
328
- "held_for": lasted,
329
- "position": [position.x, position.y],
330
- "button": button,
331
- "hovered": hovered_path,
332
- "actions": GdhValues.actions_of(press),
333
- "pressed": held and not whole,
334
- "whole": whole,
335
- "double": double
336
- },
337
- supplied
338
- )
339
- )
340
- return GdhMouse.outside_noted(said, _host.get_viewport(), at)
377
+ var answer: Dictionary = {
378
+ "type": "input_injected",
379
+ "input_type": "mouse_click",
380
+ "held_frames": GdhValues.held_answer(whole and not notch, frames),
381
+ "held_for": lasted,
382
+ "position": [position.x, position.y],
383
+ "button": button,
384
+ "hovered": hovered_path,
385
+ "actions": GdhValues.actions_of(press),
386
+ "pressed": held and not whole,
387
+ "whole": whole,
388
+ "double": double
389
+ }
390
+ answer.merge(where)
391
+ answer.merge(pressing)
392
+ return _pointer_noted(_hover_noted(answer, supplied))
341
393
 
342
394
 
343
395
  ## Moves the pointer to a position, with the movement the event carries taken from where the
@@ -349,32 +401,40 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
349
401
  ## would have carried. A relative the caller gives is kept, since a game may want to read a motion
350
402
  ## the position does not show.
351
403
  func inject_mouse_motion(params: Dictionary) -> Dictionary:
404
+ var aimed: Variant = GdhMouse.aim_of(params, _host.get_tree().root)
405
+ if aimed is String:
406
+ return {"type": "error", "message": aimed}
407
+ var aim: Dictionary = aimed
352
408
  var point: Variant = GdhMouse.read_point(params, "x", "y")
353
409
  if point is String:
354
410
  return {"type": "error", "message": point}
355
- var position: Vector2 = point
411
+ var local: Vector2 = point
412
+ var placed: Dictionary = GdhMouse.carried(aim, local, _host.get_tree().root)
413
+ var viewport: Viewport = placed["viewport"]
414
+ var position: Vector2 = placed["position"]
356
415
  var relative: Vector2 = _arrive(position)
357
416
  if params.has("relativeX") or params.has("relativeY"):
358
417
  relative = Vector2(
359
418
  GdhRead.as_float(params.get("relativeX", 0.0)), GdhRead.as_float(params.get("relativeY", 0.0))
360
419
  )
361
420
 
362
- var supplied: bool = _follow_in_the_window(position)
363
- Input.parse_input_event(_motion(position, relative))
364
-
421
+ var supplied: bool = _follow_in(viewport, position)
365
422
  var at: Array[Vector2] = [position]
366
- var said: Dictionary = _pointer_noted(
367
- _hover_noted(
368
- {
369
- "type": "input_injected",
370
- "input_type": "mouse_motion",
371
- "position": [position.x, position.y],
372
- "relative": [relative.x, relative.y]
373
- },
374
- supplied
375
- )
376
- )
377
- return GdhMouse.outside_noted(said, _host.get_viewport(), at)
423
+ var where: Dictionary = GdhMouse.where(aim, viewport, at)
424
+ var own: Viewport = _own_of(aim, viewport)
425
+ _send(viewport, _motion(position, relative, _held_in(viewport)))
426
+ # Input hands the motion on at the next frame, and the hover follows it there.
427
+ await _host.get_tree().process_frame
428
+
429
+ var answer: Dictionary = {
430
+ "type": "input_injected",
431
+ "input_type": "mouse_motion",
432
+ "position": [position.x, position.y],
433
+ "relative": [relative.x, relative.y],
434
+ "hovered": _hovered_path(own),
435
+ }
436
+ answer.merge(where)
437
+ return _pointer_noted(_hover_noted(answer, supplied))
378
438
 
379
439
 
380
440
  ## Drags with a mouse button from one point to another, through any points between, as a hand does:
@@ -387,10 +447,21 @@ func inject_mouse_motion(params: Dictionary) -> Dictionary:
387
447
  ## Each motion goes in a frame of its own because the engine merges the motions it is given within
388
448
  ## one frame into a single event, as it does a real mouse's, and a game counting motions counts one.
389
449
  func inject_mouse_drag(params: Dictionary) -> Dictionary:
450
+ var aimed: Variant = GdhMouse.aim_of(params, _host.get_tree().root)
451
+ if aimed is String:
452
+ return {"type": "error", "message": aimed}
453
+ var aim: Dictionary = aimed
390
454
  var stroke: Variant = GdhMouse.stroke_of(params)
391
455
  if stroke is String:
392
456
  return {"type": "error", "message": stroke}
393
- var points: Array[Vector2] = stroke
457
+ # Carried once, before the press: a hand drags across the screen, not along a node that moves.
458
+ var named: Array[Vector2] = stroke
459
+ var points: Array[Vector2] = []
460
+ var viewport: Viewport = _host.get_tree().root
461
+ for local: Vector2 in named:
462
+ var placed: Dictionary = GdhMouse.carried(aim, local, _host.get_tree().root)
463
+ viewport = placed["viewport"]
464
+ points.append(placed["position"])
394
465
  var button: int = GdhMouse.button_of(params.get("button", MOUSE_BUTTON_LEFT))
395
466
  if button < 0:
396
467
  return GdhMouse.no_such_button(params.get("button"))
@@ -410,85 +481,53 @@ func inject_mouse_drag(params: Dictionary) -> Dictionary:
410
481
  var start: Vector2 = points[0]
411
482
  var end: Vector2 = points[points.size() - 1]
412
483
  var _moved: Vector2 = _arrive(start)
413
- var supplied: bool = _follow_in_the_window(start)
414
- Input.parse_input_event(GdhMouse.button_event(start, button, true, false))
484
+ var supplied: bool = _follow_in(viewport, start)
485
+ var where: Dictionary = GdhMouse.where(aim, viewport, points)
486
+ var hovered_in: Viewport = _own_of(aim, viewport)
487
+ var pressing: Dictionary = _hover.press_of(aim, viewport, start)
488
+ _send(viewport, GdhMouse.button_event(start, button, true, false, _held_in(viewport)))
415
489
  var travelled: Vector2 = Vector2.ZERO
416
- for step: int in range(1, frames + 1):
490
+ var sent: int = 0
491
+ for step: int in range(1, frames + 2):
417
492
  await _host.get_tree().process_frame
493
+ # A window other than the game's can be closed by what the stroke moves, and nothing is left
494
+ # for the rest of it to go to.
495
+ if not is_instance_valid(viewport):
496
+ return {
497
+ "type": "error",
498
+ "message":
499
+ (
500
+ (
501
+ "%s was freed after the press and %d of %d motions, so the rest of the stroke"
502
+ + " and the release went nowhere"
503
+ )
504
+ % [where["window"], sent, frames]
505
+ )
506
+ }
507
+ if step > frames:
508
+ break
418
509
  var at: Vector2 = GdhMouse.along(points, lengths, float(step) / frames)
419
510
  var relative: Vector2 = _arrive(at)
420
511
  travelled += relative
421
- supplied = _follow_in_the_window(at) or supplied
422
- Input.parse_input_event(_motion(at, relative))
512
+ supplied = _follow_in(viewport, at) or supplied
513
+ _send(viewport, _motion(at, relative, _held_in(viewport)))
514
+ sent += 1
515
+ _send(viewport, GdhMouse.button_event(end, button, false, false, _held_in(viewport)))
423
516
  await _host.get_tree().process_frame
424
- Input.parse_input_event(GdhMouse.button_event(end, button, false, false))
425
- await _host.get_tree().process_frame
426
-
427
- var said: Dictionary = _pointer_noted(
428
- _hover_noted(
429
- {
430
- "type": "input_injected",
431
- "input_type": "mouse_drag",
432
- "from": [start.x, start.y],
433
- "to": [end.x, end.y],
434
- "button": button,
435
- "motions": frames,
436
- "relative_total": [travelled.x, travelled.y],
437
- },
438
- supplied
439
- )
440
- )
441
- return GdhMouse.outside_noted(said, _host.get_viewport(), points)
442
-
443
-
444
- ## Whether [param centre] is somewhere a click can reach it: inside the viewport, and inside every
445
- ## ScrollContainer between the control and the root, each of which clips what it holds.
446
- static func _in_sight(control: Control, viewport: Viewport, centre: Vector2) -> bool:
447
- return viewport.get_visible_rect().has_point(centre) and _clipped_by(control, centre) == null
448
517
 
449
-
450
- ## The ScrollContainer between [param control] and its viewport that clips [param centre] away from
451
- ## it, or null when none does. Not past the viewport: one holding a dialog is in another space.
452
- static func _clipped_by(control: Control, centre: Vector2) -> ScrollContainer:
453
- var walking: Node = control.get_parent()
454
- while walking != null and not (walking is Viewport):
455
- var holder: ScrollContainer = walking as ScrollContainer
456
- # Carried into the same space the centre is in, which is the canvas rather than the
457
- # container's own: the two are only the same while nothing above it is transformed.
458
- if holder != null:
459
- var seen: Rect2 = holder.get_global_transform_with_canvas() * Rect2(Vector2.ZERO, holder.size)
460
- if not seen.has_point(centre):
461
- return holder
462
- walking = walking.get_parent()
463
- return null
464
-
465
-
466
- ## Scrolls whatever is holding [param control] until it is on screen, and answers whether the view
467
- ## moved.
468
- ##
469
- ## Innermost container first and outwards, a frame apart. A container moves what it holds on the
470
- ## next layout pass rather than inside the call, and the next one out works out where the control is
471
- ## from where it is drawn, so asked in the same frame it scrolled to where the control had been.
472
- ## Moved means a scroll value changed: a container asked to show what it already shows moves
473
- ## nothing, and answering true for it said the view had moved when it had not. Only the containers in
474
- ## the control's own viewport: one holding a dialog scrolls the page behind it, not the dialog.
475
- func _scroll_into_view(control: Control) -> bool:
476
- var moved: bool = false
477
- var walking: Variant = control.get_parent()
478
- while walking != null and not (walking is Viewport):
479
- var step: Node = walking
480
- var holder: ScrollContainer = step as ScrollContainer
481
- if holder != null:
482
- var before: Vector2i = Vector2i(holder.scroll_horizontal, holder.scroll_vertical)
483
- holder.ensure_control_visible(control)
484
- if Vector2i(holder.scroll_horizontal, holder.scroll_vertical) != before:
485
- moved = true
486
- await _host.get_tree().process_frame
487
- # A panel that rebuilds itself can go in the frame this waited.
488
- if not is_instance_valid(control) or not is_instance_valid(holder):
489
- return moved
490
- walking = step.get_parent()
491
- return moved
518
+ var answer: Dictionary = {
519
+ "type": "input_injected",
520
+ "input_type": "mouse_drag",
521
+ "from": [start.x, start.y],
522
+ "to": [end.x, end.y],
523
+ "button": button,
524
+ "motions": frames,
525
+ "relative_total": [travelled.x, travelled.y],
526
+ "hovered": _hovered_path(hovered_in),
527
+ }
528
+ answer.merge(where)
529
+ answer.merge(pressing)
530
+ return _pointer_noted(_hover_noted(answer, supplied))
492
531
 
493
532
 
494
533
  ## A whole click on a Control: the pointer moves onto it, the button goes down, a frame passes,
@@ -525,6 +564,9 @@ func click(params: Dictionary) -> Dictionary:
525
564
  if standing.has("message"):
526
565
  return standing
527
566
  var node: Node = standing["node"]
567
+ var named_point: Variant = GdhMouse.point_in(params, node, node_path, item != null)
568
+ if named_point is String:
569
+ return {"type": "error", "message": named_point}
528
570
  if node is Node3D:
529
571
  var spatial: Node3D = node
530
572
  return await _click_in_the_world(node_path, spatial, params)
@@ -534,7 +576,7 @@ func click(params: Dictionary) -> Dictionary:
534
576
  "message": "%s is a %s, not a Control or a Node3D" % [node_path, node.get_class()]
535
577
  }
536
578
  var control: Control = node
537
- var unmoved: String = _takes_no_click(control, node_path)
579
+ var unmoved: String = GdhMouse.takes_no_click(control, node_path, _host.get_tree().paused)
538
580
  if not unmoved.is_empty():
539
581
  return {"type": "error", "message": unmoved}
540
582
 
@@ -544,6 +586,9 @@ func click(params: Dictionary) -> Dictionary:
544
586
  var aim: Vector2 = control.size * 0.5
545
587
  var at_centre: bool = true
546
588
  var item_scrolled: bool = false
589
+ if named_point != null:
590
+ aim = named_point
591
+ at_centre = false
547
592
  if item != null:
548
593
  var found_by_words: Dictionary = found
549
594
  var placed_item: Dictionary = await GdhItems.in_view(
@@ -555,7 +600,7 @@ func click(params: Dictionary) -> Dictionary:
555
600
  at_centre = false
556
601
  item_scrolled = placed_item["scrolled"]
557
602
  var rich: RichTextLabel = control as RichTextLabel
558
- if found != null and rich != null:
603
+ if found != null and rich != null and named_point == null:
559
604
  var placed: Dictionary = GdhTargets.point_on_words(rich, wanted)
560
605
  var found_here: Dictionary = found
561
606
  if placed.has("point"):
@@ -583,8 +628,8 @@ func click(params: Dictionary) -> Dictionary:
583
628
  # the click went to that instead and said so. Measured on a hall whose staff panel had been
584
629
  # scrolled past: the button was at y 69 and its container started at y 166.
585
630
  var scrolled: bool = item_scrolled
586
- if not _in_sight(control, own, local):
587
- scrolled = await _scroll_into_view(control) or item_scrolled
631
+ if not GdhScreen.in_sight(control, own, local):
632
+ scrolled = await GdhScreen.scroll_into_view(control, _host.get_tree()) or item_scrolled
588
633
  var left: String = GdhValues.afterwards(control)
589
634
  if left != "in_tree":
590
635
  return {
@@ -600,7 +645,7 @@ func click(params: Dictionary) -> Dictionary:
600
645
  # A control taller or wider than what shows it has its centre out of sight at every scroll, and
601
646
  # a player clicks the part they can see. Refused, it was called a control nothing could click.
602
647
  var on_part: bool = false
603
- if at_centre and not _in_sight(control, own, local):
648
+ if at_centre and not GdhScreen.in_sight(control, own, local):
604
649
  var part: Variant = GdhScreen.shown_part(control)
605
650
  if part is Rect2:
606
651
  var seen: Rect2 = part
@@ -619,7 +664,8 @@ func click(params: Dictionary) -> Dictionary:
619
664
  # that silently reached nothing: outside the viewport it is drawn in, outside the one that shows
620
665
  # that on the screen, or clipped away by a ScrollContainer that would not scroll it into view.
621
666
  # Each is said of the viewport or container at fault, with its own rect and the point in it.
622
- var out_there: String = _out_of_reach(control, node_path, local, reached, scrolled)
667
+ var aimed: String = "its centre" if named_point == null else "the point %s clicked" % [named_point]
668
+ var out_there: String = _out_of_reach(control, node_path, local, reached, scrolled, aimed)
623
669
  if not out_there.is_empty():
624
670
  return {"type": "error", "message": out_there}
625
671
  var button: int = GdhMouse.button_of(params.get("button", MOUSE_BUTTON_LEFT))
@@ -647,8 +693,21 @@ func click(params: Dictionary) -> Dictionary:
647
693
  if hovered != null:
648
694
  hovered_path = str(hovered.get_path())
649
695
  var landed: bool = hovered == control or (hovered != null and control.is_ancestor_of(hovered))
650
- # Asked only of a miss, before the press restacks windows: hovered missed a window over it.
651
- var cover: Node = null if landed else GdhTargets.drawn_over(control)
696
+ # Why a miss missed, said before the press: the press can free the control and what covers it, and
697
+ # a click on a pointer spot that rebuilt its room errored reading the freed spot's filter and
698
+ # answered nothing. The cover is asked before the press restacks windows too: hovered missed a
699
+ # window over it.
700
+ var why_not_landed: String = ""
701
+ # A Label and a TextureRect ignore the pointer unless told otherwise, so the click goes to what is
702
+ # under them, and landed false with that control as hovered said nothing of why.
703
+ if not landed and control.mouse_filter == Control.MOUSE_FILTER_IGNORE:
704
+ why_not_landed = (
705
+ "%s's mouse_filter is ignore, so the pointer passes through it to what is under it" % node_path
706
+ )
707
+ elif not landed:
708
+ var cover: Node = GdhTargets.drawn_over(control)
709
+ if cover != null:
710
+ why_not_landed = "%s is drawn over it at the point clicked" % cover.get_path()
652
711
 
653
712
  # The link a rich label says was pressed, which is its own word for what the press reached.
654
713
  var links: Array = []
@@ -664,12 +723,21 @@ func click(params: Dictionary) -> Dictionary:
664
723
  var was_pressed: Variant = null
665
724
  if toggle != null and toggle.toggle_mode:
666
725
  was_pressed = toggle.button_pressed
726
+ # Whether the button's own pressed signal came, which landing does not say either: a click that
727
+ # landed on Options answered nothing of whether the game heard it, and the panel it opened had
728
+ # closed again by the next read.
729
+ var fired: Array[bool] = [false]
730
+ var heard_press: Callable = func() -> void: fired[0] = true
731
+ if toggle != null:
732
+ var _hearing: int = toggle.pressed.connect(heard_press)
667
733
  var press: InputEventMouseButton = await _click_button(viewport, position, button, double)
668
734
  # The release is what a button acts on, and a queue_free it causes lands at the end of
669
735
  # this frame; the frame passes so the answer describes the control as the click left it.
670
736
  await _host.get_tree().process_frame
671
737
  if rich != null and is_instance_valid(rich) and rich.meta_clicked.is_connected(heard):
672
738
  rich.meta_clicked.disconnect(heard)
739
+ if toggle != null and is_instance_valid(toggle) and toggle.pressed.is_connected(heard_press):
740
+ toggle.pressed.disconnect(heard_press)
673
741
 
674
742
  # What became of the control: still in the tree, taken out of it, or freed. A button that
675
743
  # opened another screen is the second or the third, and the caller wants to hear that
@@ -690,14 +758,8 @@ func click(params: Dictionary) -> Dictionary:
690
758
  }
691
759
  if on_part:
692
760
  answer["pressed_on"] = "the middle of the part on screen, since no scroll brings its centre into view"
693
- # A Label and a TextureRect ignore the pointer unless told otherwise, so the click goes to what is
694
- # under them, and landed false with that control as hovered said nothing of why.
695
- if not landed and control.mouse_filter == Control.MOUSE_FILTER_IGNORE:
696
- answer["why_not_landed"] = (
697
- "%s's mouse_filter is ignore, so the pointer passes through it to what is under it" % node_path
698
- )
699
- elif cover != null:
700
- answer["why_not_landed"] = "%s is drawn over it at the point clicked" % cover.get_path()
761
+ if not why_not_landed.is_empty():
762
+ answer["why_not_landed"] = why_not_landed
701
763
  if found != null:
702
764
  answer["found"] = found
703
765
  if was_pressed != null:
@@ -705,6 +767,8 @@ func click(params: Dictionary) -> Dictionary:
705
767
  if is_instance_valid(toggle):
706
768
  now_pressed = toggle.button_pressed
707
769
  answer["pressed"] = {"before": was_pressed, "after": now_pressed}
770
+ if toggle != null:
771
+ answer["pressed_signal"] = fired[0]
708
772
  # Whether a link was pressed apart from what it carries: a bare [url] reports an empty meta,
709
773
  # and given as the meta alone that press read exactly like one that reached no link.
710
774
  if on_rich_text:
@@ -714,50 +778,15 @@ func click(params: Dictionary) -> Dictionary:
714
778
  return _pointer_noted(_hover_noted(answer, supplied))
715
779
 
716
780
 
717
- ## Why [param control] would take no click a player gave it, or "" when it would.
718
- ##
719
- ## The last two answered landed, since the pointer reaches the control, while the press did nothing,
720
- ## measured on 4.7.2: a disabled button keeps its mouse filter and ignores the press, and a control
721
- ## that does not process while the game is paused is hovered and never handed the event.
722
- ## A control whose pointer a `mouse_behavior_recursive` above it has turned off is passed through to
723
- ## what is underneath, which answered `landed: false` with nothing saying why.
724
- func _takes_no_click(control: Control, node_path: String) -> String:
725
- if not control.is_visible_in_tree():
726
- return "%s is not visible%s, so nothing can click it" % [node_path, GdhTargets.why_hidden(control)]
727
- var button: BaseButton = control as BaseButton
728
- if button != null and button.disabled:
729
- return "%s is disabled, so clicking it presses nothing" % node_path
730
- var turned_off: Control = GdhTargets.pointer_turned_off_by(control)
731
- if turned_off != null:
732
- var on: String = "itself" if turned_off == control else str(turned_off.get_path())
733
- return (
734
- (
735
- "%s takes no pointer input, since mouse_behavior_recursive is disabled on %s,"
736
- + " so clicking it presses nothing"
737
- )
738
- % [node_path, on]
739
- )
740
- if not control.can_process():
741
- var why: String = GdhTargets.why_it_does_not_process(control, node_path)
742
- var over: Node = GdhTargets.drawn_over(control)
743
- var cover: String = "" if over == null else "; %s is drawn over it" % over.get_path()
744
- if _host.get_tree().paused:
745
- return (
746
- "the game is paused and %s does not process while it is (%s), so clicking it does nothing%s"
747
- % [node_path, why, cover]
748
- )
749
- return "%s does not process (%s), so clicking it does nothing%s" % [node_path, why, cover]
750
- return ""
751
-
752
-
753
- ## Why a click at [param control]'s centre would reach nothing, or "" when it would reach it: the
754
- ## centre outside the viewport it is drawn in, at [param local]; outside the viewport showing that
755
- ## one on the screen, which [param reached] holds with the centre there; or clipped away by a
756
- ## ScrollContainer holding it. A refusal names the viewport or container at fault and the point in
757
- ## that one's own space, since naming another printed a point inside the rect it was said to be
758
- ## outside. The note on a game with no window goes only on the viewport that is the window's.
781
+ ## Why a click at [param control]'s centre, or at the point [param aimed] names, would reach nothing,
782
+ ## or "" when it would reach it: the point outside the viewport it is drawn in, at [param local];
783
+ ## outside the viewport showing that one on the screen, which [param reached] holds with the point
784
+ ## there; or clipped away by a ScrollContainer holding it. A refusal names the viewport or container
785
+ ## at fault and the point in that one's own space, since naming another printed a point inside the
786
+ ## rect it was said to be outside. The note on a game with no window goes only on the viewport that
787
+ ## is the window's.
759
788
  func _out_of_reach(
760
- control: Control, node_path: String, local: Vector2, reached: Dictionary, scrolled: bool
789
+ control: Control, node_path: String, local: Vector2, reached: Dictionary, scrolled: bool, aimed: String
761
790
  ) -> String:
762
791
  var after: String = (
763
792
  ". It was scrolled as far as what holds it goes and is still out there" if scrolled else ""
@@ -769,16 +798,17 @@ func _out_of_reach(
769
798
  continue
770
799
  var which: String = "" if viewport == _host.get_tree().root else " of %s" % viewport.get_path()
771
800
  return (
772
- "%s has its centre at %s, outside the viewport%s %s, so nothing can click it%s%s"
773
- % [node_path, point, which, viewport.get_visible_rect(), after, _no_window_note(viewport)]
801
+ "%s has %s at %s, outside the viewport%s %s, so nothing can click it%s%s"
802
+ % [node_path, aimed, point, which, viewport.get_visible_rect(), after, _no_window_note(viewport)]
774
803
  )
775
- var holder: ScrollContainer = _clipped_by(control, local)
804
+ var holder: ScrollContainer = GdhScreen.clipped_by(control, local)
776
805
  if holder == null:
777
806
  return ""
778
807
  return (
779
- "%s has its centre at %s, outside the part of %s that shows it, so nothing can click it%s"
808
+ "%s has %s at %s, outside the part of %s that shows it, so nothing can click it%s"
780
809
  % [
781
810
  node_path,
811
+ aimed,
782
812
  local,
783
813
  holder.get_path(),
784
814
  after if scrolled else ". It could not be scrolled into view",
@@ -898,7 +928,7 @@ func _click_in_the_world(node_path: String, item: Node3D, params: Dictionary) ->
898
928
  var supplied: bool = _hover.follow(arriving, reached_point)
899
929
  # Read before the press, for the reason the Control click reads it: what the caller needs to
900
930
  # know is whether a panel is sitting over the room, and the press is what would change it.
901
- var through: Dictionary = _through_the_interface(out)
931
+ var through: Dictionary = GdhScreen.through_the_interface(out, _hover)
902
932
  var hovered_path: Variant = through["hovered"]
903
933
 
904
934
  var _press: InputEventMouseButton = await _click_button(arriving, position, button, double)
@@ -924,61 +954,6 @@ func _click_in_the_world(node_path: String, item: Node3D, params: Dictionary) ->
924
954
  )
925
955
 
926
956
 
927
- ## Whether a pointer on its way out through [param out], the steps [method GdhScreen.steps_out]
928
- ## answers, gets past the interface in every viewport, as {"landed", "hovered"}: the path of the
929
- ## control that took it, or of the one under it that let it by, or null.
930
- ##
931
- ## In the viewport the node is drawn in, a control under the pointer takes it unless it and every
932
- ## control holding it let the pointer pass. In each viewport outside that one, the pointer has to be
933
- ## over what shows the one inside: the SubViewportContainer, with nothing drawn over it, or inside
934
- ## the embedded window. Read in the first viewport alone, a click beside a container that is smaller
935
- ## than what it shows, or under a panel drawn over the container, answered landed.
936
- func _through_the_interface(out: Array[Dictionary]) -> Dictionary:
937
- var first_under: Variant = null
938
- for step: Dictionary in out:
939
- var viewport: Viewport = step["viewport"]
940
- var through: Variant = step["through"]
941
- var hovered: Control = _hover.hovered_in(viewport)
942
- if through is Window:
943
- var window: Window = through
944
- var point: Vector2 = step["point"]
945
- if not Rect2(window.position, window.size).has_point(point):
946
- return {"landed": false, "hovered": _path_or_null(hovered)}
947
- continue
948
- if through is SubViewportContainer:
949
- if hovered != through:
950
- return {"landed": false, "hovered": _path_or_null(hovered)}
951
- continue
952
- if hovered != null and not _passes_on(hovered):
953
- return {"landed": false, "hovered": str(hovered.get_path())}
954
- if hovered != null and first_under == null:
955
- first_under = str(hovered.get_path())
956
- return {"landed": true, "hovered": first_under}
957
-
958
-
959
- ## Whether a pointer over [param control] goes on past the interface: a control that lets it pass
960
- ## hands it to the one holding it, and one that stops it takes it, up to a control drawn on its own
961
- ## or something that is not a control. Measured on 4.7.2: a HUD over the room letting the pointer
962
- ## pass answered not landed while the game's own input took the press. A script on a control that
963
- ## lets it pass can still take the event for itself, which nothing outside it can see.
964
- static func _passes_on(control: Control) -> bool:
965
- var walk: Node = control
966
- while walk is Control:
967
- var at: Control = walk
968
- if at.get_mouse_filter_with_override() == Control.MOUSE_FILTER_STOP:
969
- return false
970
- if at.top_level:
971
- break
972
- walk = at.get_parent()
973
- return true
974
-
975
-
976
- static func _path_or_null(node: Node) -> Variant:
977
- if node == null:
978
- return null
979
- return str(node.get_path())
980
-
981
-
982
957
  ## The buttons a real pointer event carries are the ones held as it happens, and Input keeps that
983
958
  ## from every button event it is given, injected ones included, so a motion between a held click
984
959
  ## and its release is a drag to a control that reads the mask rather than remembering the click.
@@ -987,7 +962,7 @@ static func _path_or_null(node: Node) -> Variant:
987
962
  ## menu ignores a motion with none, so that one opening under a resting pointer lights nothing: a
988
963
  ## motion over an open drop-down's item answered as injected and the menu highlighted nothing. A
989
964
  ## motion that goes nowhere still has none, as a resting pointer does.
990
- func _motion(position: Vector2, relative: Vector2) -> InputEventMouseMotion:
965
+ func _motion(position: Vector2, relative: Vector2, held: int = -1) -> InputEventMouseMotion:
991
966
  var event: InputEventMouseMotion = InputEventMouseMotion.new()
992
967
  event.position = position
993
968
  event.global_position = position
@@ -995,5 +970,5 @@ func _motion(position: Vector2, relative: Vector2) -> InputEventMouseMotion:
995
970
  event.screen_relative = relative
996
971
  event.velocity = relative / _arrival_seconds
997
972
  event.screen_velocity = event.velocity
998
- event.button_mask = Input.get_mouse_button_mask()
973
+ event.button_mask = Input.get_mouse_button_mask() if held < 0 else held
999
974
  return event