feat: overhaul controller menu accessibility
This commit is contained in:
parent
a7837c3711
commit
4f9c85989b
46 changed files with 4091 additions and 396 deletions
|
|
@ -3,16 +3,22 @@ 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 control == null or not control.is_visible_in_tree():
|
||||
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
|
||||
)
|
||||
|
|
@ -25,6 +31,80 @@ static func configure_spatial_neighbors(controls: Array) -> void:
|
|||
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(
|
||||
|
|
@ -36,7 +116,7 @@ static func _neighbor_path(
|
|||
var best: Control = null
|
||||
var best_score: float = INF
|
||||
for candidate: Control in candidates:
|
||||
if candidate == origin or candidate.focus_mode == Control.FOCUS_NONE:
|
||||
if candidate == origin or not is_focusable(candidate):
|
||||
continue
|
||||
var delta: Vector2 = (
|
||||
candidate.get_global_rect().get_center() - origin_center
|
||||
|
|
@ -45,6 +125,8 @@ static func _neighbor_path(
|
|||
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
|
||||
|
|
@ -53,5 +135,5 @@ static func _neighbor_path(
|
|||
best = candidate
|
||||
best_score = score
|
||||
if best == null:
|
||||
return NodePath()
|
||||
return origin.get_path_to(origin)
|
||||
return origin.get_path_to(best)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue