netfishing/ui/controller_focus_navigation.gd

140 lines
4.1 KiB
GDScript3
Raw Normal View History

class_name ControllerFocusNavigation
extends RefCounted
const PERPENDICULAR_WEIGHT: float = 2.5
const MINIMUM_FORWARD_DISTANCE: float = 0.5
const MAXIMUM_DIRECTION_SLOPE: float = 1.0
static func configure_spatial_neighbors(controls: Array) -> void:
var candidates: Array[Control] = []
for item: Variant in controls:
var control := item as Control
if not is_focusable(control):
if control != null:
_clear_neighbors(control)
continue
candidates.append(control)
var traversal_order: Array[Control] = candidates.duplicate()
traversal_order.sort_custom(_sort_by_position)
for control: Control in candidates:
_enable_ancestor_scroll_follow(control)
control.focus_neighbor_left = _neighbor_path(
control, candidates, Vector2.LEFT
)
control.focus_neighbor_right = _neighbor_path(
control, candidates, Vector2.RIGHT
)
control.focus_neighbor_top = _neighbor_path(
control, candidates, Vector2.UP
)
control.focus_neighbor_bottom = _neighbor_path(
control, candidates, Vector2.DOWN
)
var traversal_index: int = traversal_order.find(control)
control.focus_previous = control.get_path_to(
traversal_order[wrapi(
traversal_index - 1,
0,
traversal_order.size(),
)]
)
control.focus_next = control.get_path_to(
traversal_order[wrapi(
traversal_index + 1,
0,
traversal_order.size(),
)]
)
static func configure_traversal(controls: Array) -> void:
var candidates: Array[Control] = []
for item: Variant in controls:
var control := item as Control
if is_focusable(control):
candidates.append(control)
elif control != null:
control.focus_previous = NodePath()
control.focus_next = NodePath()
if candidates.is_empty():
return
for index: int in candidates.size():
var control: Control = candidates[index]
_enable_ancestor_scroll_follow(control)
control.focus_previous = control.get_path_to(
candidates[wrapi(index - 1, 0, candidates.size())]
)
control.focus_next = control.get_path_to(
candidates[wrapi(index + 1, 0, candidates.size())]
)
static func is_focusable(control: Control) -> bool:
if (
control == null
or not control.is_visible_in_tree()
or control.focus_mode not in [Control.FOCUS_CLICK, Control.FOCUS_ALL]
):
return false
var button := control as BaseButton
return button == null or not button.disabled
static func _clear_neighbors(control: Control) -> void:
control.focus_neighbor_left = NodePath()
control.focus_neighbor_right = NodePath()
control.focus_neighbor_top = NodePath()
control.focus_neighbor_bottom = NodePath()
control.focus_previous = NodePath()
control.focus_next = NodePath()
static func _enable_ancestor_scroll_follow(control: Control) -> void:
var ancestor: Node = control.get_parent()
while ancestor != null:
var scroll := ancestor as ScrollContainer
if scroll != null:
scroll.follow_focus = true
ancestor = ancestor.get_parent()
static func _sort_by_position(first: Control, second: Control) -> bool:
var first_center: Vector2 = first.get_global_rect().get_center()
var second_center: Vector2 = second.get_global_rect().get_center()
if not is_equal_approx(first_center.y, second_center.y):
return first_center.y < second_center.y
return first_center.x < second_center.x
static func _neighbor_path(
origin: Control,
candidates: Array[Control],
direction: Vector2,
) -> NodePath:
var origin_center: Vector2 = origin.get_global_rect().get_center()
var best: Control = null
var best_score: float = INF
for candidate: Control in candidates:
if candidate == origin or not is_focusable(candidate):
continue
var delta: Vector2 = (
candidate.get_global_rect().get_center() - origin_center
)
var forward_distance: float = delta.dot(direction)
if forward_distance <= MINIMUM_FORWARD_DISTANCE:
continue
var perpendicular_distance: float = absf(delta.cross(direction))
if perpendicular_distance > forward_distance * MAXIMUM_DIRECTION_SLOPE:
continue
var score: float = (
forward_distance
+ perpendicular_distance * PERPENDICULAR_WEIGHT
)
if score < best_score:
best = candidate
best_score = score
if best == null:
return origin.get_path_to(origin)
return origin.get_path_to(best)