2026-08-08 14:23:28 -04:00
|
|
|
extends SceneTree
|
|
|
|
|
|
|
|
|
|
const FocusPresentationType = preload(
|
|
|
|
|
"res://ui/controller_focus_presentation.gd"
|
|
|
|
|
)
|
2026-08-16 16:40:10 -04:00
|
|
|
const FishBatchSelectionType = preload(
|
|
|
|
|
"res://ui/fish_batch_selection.gd"
|
|
|
|
|
)
|
2026-08-08 14:23:28 -04:00
|
|
|
|
|
|
|
|
var _failures: Array[String] = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _initialize() -> void:
|
|
|
|
|
_run.call_deferred()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _run() -> void:
|
2026-08-16 16:40:10 -04:00
|
|
|
_validate_controller_cooler_focus()
|
2026-08-15 23:59:56 -04:00
|
|
|
root.size = Vector2i(1280, 720)
|
2026-08-08 14:23:28 -04:00
|
|
|
var stage := Control.new()
|
|
|
|
|
stage.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
|
|
|
root.add_child(stage)
|
|
|
|
|
var presentation := FocusPresentationType.new()
|
|
|
|
|
stage.add_child(presentation)
|
|
|
|
|
var standard_button := Button.new()
|
|
|
|
|
standard_button.text = "standard"
|
2026-08-15 23:59:56 -04:00
|
|
|
standard_button.position = Vector2(80.0, 60.0)
|
2026-08-08 14:23:28 -04:00
|
|
|
standard_button.custom_minimum_size = Vector2(120.0, 60.0)
|
2026-08-15 23:59:56 -04:00
|
|
|
var normal_style := _button_style(Color("123f4e"))
|
2026-08-16 16:40:10 -04:00
|
|
|
var old_hover_style := _button_style(Color("58c6d4"))
|
2026-08-15 23:59:56 -04:00
|
|
|
var old_focus_style := _button_style(Color("238697"))
|
|
|
|
|
standard_button.add_theme_stylebox_override("normal", normal_style)
|
2026-08-16 16:40:10 -04:00
|
|
|
standard_button.add_theme_stylebox_override("hover", old_hover_style)
|
2026-08-15 23:59:56 -04:00
|
|
|
standard_button.add_theme_stylebox_override("focus", old_focus_style)
|
2026-08-08 14:23:28 -04:00
|
|
|
stage.add_child(standard_button)
|
2026-08-15 23:59:56 -04:00
|
|
|
var second_button := Button.new()
|
|
|
|
|
second_button.text = "second"
|
|
|
|
|
second_button.position = Vector2(260.0, 60.0)
|
|
|
|
|
second_button.custom_minimum_size = Vector2(120.0, 60.0)
|
|
|
|
|
# Legacy opt-outs must not prevent the new universal cursor from appearing.
|
|
|
|
|
second_button.set_meta(&"controller_focus_inversion_disabled", true)
|
|
|
|
|
stage.add_child(second_button)
|
2026-08-08 14:23:28 -04:00
|
|
|
await process_frame
|
|
|
|
|
|
2026-08-19 10:02:28 -04:00
|
|
|
standard_button.grab_focus()
|
|
|
|
|
await process_frame
|
|
|
|
|
_expect(
|
|
|
|
|
root.gui_get_focus_owner() == null,
|
|
|
|
|
"a menu's programmatic first focus is visible before navigation input",
|
|
|
|
|
)
|
|
|
|
|
_expect(
|
|
|
|
|
presentation.get("_neutral_focus_seed") == standard_button,
|
|
|
|
|
"the neutral menu focus seed did not remember the first action",
|
|
|
|
|
)
|
|
|
|
|
var first_navigation := InputEventJoypadButton.new()
|
|
|
|
|
first_navigation.button_index = JOY_BUTTON_DPAD_DOWN
|
|
|
|
|
first_navigation.pressed = true
|
|
|
|
|
presentation._input(first_navigation)
|
|
|
|
|
await process_frame
|
|
|
|
|
_expect(
|
|
|
|
|
root.gui_get_focus_owner() == standard_button,
|
|
|
|
|
"the first directional input did not activate the neutral focus seed",
|
|
|
|
|
)
|
|
|
|
|
|
2026-08-08 14:23:28 -04:00
|
|
|
var controller_event := InputEventJoypadButton.new()
|
|
|
|
|
controller_event.button_index = JOY_BUTTON_A
|
|
|
|
|
controller_event.pressed = true
|
|
|
|
|
presentation._input(controller_event)
|
|
|
|
|
await process_frame
|
2026-08-15 23:59:56 -04:00
|
|
|
var focus_arrow := presentation.get("_focus_arrow") as TextureRect
|
2026-08-17 21:43:58 -04:00
|
|
|
var focus_shadow := presentation.get("_focus_arrow_shadow") as TextureRect
|
2026-08-15 23:59:56 -04:00
|
|
|
_expect(focus_arrow != null, "controller focus arrow was not created")
|
2026-08-17 21:43:58 -04:00
|
|
|
_expect(focus_shadow != null, "controller focus shadow was not created")
|
2026-08-15 23:59:56 -04:00
|
|
|
_expect(focus_arrow.visible, "controller focus arrow is not visible")
|
2026-08-17 21:43:58 -04:00
|
|
|
_expect(focus_shadow.visible, "controller focus shadow is not visible")
|
2026-08-08 14:23:28 -04:00
|
|
|
_expect(
|
2026-08-15 23:59:56 -04:00
|
|
|
focus_arrow.texture == FocusPresentationType.FOCUS_ARROW_TEXTURE,
|
|
|
|
|
"controller focus arrow uses the wrong artwork",
|
2026-08-08 14:23:28 -04:00
|
|
|
)
|
2026-08-17 21:43:58 -04:00
|
|
|
_expect(
|
|
|
|
|
focus_shadow.texture == focus_arrow.texture,
|
|
|
|
|
"controller focus shadow does not follow the cursor silhouette",
|
|
|
|
|
)
|
|
|
|
|
_expect(
|
|
|
|
|
focus_shadow.position.is_equal_approx(
|
|
|
|
|
focus_arrow.position
|
|
|
|
|
+ FocusPresentationType.FOCUS_ARROW_SHADOW_OFFSET
|
|
|
|
|
),
|
|
|
|
|
"controller focus shadow uses the wrong offset",
|
|
|
|
|
)
|
2026-08-15 23:59:56 -04:00
|
|
|
_expect(
|
|
|
|
|
is_equal_approx(
|
|
|
|
|
focus_arrow.rotation_degrees,
|
|
|
|
|
FocusPresentationType.FOCUS_ARROW_ROTATION_DEGREES,
|
|
|
|
|
),
|
|
|
|
|
"controller focus arrow does not point toward five o'clock",
|
|
|
|
|
)
|
|
|
|
|
_expect(
|
|
|
|
|
standard_button.material == null,
|
|
|
|
|
"controller focus still applies material inversion",
|
|
|
|
|
)
|
|
|
|
|
_expect(
|
|
|
|
|
standard_button.get_theme_stylebox("focus") == normal_style,
|
|
|
|
|
"native focus highlighting was not neutralized",
|
|
|
|
|
)
|
|
|
|
|
_expect(
|
|
|
|
|
_arrow_marks_top_left(focus_arrow, standard_button.get_global_rect()),
|
|
|
|
|
"controller focus arrow is not in the control's top-left corner",
|
|
|
|
|
)
|
|
|
|
|
stage.scale = Vector2.ONE * 0.5
|
|
|
|
|
await process_frame
|
|
|
|
|
_expect(
|
|
|
|
|
focus_arrow.size.is_equal_approx(
|
|
|
|
|
FocusPresentationType.FOCUS_ARROW_SIZE * 0.5
|
|
|
|
|
),
|
|
|
|
|
"controller focus arrow does not follow canonical UI stage scale",
|
|
|
|
|
)
|
|
|
|
|
_expect(
|
|
|
|
|
_arrow_marks_top_left(focus_arrow, standard_button.get_global_rect()),
|
|
|
|
|
"scaled controller focus arrow left its control's top-left corner",
|
|
|
|
|
)
|
|
|
|
|
stage.scale = Vector2.ONE
|
|
|
|
|
await process_frame
|
2026-08-16 16:40:10 -04:00
|
|
|
var hover_motion := InputEventMouseMotion.new()
|
|
|
|
|
hover_motion.position = standard_button.get_global_rect().get_center()
|
|
|
|
|
hover_motion.global_position = hover_motion.position
|
|
|
|
|
root.push_input(hover_motion, true)
|
|
|
|
|
await process_frame
|
|
|
|
|
_expect(
|
|
|
|
|
root.gui_get_hovered_control() == standard_button,
|
|
|
|
|
"the pointer did not reach the hover-suppression test button",
|
|
|
|
|
)
|
2026-08-19 10:02:28 -04:00
|
|
|
presentation._input(first_navigation)
|
2026-08-16 16:40:10 -04:00
|
|
|
presentation._input(controller_event)
|
|
|
|
|
await process_frame
|
|
|
|
|
_expect(
|
|
|
|
|
standard_button.get_theme_stylebox("hover") == normal_style,
|
|
|
|
|
"controller use leaves a stale mouse-hover highlight visible",
|
|
|
|
|
)
|
2026-08-15 23:59:56 -04:00
|
|
|
|
|
|
|
|
second_button.grab_focus()
|
|
|
|
|
await process_frame
|
|
|
|
|
_expect(
|
|
|
|
|
focus_arrow.visible,
|
|
|
|
|
"legacy inversion metadata incorrectly hides the focus arrow",
|
|
|
|
|
)
|
|
|
|
|
_expect(
|
|
|
|
|
_arrow_marks_top_left(focus_arrow, second_button.get_global_rect()),
|
|
|
|
|
"controller focus arrow did not follow the focused control",
|
|
|
|
|
)
|
2026-08-17 21:43:58 -04:00
|
|
|
var bubble_button := BubbleButton.new()
|
|
|
|
|
bubble_button.text = "bubble"
|
|
|
|
|
bubble_button.position = Vector2(440.0, 60.0)
|
|
|
|
|
bubble_button.size = Vector2(120.0, 120.0)
|
|
|
|
|
stage.add_child(bubble_button)
|
|
|
|
|
await process_frame
|
|
|
|
|
bubble_button.grab_focus()
|
|
|
|
|
await process_frame
|
|
|
|
|
_expect(
|
|
|
|
|
_arrow_overlaps_bubble_edge(focus_arrow, bubble_button),
|
|
|
|
|
"controller focus arrow does not meet the visible bubble edge",
|
|
|
|
|
)
|
2026-08-15 23:59:56 -04:00
|
|
|
|
2026-08-11 20:02:54 -04:00
|
|
|
var pointer_motion := InputEventMouseMotion.new()
|
|
|
|
|
pointer_motion.position = Vector2(4.0, 4.0)
|
|
|
|
|
presentation._input(pointer_motion)
|
|
|
|
|
await process_frame
|
|
|
|
|
_expect(
|
2026-08-15 23:59:56 -04:00
|
|
|
not focus_arrow.visible,
|
|
|
|
|
"mouse motion does not clear controller focus presentation",
|
|
|
|
|
)
|
2026-08-17 21:43:58 -04:00
|
|
|
_expect(
|
|
|
|
|
not focus_shadow.visible,
|
|
|
|
|
"mouse motion leaves the controller focus shadow visible",
|
|
|
|
|
)
|
2026-08-19 10:02:28 -04:00
|
|
|
_expect(
|
|
|
|
|
root.gui_get_focus_owner() == null,
|
|
|
|
|
"mouse motion leaves a stale menu action focused",
|
|
|
|
|
)
|
2026-08-15 23:59:56 -04:00
|
|
|
_expect(
|
|
|
|
|
standard_button.get_theme_stylebox("focus") == old_focus_style,
|
|
|
|
|
"native theme overrides were not restored after controller use",
|
2026-08-11 20:02:54 -04:00
|
|
|
)
|
2026-08-16 16:40:10 -04:00
|
|
|
_expect(
|
|
|
|
|
standard_button.get_theme_stylebox("hover") == old_hover_style,
|
|
|
|
|
"mouse hover styling was not restored after controller use",
|
|
|
|
|
)
|
2026-08-15 23:59:56 -04:00
|
|
|
var item_list := ItemList.new()
|
|
|
|
|
item_list.position = Vector2(80.0, 160.0)
|
|
|
|
|
item_list.size = Vector2(240.0, 150.0)
|
|
|
|
|
item_list.max_columns = 1
|
|
|
|
|
for index: int in 4:
|
|
|
|
|
item_list.add_item("list item %d" % index)
|
|
|
|
|
stage.add_child(item_list)
|
|
|
|
|
await process_frame
|
|
|
|
|
item_list.select(0)
|
|
|
|
|
item_list.grab_focus()
|
|
|
|
|
await process_frame
|
2026-08-19 10:02:28 -04:00
|
|
|
presentation._input(first_navigation)
|
|
|
|
|
presentation._input(controller_event)
|
|
|
|
|
await process_frame
|
2026-08-15 23:59:56 -04:00
|
|
|
var first_item_arrow_y: float = focus_arrow.position.y
|
|
|
|
|
item_list.select(2)
|
2026-08-08 14:23:28 -04:00
|
|
|
await process_frame
|
|
|
|
|
_expect(
|
2026-08-15 23:59:56 -04:00
|
|
|
focus_arrow.position.y > first_item_arrow_y,
|
|
|
|
|
"focus arrow does not follow the selected ItemList row",
|
|
|
|
|
)
|
|
|
|
|
_expect(
|
|
|
|
|
item_list.get_theme_stylebox("selected") is StyleBoxEmpty,
|
|
|
|
|
"ItemList still draws a selected-row cursor highlight",
|
2026-08-08 14:23:28 -04:00
|
|
|
)
|
|
|
|
|
_expect(
|
2026-08-15 23:59:56 -04:00
|
|
|
item_list.get_theme_color("font_selected_color")
|
|
|
|
|
== item_list.get_theme_color("font_color"),
|
|
|
|
|
"ItemList still recolors the selected-row cursor text",
|
2026-08-08 14:23:28 -04:00
|
|
|
)
|
|
|
|
|
|
2026-08-15 23:59:56 -04:00
|
|
|
var tree := Tree.new()
|
|
|
|
|
tree.position = Vector2(700.0, 160.0)
|
|
|
|
|
tree.size = Vector2(240.0, 150.0)
|
|
|
|
|
tree.hide_root = true
|
|
|
|
|
var tree_root: TreeItem = tree.create_item()
|
|
|
|
|
var first_tree_item: TreeItem = tree.create_item(tree_root)
|
|
|
|
|
first_tree_item.set_text(0, "first folder")
|
|
|
|
|
var second_tree_item: TreeItem = tree.create_item(tree_root)
|
|
|
|
|
second_tree_item.set_text(0, "second folder")
|
|
|
|
|
stage.add_child(tree)
|
2026-08-08 14:23:28 -04:00
|
|
|
await process_frame
|
2026-08-15 23:59:56 -04:00
|
|
|
first_tree_item.select(0)
|
|
|
|
|
tree.grab_focus()
|
|
|
|
|
await process_frame
|
|
|
|
|
var first_tree_arrow_y: float = focus_arrow.position.y
|
|
|
|
|
second_tree_item.select(0)
|
2026-08-08 14:23:28 -04:00
|
|
|
await process_frame
|
|
|
|
|
_expect(
|
2026-08-15 23:59:56 -04:00
|
|
|
focus_arrow.position.y > first_tree_arrow_y,
|
|
|
|
|
"focus arrow does not follow the selected Tree row",
|
2026-08-08 14:23:28 -04:00
|
|
|
)
|
|
|
|
|
|
2026-08-15 23:59:56 -04:00
|
|
|
var popup := PopupMenu.new()
|
|
|
|
|
for index: int in 5:
|
|
|
|
|
popup.add_item("popup item %d" % index)
|
|
|
|
|
root.add_child(popup)
|
|
|
|
|
popup.popup(Rect2i(500, 300, 0, 0))
|
|
|
|
|
popup.set_focused_item(0)
|
|
|
|
|
for _frame: int in 2:
|
|
|
|
|
await process_frame
|
|
|
|
|
_expect(
|
|
|
|
|
presentation.get("_focused_popup") == popup,
|
|
|
|
|
"focus arrow did not enter an open PopupMenu",
|
|
|
|
|
)
|
|
|
|
|
_expect(
|
|
|
|
|
presentation.get("_focus_layer").custom_viewport == popup,
|
|
|
|
|
"focus arrow is not rendered inside the PopupMenu viewport",
|
|
|
|
|
)
|
|
|
|
|
_expect(
|
|
|
|
|
popup.get_theme_stylebox("hover") is StyleBoxEmpty,
|
|
|
|
|
"PopupMenu still draws its native focus highlight",
|
|
|
|
|
)
|
|
|
|
|
_expect(
|
|
|
|
|
_arrow_marks_top_left(
|
|
|
|
|
focus_arrow,
|
|
|
|
|
presentation.call("_popup_focus_target_rect", popup) as Rect2,
|
|
|
|
|
),
|
|
|
|
|
"focus arrow does not mark the focused PopupMenu item",
|
|
|
|
|
)
|
|
|
|
|
var first_popup_arrow_y: float = focus_arrow.position.y
|
|
|
|
|
popup.set_focused_item(2)
|
|
|
|
|
await process_frame
|
|
|
|
|
_expect(
|
|
|
|
|
focus_arrow.position.y > first_popup_arrow_y,
|
|
|
|
|
"focus arrow does not follow PopupMenu item navigation",
|
|
|
|
|
)
|
|
|
|
|
popup.hide()
|
|
|
|
|
popup.queue_free()
|
|
|
|
|
await process_frame
|
|
|
|
|
item_list.grab_focus()
|
|
|
|
|
await process_frame
|
|
|
|
|
|
|
|
|
|
var dialog := FileDialog.new()
|
|
|
|
|
dialog.file_mode = FileDialog.FILE_MODE_OPEN_DIR
|
|
|
|
|
dialog.access = FileDialog.ACCESS_FILESYSTEM
|
|
|
|
|
dialog.size = Vector2i(720, 480)
|
|
|
|
|
root.add_child(dialog)
|
|
|
|
|
dialog.popup_centered()
|
|
|
|
|
for _frame: int in 2:
|
|
|
|
|
await process_frame
|
|
|
|
|
var dialog_cancel := dialog.get_cancel_button()
|
|
|
|
|
dialog_cancel.grab_focus()
|
|
|
|
|
for _frame: int in 2:
|
|
|
|
|
await process_frame
|
|
|
|
|
_expect(
|
|
|
|
|
focus_arrow.visible,
|
|
|
|
|
"focus arrow is hidden for an embedded dialog control",
|
|
|
|
|
)
|
|
|
|
|
_expect(
|
|
|
|
|
presentation.get("_focus_layer").custom_viewport
|
|
|
|
|
== dialog_cancel.get_viewport(),
|
|
|
|
|
"focus arrow is not rendered inside the dialog viewport",
|
|
|
|
|
)
|
|
|
|
|
_expect(
|
|
|
|
|
_arrow_marks_top_left(
|
|
|
|
|
focus_arrow,
|
|
|
|
|
presentation.call("_focus_target_rect", dialog_cancel) as Rect2,
|
|
|
|
|
),
|
|
|
|
|
"focus arrow does not follow an embedded dialog control",
|
|
|
|
|
)
|
|
|
|
|
dialog.hide()
|
|
|
|
|
dialog.queue_free()
|
|
|
|
|
await process_frame
|
|
|
|
|
item_list.grab_focus()
|
|
|
|
|
await process_frame
|
|
|
|
|
|
2026-08-15 19:20:42 -04:00
|
|
|
var scroll := ScrollContainer.new()
|
2026-08-15 23:59:56 -04:00
|
|
|
scroll.position = Vector2(420.0, 160.0)
|
2026-08-15 19:20:42 -04:00
|
|
|
scroll.size = Vector2(180.0, 90.0)
|
|
|
|
|
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
|
|
|
|
stage.add_child(scroll)
|
|
|
|
|
var list := VBoxContainer.new()
|
|
|
|
|
list.custom_minimum_size.x = 160.0
|
|
|
|
|
scroll.add_child(list)
|
|
|
|
|
var last_scroll_button: Button
|
|
|
|
|
for index: int in 8:
|
|
|
|
|
var scroll_button := Button.new()
|
|
|
|
|
scroll_button.text = "scroll option %d" % index
|
|
|
|
|
scroll_button.custom_minimum_size = Vector2(160.0, 40.0)
|
|
|
|
|
list.add_child(scroll_button)
|
|
|
|
|
last_scroll_button = scroll_button
|
|
|
|
|
await process_frame
|
|
|
|
|
last_scroll_button.grab_focus()
|
2026-08-15 23:59:56 -04:00
|
|
|
for _frame: int in 2:
|
|
|
|
|
await process_frame
|
2026-08-15 19:20:42 -04:00
|
|
|
_expect(
|
|
|
|
|
scroll.scroll_vertical > 0,
|
2026-08-15 23:59:56 -04:00
|
|
|
"controller focus does not reveal an off-screen selection",
|
2026-08-15 19:20:42 -04:00
|
|
|
)
|
|
|
|
|
_expect(
|
|
|
|
|
root.gui_get_focus_owner() == last_scroll_button,
|
2026-08-15 23:59:56 -04:00
|
|
|
"scroll following does not preserve the focused control",
|
|
|
|
|
)
|
|
|
|
|
_expect(
|
|
|
|
|
_arrow_marks_top_left(
|
|
|
|
|
focus_arrow,
|
|
|
|
|
last_scroll_button.get_global_rect(),
|
|
|
|
|
),
|
|
|
|
|
"focus arrow does not follow a scrolled control",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
last_scroll_button.focus_mode = Control.FOCUS_NONE
|
|
|
|
|
await process_frame
|
|
|
|
|
_expect(
|
|
|
|
|
not focus_arrow.visible,
|
|
|
|
|
"focus arrow remains after its control becomes unfocusable",
|
2026-08-15 19:20:42 -04:00
|
|
|
)
|
2026-08-19 10:02:28 -04:00
|
|
|
var event_focused_button := Button.new()
|
|
|
|
|
event_focused_button.text = "event focused"
|
|
|
|
|
event_focused_button.position = Vector2(700.0, 360.0)
|
|
|
|
|
event_focused_button.custom_minimum_size = Vector2(160.0, 50.0)
|
|
|
|
|
stage.add_child(event_focused_button)
|
|
|
|
|
await process_frame
|
|
|
|
|
presentation._input(first_navigation)
|
|
|
|
|
event_focused_button.grab_focus()
|
|
|
|
|
await process_frame
|
|
|
|
|
_expect(
|
|
|
|
|
root.gui_get_focus_owner() == event_focused_button,
|
|
|
|
|
(
|
|
|
|
|
"a menu focus established by the current directional event was "
|
|
|
|
|
+ "mistaken for an automatic default"
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
presentation.set_virtual_pointer_active(true)
|
|
|
|
|
await process_frame
|
|
|
|
|
_expect(
|
|
|
|
|
not focus_arrow.visible,
|
|
|
|
|
"focus arrow competes with active virtual-pointer presentation",
|
|
|
|
|
)
|
|
|
|
|
presentation._input(controller_event)
|
|
|
|
|
await process_frame
|
|
|
|
|
_expect(
|
|
|
|
|
not focus_arrow.visible,
|
|
|
|
|
"controller input stole presentation from the held virtual pointer",
|
|
|
|
|
)
|
|
|
|
|
presentation.set_virtual_pointer_active(false)
|
|
|
|
|
presentation._input(controller_event)
|
|
|
|
|
await process_frame
|
|
|
|
|
_expect(
|
|
|
|
|
focus_arrow.visible,
|
|
|
|
|
"controller focus presentation did not resume after virtual-pointer use",
|
|
|
|
|
)
|
2026-08-15 19:20:42 -04:00
|
|
|
|
2026-08-08 14:23:28 -04:00
|
|
|
stage.queue_free()
|
|
|
|
|
if _failures.is_empty():
|
|
|
|
|
print("Controller focus presentation validation: PASS")
|
|
|
|
|
quit(0)
|
|
|
|
|
return
|
|
|
|
|
for failure: String in _failures:
|
|
|
|
|
push_error(failure)
|
|
|
|
|
quit(1)
|
|
|
|
|
|
|
|
|
|
|
2026-08-16 16:40:10 -04:00
|
|
|
func _validate_controller_cooler_focus() -> void:
|
|
|
|
|
var selection := FishBatchSelectionType.new()
|
|
|
|
|
selection.set_visible_order([&"first", &"second"])
|
|
|
|
|
selection.select_only(&"first")
|
|
|
|
|
selection.focus_only(&"second")
|
|
|
|
|
_expect(
|
|
|
|
|
selection.get_focused_id() == &"second",
|
|
|
|
|
"cooler focus does not follow controller navigation",
|
|
|
|
|
)
|
|
|
|
|
_expect(
|
|
|
|
|
selection.get_selected_ids() == [&"first"],
|
|
|
|
|
"moving cooler focus unexpectedly changes the selected catch set",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-08-15 23:59:56 -04:00
|
|
|
func _button_style(color: Color) -> StyleBoxFlat:
|
|
|
|
|
var style := StyleBoxFlat.new()
|
|
|
|
|
style.bg_color = color
|
|
|
|
|
style.set_corner_radius_all(8)
|
|
|
|
|
return style
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _arrow_marks_top_left(arrow: Control, target: Rect2) -> bool:
|
|
|
|
|
var arrow_pivot: Vector2 = arrow.position + arrow.size * 0.5
|
|
|
|
|
var arrow_scale: Vector2 = arrow.size / FocusPresentationType.FOCUS_ARROW_SIZE
|
|
|
|
|
var tip_from_pivot: Vector2 = (
|
|
|
|
|
(FocusPresentationType.FOCUS_ARROW_TIP_UV - Vector2.ONE * 0.5)
|
|
|
|
|
* arrow.size
|
|
|
|
|
).rotated(deg_to_rad(
|
|
|
|
|
FocusPresentationType.FOCUS_ARROW_ROTATION_DEGREES
|
|
|
|
|
))
|
|
|
|
|
var actual_tip: Vector2 = arrow_pivot + tip_from_pivot
|
|
|
|
|
var expected_tip: Vector2 = (
|
|
|
|
|
target.position
|
|
|
|
|
+ FocusPresentationType.FOCUS_ARROW_TARGET_OVERLAP * arrow_scale
|
|
|
|
|
)
|
|
|
|
|
return actual_tip.distance_to(expected_tip) <= 0.5
|
|
|
|
|
|
|
|
|
|
|
2026-08-17 21:43:58 -04:00
|
|
|
func _arrow_overlaps_bubble_edge(
|
|
|
|
|
arrow: Control,
|
|
|
|
|
bubble: BubbleButton,
|
|
|
|
|
) -> bool:
|
|
|
|
|
var arrow_pivot: Vector2 = arrow.position + arrow.size * 0.5
|
|
|
|
|
var tip_from_pivot: Vector2 = (
|
|
|
|
|
(FocusPresentationType.FOCUS_ARROW_TIP_UV - Vector2.ONE * 0.5)
|
|
|
|
|
* arrow.size
|
|
|
|
|
).rotated(deg_to_rad(
|
|
|
|
|
FocusPresentationType.FOCUS_ARROW_ROTATION_DEGREES
|
|
|
|
|
))
|
|
|
|
|
var actual_tip: Vector2 = arrow_pivot + tip_from_pivot
|
|
|
|
|
var local_tip: Vector2 = (
|
|
|
|
|
bubble.get_global_transform_with_canvas().affine_inverse()
|
|
|
|
|
* actual_tip
|
|
|
|
|
)
|
|
|
|
|
var radius: Vector2 = bubble.size * 0.5
|
|
|
|
|
var normalized: Vector2 = (local_tip - radius) / radius
|
|
|
|
|
return normalized.length() >= 0.75 and normalized.length() <= 1.0
|
|
|
|
|
|
|
|
|
|
|
2026-08-08 14:23:28 -04:00
|
|
|
func _expect(condition: bool, message: String) -> void:
|
|
|
|
|
if not condition:
|
|
|
|
|
_failures.append(message)
|