Add basic accessibility features demo. (#1238)

This commit is contained in:
bruvzg
2025-10-02 04:41:42 +03:00
committed by GitHub
parent dc4abff7d0
commit bdc33d1568
14 changed files with 490 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
# UI Accessibility
This is a demo of UI accessibility in Godot.
Demo shows basic UI accessibility features and making custom nodes accessible.
Language: GDScript
Renderer: Compatibility
## Screenshots
![Screenshot](screenshots/ui_access.webp)

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#e0e0e0" d="m8 1a1 1 0 0 0 -1 1v5h-2c-1.108 0-2 .892-2 2v1h10v-1c0-1.108-.892-2-2-2h-2v-5a1 1 0 0 0 -1-1zm-5 10v4l10-1v-3z"/></svg>

After

Width:  |  Height:  |  Size: 207 B

View File

@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ni6t4bukhhtq"
path="res://.godot/imported/clear.svg-8b4ed1a66e68ce7d40ebb58ca5ec90ed.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://clear.svg"
dest_files=["res://.godot/imported/clear.svg-8b4ed1a66e68ce7d40ebb58ca5ec90ed.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@@ -0,0 +1,9 @@
extends Control
func _ready() -> void:
$LineEditName.grab_focus() # Accessible UI should always have keyboard focus, since it is a main way of interacting with UI.
func _on_button_set_pressed() -> void:
$Panel/LabelRegion.text = $LineEditLiveReg.text # Set live region text.

View File

@@ -0,0 +1 @@
uid://7d87a4p1kd2u

View File

@@ -0,0 +1,207 @@
[gd_scene load_steps=4 format=3 uid="uid://c50snxy83byec"]
[ext_resource type="Script" uid="uid://7d87a4p1kd2u" path="res://controls.gd" id="1_gpdjo"]
[ext_resource type="Texture2D" uid="uid://ni6t4bukhhtq" path="res://clear.svg" id="2_qo8cm"]
[ext_resource type="Script" uid="uid://v3wb3vhx0ang" path="res://custom_control.gd" id="3_xwvqn"]
[node name="Accessibility" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_gpdjo")
[node name="SectionNames" type="Label" parent="."]
layout_mode = 0
offset_left = 40.0
offset_top = 16.0
offset_right = 249.0
offset_bottom = 50.0
theme_override_font_sizes/font_size = 24
text = "Accessible names:"
[node name="NamesDescription1" type="Label" parent="."]
layout_mode = 0
offset_left = 376.0
offset_top = 56.0
offset_right = 1088.0
offset_bottom = 88.0
theme_override_colors/font_color = Color(1, 1, 1, 0.5882353)
text = "Readable name for control can be specified using \"accessible_name\" property."
autowrap_mode = 2
[node name="LabelName" type="Label" parent="."]
layout_mode = 0
offset_left = 64.0
offset_top = 56.0
offset_right = 115.0
offset_bottom = 88.0
text = "Name:"
[node name="LineEditName" type="LineEdit" parent="."]
layout_mode = 0
offset_left = 152.0
offset_top = 56.0
offset_right = 336.0
offset_bottom = 87.0
accessibility_name = "Name"
[node name="NamesDescription2" type="Label" parent="."]
layout_mode = 0
offset_left = 376.0
offset_top = 104.0
offset_right = 1088.0
offset_bottom = 136.0
theme_override_colors/font_color = Color(1, 1, 1, 0.5882353)
text = "Or by setting \"placeholder\" in case of input controls like \"LineEdit\"."
autowrap_mode = 2
[node name="NamesDescription4" type="Label" parent="."]
layout_mode = 0
offset_left = 376.0
offset_top = 152.0
offset_right = 1088.0
offset_bottom = 184.0
theme_override_colors/font_color = Color(1, 1, 1, 0.5882353)
text = "Node without name still works with screen reader, but it's harder to determine its purpose."
autowrap_mode = 2
[node name="LabelType" type="Label" parent="."]
layout_mode = 0
offset_left = 64.0
offset_top = 104.0
offset_right = 115.0
offset_bottom = 136.0
text = "Type:"
[node name="LineEditType" type="LineEdit" parent="."]
layout_mode = 0
offset_left = 152.0
offset_top = 104.0
offset_right = 336.0
offset_bottom = 135.0
placeholder_text = "Type"
[node name="NamesDescription3" type="Label" parent="."]
layout_mode = 0
offset_left = 376.0
offset_top = 200.0
offset_right = 1088.0
offset_bottom = 232.0
theme_override_colors/font_color = Color(1, 1, 1, 0.5882353)
text = "Controls with a static \"text\" property, like \"Button\" will use it as the default readable name."
autowrap_mode = 2
[node name="LabelBroken" type="Label" parent="."]
layout_mode = 0
offset_left = 64.0
offset_top = 152.0
offset_right = 115.0
offset_bottom = 184.0
text = "No name:"
[node name="LineEdit" type="LineEdit" parent="."]
layout_mode = 0
offset_left = 152.0
offset_top = 152.0
offset_right = 336.0
offset_bottom = 183.0
[node name="ButtonOK" type="Button" parent="."]
layout_mode = 0
offset_left = 64.0
offset_top = 200.0
offset_right = 127.0
offset_bottom = 231.0
text = "OK"
[node name="ButtonClear" type="Button" parent="."]
layout_mode = 0
offset_left = 136.0
offset_top = 200.0
offset_right = 160.0
offset_bottom = 231.0
accessibility_name = "Clear"
icon = ExtResource("2_qo8cm")
[node name="LabelRegion" type="Label" parent="."]
layout_mode = 0
offset_left = 40.0
offset_top = 264.0
offset_right = 173.0
offset_bottom = 298.0
theme_override_font_sizes/font_size = 24
text = "Live region:"
[node name="Panel" type="Panel" parent="."]
layout_mode = 0
offset_left = 64.0
offset_top = 304.0
offset_right = 336.0
offset_bottom = 352.0
[node name="LabelRegion" type="Label" parent="Panel"]
layout_mode = 0
offset_left = 8.0
offset_top = 8.0
offset_right = 264.0
offset_bottom = 40.0
accessibility_live = 2
[node name="LineEditLiveReg" type="LineEdit" parent="."]
layout_mode = 0
offset_left = 64.0
offset_top = 368.0
offset_right = 336.0
offset_bottom = 399.0
placeholder_text = "Live region text"
[node name="LiveDescription1" type="Label" parent="."]
layout_mode = 0
offset_left = 376.0
offset_top = 304.0
offset_right = 1088.0
offset_bottom = 379.0
theme_override_colors/font_color = Color(1, 1, 1, 0.5882353)
text = "Live regions (\"accessibility live\" property) can be used to announce changes to unfocused elements, depending on the setting screen reader will speak changes immediately or whenever the user is idle."
autowrap_mode = 2
[node name="LiveDescription2" type="Label" parent="."]
layout_mode = 0
offset_left = 376.0
offset_top = 528.0
offset_right = 1088.0
offset_bottom = 603.0
theme_override_colors/font_color = Color(1, 1, 1, 0.5882353)
text = "Accessibility support for a node can be fully customized by implementing \"ACCESSIBILITY_*\" notifications handler and associated virtual methods, see script attached to \"CustomControl\" node."
autowrap_mode = 2
[node name="ButtonSet" type="Button" parent="."]
layout_mode = 0
offset_left = 64.0
offset_top = 416.0
offset_right = 127.0
offset_bottom = 447.0
text = "Set"
[node name="LabelCustom" type="Label" parent="."]
layout_mode = 0
offset_left = 40.0
offset_top = 488.0
offset_right = 249.0
offset_bottom = 522.0
theme_override_font_sizes/font_size = 24
text = "Custom control:"
[node name="CustomControl" type="Control" parent="."]
anchors_preset = 0
offset_left = 64.0
offset_top = 528.0
offset_right = 364.0
offset_bottom = 568.0
focus_mode = 2
script = ExtResource("3_xwvqn")
[connection signal="pressed" from="ButtonSet" to="." method="_on_button_set_pressed"]

View File

@@ -0,0 +1 @@
uid://bttvjfh81iwsa

View File

@@ -0,0 +1,126 @@
extends Control
var item_aes: Array[RID] = [RID(), RID(), RID()]
var item_names: Array[String] = ["Item 1", "Item 2", "Item 3"]
var item_values: Array[int] = [0, 0, 0]
var item_rects: Array[Rect2] = [Rect2(0, 0, 40, 40), Rect2(40, 0, 40, 40), Rect2(80, 0, 40, 40)]
var selected: int = 0
# Input:
func _gui_input(event: InputEvent) -> void:
if event.is_action_pressed(&"ui_left"):
selected = (selected - 1) % item_aes.size()
queue_redraw()
queue_accessibility_update() # Request node accessibility information update. Similar to "queue_redraw" for drawing.
accept_event()
if event.is_action_pressed(&"ui_right"):
selected = (selected + 1) % item_aes.size()
queue_redraw()
queue_accessibility_update()
accept_event()
if event.is_action_pressed(&"ui_up"):
item_values[selected] = clampi(item_values[selected] - 1, -100, 100)
queue_redraw()
queue_accessibility_update()
accept_event()
if event.is_action_pressed(&"ui_down"):
item_values[selected] = clampi(item_values[selected] + 1, -100, 100)
queue_redraw()
queue_accessibility_update()
accept_event()
# Accessibility actions and focus callback:
func _accessibility_action_dec(_data: Variant, item: int) -> void:
# Numeric value decrement, "data" is not used for this action.
item_values[item] = clampi(item_values[item] - 1, -100, 100)
queue_redraw()
queue_accessibility_update()
func _accessibility_action_inc(_data: Variant, item: int) -> void:
# Numeric value increment, "data" is not used for this action.
item_values[item] = clampi(item_values[item] + 1, -100, 100)
queue_redraw()
queue_accessibility_update()
func _accessibility_action_set_num_value(data: Variant, item: int) -> void:
# Numeric value set, "data" is a new value.
item_values[item] = clampi(data, -100, 100)
queue_redraw()
queue_accessibility_update()
func _get_focused_accessibility_element() -> RID:
# Return focused sub-element, if no sub-element is focused, return base element (value returned by "get_accessibility_element") instead.
return item_aes[selected]
# Notifications handler:
func _notification(what: int) -> void:
if what == NOTIFICATION_ACCESSIBILITY_INVALIDATE:
# Accessibility cleanup:
#
# Called when existing main element is destroyed.
# Note: since item sub-elements are children of the main element, there's no need to destroy them manually. But we should keep track when handles are invalidated.
for i in range(item_aes.size()):
item_aes[i] = RID()
if what == NOTIFICATION_ACCESSIBILITY_UPDATE:
# Accessibility update handler:
#
# This function acts as an alternative "draw" for the screen reader, and provides information about this node.
var ae: RID = get_accessibility_element() # Get handle to the accessibilty element, accessibilty element is created and destroyed automatically.
# Set role of the element.
DisplayServer.accessibility_update_set_role(ae, DisplayServer.ROLE_LIST_BOX)
# Set other properties.
DisplayServer.accessibility_update_set_list_item_count(ae, item_aes.size())
DisplayServer.accessibility_update_set_name(ae, "List")
for i in range(item_aes.size()):
# Create a new sub-element for the item if it doesn't exist.
if not item_aes[i].is_valid():
item_aes[i] = DisplayServer.accessibility_create_sub_element(ae, DisplayServer.ROLE_LIST_BOX_OPTION)
# Sub-element properties.
DisplayServer.accessibility_update_set_list_item_index(item_aes[i], i)
DisplayServer.accessibility_update_set_list_item_selected(item_aes[i], selected == i)
DisplayServer.accessibility_update_set_name(item_aes[i], item_names[i]) # Readable name.
DisplayServer.accessibility_update_set_value(item_aes[i], str(item_values[i])) # Readable value.
# Numeric value info for the actions.
DisplayServer.accessibility_update_set_num_value(item_aes[i], item_values[i]);
DisplayServer.accessibility_update_set_num_range(item_aes[i], -100.0, 100.0);
DisplayServer.accessibility_update_set_num_step(item_aes[i], 1.0)
# Sub-element bounding box, relative to the parent element.
DisplayServer.accessibility_update_set_bounds(item_aes[i], item_rects[i])
# Set supported actions for the item, actions can be invoked directly by the screen-reader (e.g, via global keyboard shortcuts).
DisplayServer.accessibility_update_add_action(item_aes[i], DisplayServer.ACTION_DECREMENT, _accessibility_action_dec.bind(i))
DisplayServer.accessibility_update_add_action(item_aes[i], DisplayServer.ACTION_INCREMENT, _accessibility_action_inc.bind(i))
DisplayServer.accessibility_update_add_action(item_aes[i], DisplayServer.ACTION_SET_VALUE, _accessibility_action_set_num_value.bind(i))
if what == NOTIFICATION_FOCUS_ENTER or what == NOTIFICATION_FOCUS_EXIT:
queue_redraw()
# Draw:
func _draw() -> void:
# Draw, provided for convenience and NOT required for screen-reader support.
for i in range(item_aes.size()):
draw_rect(item_rects[selected], Color(0.8, 0.8, 0.8, 0.5), false, 1.0)
draw_string(get_theme_font("font"), item_rects[i].position + Vector2(0, 30), str(item_values[i]), HORIZONTAL_ALIGNMENT_CENTER, 40.0)
if has_focus():
draw_rect(Rect2(Vector2(), get_size()), Color(0, 0, 1, 0.5), false, 3.0)
draw_rect(item_rects[selected], Color(0, 1, 0, 0.5), false, 2.0)

View File

@@ -0,0 +1 @@
uid://v3wb3vhx0ang

View File

@@ -0,0 +1 @@
<svg height="128" width="128" xmlns="http://www.w3.org/2000/svg"><rect x="2" y="2" width="124" height="124" rx="14" fill="#363d52" stroke="#212532" stroke-width="4"/><g transform="scale(.101) translate(122 122)"><g fill="#fff"><path d="M105 673v33q407 354 814 0v-33z"/><path d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 814 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H446l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z" fill="#478cbf"/><path d="M483 600c0 34 58 34 58 0v-86c0-34-58-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></g></svg>

After

Width:  |  Height:  |  Size: 949 B

View File

@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dxw1ukquxg8iq"
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://icon.svg"
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@@ -0,0 +1,44 @@
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters
config_version=5
[application]
config/name="UI Accessibility"
config/description="This is a demo of UI accessibility in Godot.
Demo shows basic UI accessibility features and making custom nodes accessible."
config/tags=PackedStringArray("accessibility", "demo", "gui", "input", "official")
run/main_scene="uid://c50snxy83byec"
config/features=PackedStringArray("4.5")
run/max_fps=120
config/icon="uid://dxw1ukquxg8iq"
[debug]
gdscript/warnings/untyped_declaration=1
[display]
window/stretch/mode="canvas_items"
window/stretch/aspect="expand"
window/vsync/vsync_mode=0
[input]
toggle_msdf_font={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":83,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
[rendering]
renderer/rendering_method="gl_compatibility"
renderer/rendering_method.mobile="gl_compatibility"

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB