// Copyright 2026 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
/// A11y runtime bridge: engine produces AccessKit updates; runtimes send actions.
///
/// This package owns the host bridge plumbing (imports + action polling).
/// UI extraction currently lives in `@ui` (so it can access UI-specific ECS).
///|
#ecs.resource
pub struct A11yState {
mut last_update : @moon_accesskit.TreeUpdate?
mut pending_actions : Array[@moon_accesskit.ActionRequest]
mut focused_node : @moon_accesskit.NodeId
}
///|
pub fn A11yState::default() -> A11yState {
A11yState::{
last_update: None,
pending_actions: [],
focused_node: @moon_accesskit.NodeId::root(),
}
}
///|
pub fn A11yState::push_action(
self : Ref[A11yState],
action : @moon_accesskit.ActionRequest,
) -> Unit {
self.val.pending_actions = self.val.pending_actions + [action]
}
///|
pub fn A11yState::drain_actions(
self : Ref[A11yState],
) -> Array[@moon_accesskit.ActionRequest] {
let out = self.val.pending_actions
self.val.pending_actions = []
out
}
///|
pub fn A11yState::set_update(
self : Ref[A11yState],
update : @moon_accesskit.TreeUpdate,
) -> Unit {
self.val.last_update = Some(update)
}
///|
pub fn A11yState::focused_node(self : A11yState) -> @moon_accesskit.NodeId {
self.focused_node
}
///|
pub fn A11yState::set_focused_node(
self : Ref[A11yState],
node : @moon_accesskit.NodeId,
) -> Unit {
self.val.focused_node = node
}
// -----------------------------------------------------------------------------
// Host bridge (guest -> host)
// -----------------------------------------------------------------------------
///|
fn host_a11y_begin_update(root_id : Int) -> Unit = "mgstudio_host" "a11y_begin_update"
///|
fn host_a11y_push_node(
node_id : Int,
parent_id : Int,
role_id : Int,
x : Float,
y : Float,
width : Float,
height : Float,
name_id : Int,
actions_mask : Int,
) -> Unit = "mgstudio_host" "a11y_push_node"
///|
fn host_a11y_end_update() -> Unit = "mgstudio_host" "a11y_end_update"
///|
fn host_a11y_actions_len() -> Int = "mgstudio_host" "a11y_actions_len"
///|
fn host_a11y_action_target(index : Int) -> Int = "mgstudio_host" "a11y_action_target"
///|
fn host_a11y_action_kind(index : Int) -> Int = "mgstudio_host" "a11y_action_kind"
///|
fn host_a11y_action_value_len(index : Int) -> Int = "mgstudio_host" "a11y_action_value_len"
///|
fn host_a11y_action_value_code_unit(index : Int, offset : Int) -> Int = "mgstudio_host" "a11y_action_value_code_unit"
///|
fn host_a11y_actions_clear() -> Unit = "mgstudio_host" "a11y_actions_clear"
///|
/// Role ids shared with runtimes.
pub let host_role_window : Int = 1
///|
pub let host_role_container : Int = 2
///|
pub let host_role_button : Int = 3
///|
pub let host_role_label : Int = 4
///|
pub let host_role_image : Int = 5
///|
/// Action mask bits shared with runtimes.
pub let host_action_focus_bit : Int = 1
///|
pub let host_action_default_bit : Int = 2
///|
pub let host_action_scroll_bit : Int = 4
///|
pub let host_action_set_value_bit : Int = 8
///|
/// Convert an entity into a stable `NodeId` for the host bridge.
pub fn node_id_for_entity(entity : @core.Entity) -> @moon_accesskit.NodeId {
// Pack (id, generation) into a small `u64` for JSON/JS safety, while
// reserving 0 for AccessKit's root node.
let packed = entity.id * 65536 + entity.generation + 1
@moon_accesskit.NodeId::from_u64(packed.to_uint64())
}
///|
fn host_action_value(index : Int) -> String? {
let len = host_a11y_action_value_len(index)
if len <= 0 {
return None
}
let chars : Array[Char] = []
for i in 0.. Unit {
host_a11y_begin_update(root_id)
}
///|
/// Push one node to the host (flat list with a parent id).
pub fn a11y_host_push_node(
node_id : Int,
parent_id : Int,
role_id : Int,
x : Float,
y : Float,
width : Float,
height : Float,
name_id : Int,
actions_mask : Int,
) -> Unit {
host_a11y_push_node(
node_id, parent_id, role_id, x, y, width, height, name_id, actions_mask,
)
}
///|
/// End a full-tree update on the host.
pub fn a11y_host_end_update() -> Unit {
host_a11y_end_update()
}
///|
fn host_a11y_apply_update(update_json_id : Int) -> Unit = "mgstudio_host" "a11y_apply_update"
///|
/// Apply an AccessKit `TreeUpdate` to the host runtime.
pub fn a11y_host_apply_update(update : @moon_accesskit.TreeUpdate) -> Unit {
let payload = update.to_json().stringify()
@core.with_host_string_id(payload, fn(payload_id) {
host_a11y_apply_update(payload_id)
})
}
///|
pub let a11y_set : @app.SystemSet = @app.system_set("mgstudio.a11y")
///|
pub fn[W : HasRes_A11yState + @core.ChangeTickWorld] ensure_a11y_state(
world : W,
) -> Unit {
let tick = @core.ChangeTickWorld::read_change_tick(world)
let res = HasRes_A11yState::get_a11y_state_resource(world)
if res.get() is None {
res.insert(A11yState::default(), tick)
}
}
///|
/// Poll host-side accessibility actions into `A11yState.pending_actions`.
pub fn[W : HasRes_A11yState + @core.ChangeTickWorld] a11y_poll_actions_system(
world : W,
) -> Unit {
let tick = @core.ChangeTickWorld::read_change_tick(world)
guard HasRes_A11yState::get_a11y_state_resource(world).get_ref_mut(tick)
is Some(state) else {
return
}
let n = host_a11y_actions_len()
if n <= 0 {
return
}
for i in 0.. @moon_accesskit.Action::Focus
2 => @moon_accesskit.Action::Click
3 => @moon_accesskit.Action::ScrollUp
4 => @moon_accesskit.Action::ScrollDown
5 => @moon_accesskit.Action::ScrollLeft
6 => @moon_accesskit.Action::ScrollRight
7 => @moon_accesskit.Action::SetValue
_ => @moon_accesskit.Action::Click
}
let node_id = @moon_accesskit.NodeId::from_u64(target.to_uint64())
let req = @moon_accesskit.ActionRequest::new(
action,
@moon_accesskit.TreeId::root(),
node_id,
)
if action == @moon_accesskit.Action::SetValue &&
host_action_value(i) is Some(value) {
req.set_data(@moon_accesskit.ActionData::Value(value))
}
if action == @moon_accesskit.Action::Focus ||
action == @moon_accesskit.Action::SetValue {
A11yState::set_focused_node(state, node_id)
}
A11yState::push_action(state, req)
}
host_a11y_actions_clear()
}
///|
pub fn[W : HasRes_A11yState + @core.ChangeTickWorld] a11y_plugin(
app : @app.App[W],
) -> @app.App[W] {
app
.add_pre_update_system_config(@app.system(ensure_a11y_state).in_set(a11y_set))
.add_pre_update_system_config(
@app.system(a11y_poll_actions_system).in_set(a11y_set),
)
}