nightshade 0.40.0

A cross-platform data-oriented game engine.
Documentation
//! Bridges the retained UI tree to platform screen readers through AccessKit.
//!
//! Every interactive widget already carries an [`AccessibleRole`]. This module
//! walks the live UI tree each time it changes, builds an `accesskit::TreeUpdate`
//! mirroring the on screen layout, and feeds it to an `accesskit_winit::Adapter`.
//! The adapter owns its handlers on a platform thread, so the small
//! [`AccessKitBridge`] behind an `Arc<Mutex<...>>` is the one piece of shared
//! state required to hand the latest tree back on activation and to collect
//! action requests for the frame loop to apply.

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) {}
}

/// Owns the AccessKit adapter for one window plus the shared bridge that feeds
/// it. Created once the window exists and updated each frame after UI layout.
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);
    }
}