use std::sync::{Arc, Mutex};
use accesskit::{
Action, ActionHandler, ActionRequest, ActivationHandler, DeactivationHandler, Node, NodeId,
Rect, Role, Toggled, Tree, TreeId, TreeUpdate,
};
use accesskit_winit::Adapter;
use winit::event::WindowEvent;
use winit::event_loop::ActiveEventLoop;
use winit::window::Window;
use crate::ecs::ui::components::{AccessibleRole, UiNodeContent};
use crate::ecs::world::{UI_LAYOUT_NODE, UI_LAYOUT_ROOT, World};
const ROOT_ID: NodeId = NodeId(1);
const ENTITY_ID_OFFSET: u64 = 2;
fn node_id(entity: freecs::Entity) -> NodeId {
NodeId(entity.id as u64 + ENTITY_ID_OFFSET)
}
#[derive(Default)]
struct AccessKitBridge {
latest_tree: Option<TreeUpdate>,
pending_actions: Vec<ActionRequest>,
}
struct ActivationBridge {
bridge: Arc<Mutex<AccessKitBridge>>,
}
impl ActivationHandler for ActivationBridge {
fn request_initial_tree(&mut self) -> Option<TreeUpdate> {
self.bridge
.lock()
.ok()
.and_then(|bridge| bridge.latest_tree.clone())
}
}
struct ActionBridge {
bridge: Arc<Mutex<AccessKitBridge>>,
}
impl ActionHandler for ActionBridge {
fn do_action(&mut self, request: ActionRequest) {
if let Ok(mut bridge) = self.bridge.lock() {
bridge.pending_actions.push(request);
}
}
}
struct DeactivationBridge;
impl DeactivationHandler for DeactivationBridge {
fn deactivate_accessibility(&mut self) {}
}
pub struct AccessKitHost {
adapter: Adapter,
bridge: Arc<Mutex<AccessKitBridge>>,
last_version: u64,
last_focus: Option<freecs::Entity>,
}
impl AccessKitHost {
pub fn new(event_loop: &ActiveEventLoop, window: &Window) -> Self {
let bridge = Arc::new(Mutex::new(AccessKitBridge::default()));
let adapter = Adapter::with_direct_handlers(
event_loop,
window,
ActivationBridge {
bridge: bridge.clone(),
},
ActionBridge {
bridge: bridge.clone(),
},
DeactivationBridge,
);
Self {
adapter,
bridge,
last_version: u64::MAX,
last_focus: None,
}
}
pub fn process_event(&mut self, window: &Window, event: &WindowEvent) {
self.adapter.process_event(window, event);
}
pub fn update(&mut self, world: &mut World) {
let version = world.resources.retained_ui.dirty.global_version;
let focus = world.resources.retained_ui.interaction.focused_entity;
if version != self.last_version || focus != self.last_focus {
self.last_version = version;
self.last_focus = focus;
let tree = build_tree_update(world);
self.adapter.update_if_active(|| tree.clone());
if let Ok(mut bridge) = self.bridge.lock() {
bridge.latest_tree = Some(tree);
}
}
let actions = match self.bridge.lock() {
Ok(mut bridge) => std::mem::take(&mut bridge.pending_actions),
Err(_) => Vec::new(),
};
for request in actions {
apply_action(world, request);
}
}
}
fn map_role(role: AccessibleRole) -> Role {
match role {
AccessibleRole::Button => Role::Button,
AccessibleRole::Slider => Role::Slider,
AccessibleRole::Checkbox => Role::CheckBox,
AccessibleRole::Radio => Role::RadioButton,
AccessibleRole::Toggle => Role::Switch,
AccessibleRole::TextInput => Role::TextInput,
AccessibleRole::TextArea => Role::MultilineTextInput,
AccessibleRole::Dropdown => Role::ComboBox,
AccessibleRole::Tab => Role::Tab,
AccessibleRole::TabPanel => Role::TabPanel,
AccessibleRole::Tree => Role::Tree,
AccessibleRole::TreeItem => Role::TreeItem,
AccessibleRole::Grid => Role::Grid,
AccessibleRole::GridCell => Role::Cell,
AccessibleRole::Dialog => Role::Dialog,
AccessibleRole::Alert => Role::Alert,
AccessibleRole::ProgressBar => Role::ProgressIndicator,
AccessibleRole::Menu => Role::Menu,
AccessibleRole::MenuItem => Role::MenuItem,
}
}
fn default_role(world: &World, entity: freecs::Entity) -> Role {
match world.ui.get_ui_node_content(entity) {
Some(UiNodeContent::Text { .. }) => Role::Label,
_ => Role::GenericContainer,
}
}
fn node_label(world: &World, entity: freecs::Entity) -> Option<String> {
let UiNodeContent::Text { text_slot, .. } = world.ui.get_ui_node_content(entity)? else {
return None;
};
let text = world.resources.text.cache.get_text(*text_slot)?;
if text.is_empty() {
return None;
}
Some(text.to_string())
}
fn toggled(value: bool) -> Toggled {
if value { Toggled::True } else { Toggled::False }
}
fn apply_widget_state(world: &World, entity: freecs::Entity, node: &mut Node) {
if let Some(data) = world.ui.get_ui_checkbox(entity) {
node.set_toggled(toggled(data.value));
}
if let Some(data) = world.ui.get_ui_toggle(entity) {
node.set_toggled(toggled(data.value));
}
if let Some(data) = world.ui.get_ui_radio(entity) {
node.set_toggled(toggled(data.selected));
}
if let Some(data) = world.ui.get_ui_slider(entity) {
node.set_numeric_value(data.value as f64);
node.set_min_numeric_value(data.min as f64);
node.set_max_numeric_value(data.max as f64);
}
if let Some(data) = world.ui.get_ui_progress_bar(entity) {
node.set_numeric_value(data.value as f64);
node.set_min_numeric_value(0.0);
node.set_max_numeric_value(1.0);
}
if let Some(data) = world.ui.get_ui_text_input(entity) {
node.set_value(data.text.clone());
}
if let Some(data) = world.ui.get_ui_text_area(entity) {
node.set_value(data.text.clone());
}
if let Some(data) = world.ui.get_ui_tree_node(entity) {
node.set_expanded(data.expanded);
}
}
fn build_node(
world: &World,
entity: freecs::Entity,
nodes: &mut Vec<(NodeId, Node)>,
siblings: &mut Vec<NodeId>,
) {
let Some(layout) = world.ui.get_ui_layout_node(entity) else {
return;
};
if !layout.visible {
return;
}
let id = node_id(entity);
siblings.push(id);
let interaction = world.ui.get_ui_node_interaction(entity);
let role = interaction
.and_then(|interaction| interaction.accessible_role.clone())
.map(map_role)
.unwrap_or_else(|| default_role(world, entity));
let mut node = Node::new(role);
let rect = layout.computed_rect;
node.set_bounds(Rect {
x0: rect.min.x as f64,
y0: rect.min.y as f64,
x1: rect.max.x as f64,
y1: rect.max.y as f64,
});
if let Some(label) = node_label(world, entity) {
node.set_label(label);
}
apply_widget_state(world, entity, &mut node);
if let Some(interaction) = interaction
&& (interaction.tab_index.is_some() || interaction.accessible_role.is_some())
{
node.add_action(Action::Focus);
}
let mut child_ids: Vec<NodeId> = Vec::new();
if let Some(children) = world.resources.transform_state.children_cache.get(&entity) {
for &child in children {
build_node(world, child, nodes, &mut child_ids);
}
}
node.set_children(child_ids);
nodes.push((id, node));
}
fn build_tree_update(world: &World) -> TreeUpdate {
let mut nodes: Vec<(NodeId, Node)> = Vec::new();
let mut root_children: Vec<NodeId> = Vec::new();
for root_entity in world.ui.query_entities(UI_LAYOUT_ROOT) {
if let Some(children) = world
.resources
.transform_state
.children_cache
.get(&root_entity)
{
for &child in children {
build_node(world, child, &mut nodes, &mut root_children);
}
}
}
let (viewport_width, viewport_height) = world
.resources
.window
.cached_viewport_size
.map(|(width, height)| (width as f64, height as f64))
.unwrap_or((800.0, 600.0));
let mut root = Node::new(Role::Window);
root.set_bounds(Rect {
x0: 0.0,
y0: 0.0,
x1: viewport_width,
y1: viewport_height,
});
root.set_children(root_children);
nodes.push((ROOT_ID, root));
let focus = world
.resources
.retained_ui
.interaction
.focused_entity
.map(node_id)
.filter(|focus_id| nodes.iter().any(|(id, _)| id == focus_id))
.unwrap_or(ROOT_ID);
TreeUpdate {
nodes,
tree: Some(Tree::new(ROOT_ID)),
tree_id: TreeId::ROOT,
focus,
}
}
fn apply_action(world: &mut World, request: ActionRequest) {
if !matches!(request.action, Action::Focus) {
return;
}
let target = request.target_node.0;
if target < ENTITY_ID_OFFSET {
return;
}
let entity_id = (target - ENTITY_ID_OFFSET) as u32;
let mut focus_entity = None;
for entity in world.ui.query_entities(UI_LAYOUT_NODE) {
if entity.id == entity_id {
focus_entity = Some(entity);
break;
}
}
if let Some(entity) = focus_entity {
world.resources.retained_ui.interaction.focused_entity = Some(entity);
}
}