lgui-core 0.2.2

Platform-neutral runtime and UI primitives for LGUI
Documentation
use std::ops::Range;

use super::{InteractionRole, UiId, UiNode, UiNodeKind, UiRect};

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SemanticRole {
    #[default]
    Generic,
    Window,
    Group,
    Label,
    Text,
    Image,
    Button,
    Link,
    Navigation,
    List,
    ListItem,
    Table,
    Row,
    Cell,
    CheckBox,
    Switch,
    RadioButton,
    TextInput,
    PasswordInput,
    SearchInput,
    ComboBox,
    ListBoxOption,
    Slider,
    ProgressIndicator,
    Dialog,
    Alert,
    Tab,
    TabList,
    TabPanel,
    Menu,
    MenuItem,
    ScrollView,
    Custom,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum SemanticAction {
    Click,
    Focus,
    Blur,
    SetValue,
    Increment,
    Decrement,
    ScrollIntoView,
    ScrollUp,
    ScrollDown,
    ScrollLeft,
    ScrollRight,
    SetTextSelection,
}

#[derive(Clone, Debug, Default, PartialEq)]
pub struct SemanticState {
    pub disabled: bool,
    pub selected: bool,
    pub checked: Option<bool>,
    pub expanded: Option<bool>,
    pub read_only: bool,
    pub required: bool,
    pub busy: bool,
    pub invalid: bool,
    pub hidden: bool,
    pub multiline: bool,
    pub password: bool,
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SemanticRelationships {
    pub labelled_by: Vec<UiId>,
    pub described_by: Vec<UiId>,
    pub controls: Vec<UiId>,
    pub owns: Vec<UiId>,
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SemanticText {
    pub selection: Option<Range<usize>>,
    pub character_count: Option<usize>,
}

#[derive(Clone, Debug, PartialEq)]
pub struct Semantics {
    pub role: SemanticRole,
    pub name: Option<String>,
    pub description: Option<String>,
    pub value: Option<String>,
    pub numeric_value: Option<f64>,
    pub numeric_min: Option<f64>,
    pub numeric_max: Option<f64>,
    pub numeric_step: Option<f64>,
    pub state: SemanticState,
    pub actions: Vec<SemanticAction>,
    pub relationships: SemanticRelationships,
    pub text: SemanticText,
}

impl Semantics {
    pub fn new(role: SemanticRole) -> Self {
        Self {
            role,
            name: None,
            description: None,
            value: None,
            numeric_value: None,
            numeric_min: None,
            numeric_max: None,
            numeric_step: None,
            state: SemanticState::default(),
            actions: Vec::new(),
            relationships: SemanticRelationships::default(),
            text: SemanticText::default(),
        }
    }

    pub fn name(mut self, value: impl Into<String>) -> Self {
        self.name = Some(value.into());
        self
    }

    pub fn description(mut self, value: impl Into<String>) -> Self {
        self.description = Some(value.into());
        self
    }

    pub fn value(mut self, value: impl Into<String>) -> Self {
        self.value = Some(value.into());
        self
    }

    pub fn action(mut self, action: SemanticAction) -> Self {
        if !self.actions.contains(&action) {
            self.actions.push(action);
        }
        self
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct SemanticNode {
    pub id: UiId,
    pub parent: Option<UiId>,
    pub children: Vec<UiId>,
    pub bounds: UiRect,
    pub semantics: Semantics,
}

impl SemanticNode {
    pub(crate) fn from_ui_node(node: &UiNode) -> Self {
        let mut semantics = node
            .semantics
            .clone()
            .unwrap_or_else(|| inferred_semantics(node));
        if node.event_policy.focus && !semantics.actions.contains(&SemanticAction::Focus) {
            semantics.actions.push(SemanticAction::Focus);
        }
        if (node.click_action.is_some() || node.click_handler.is_some())
            && !semantics.actions.contains(&SemanticAction::Click)
        {
            semantics.actions.push(SemanticAction::Click);
        }
        Self {
            id: node.id.clone(),
            parent: node.parent.clone(),
            children: node.children.clone(),
            bounds: node.hit_rect,
            semantics,
        }
    }
}

#[derive(Clone, Debug, Default, PartialEq)]
pub struct SemanticUpdate {
    pub nodes: Vec<SemanticNode>,
    pub removed: Vec<UiId>,
    pub focus: Option<UiId>,
    pub full: bool,
}

impl SemanticUpdate {
    #[cfg(feature = "accessibility")]
    pub(crate) fn full_from_tree(tree: &super::HostTree, focus: Option<UiId>) -> Self {
        Self {
            nodes: tree
                .nodes()
                .iter()
                .map(|node| SemanticNode::from_ui_node(node))
                .collect(),
            removed: Vec::new(),
            focus,
            full: true,
        }
    }
}

fn inferred_semantics(node: &UiNode) -> Semantics {
    let role = match (node.kind, node.interaction) {
        (UiNodeKind::Root, _) => SemanticRole::Window,
        (UiNodeKind::Text, _) => SemanticRole::Text,
        (UiNodeKind::Image | UiNodeKind::Icon, _) => SemanticRole::Image,
        (UiNodeKind::Button, _) | (_, InteractionRole::Button) => SemanticRole::Button,
        (UiNodeKind::Table, _) => SemanticRole::Table,
        (UiNodeKind::TableRow, _) | (_, InteractionRole::Row) => SemanticRole::Row,
        (_, InteractionRole::Navigation) => SemanticRole::Navigation,
        (_, InteractionRole::DragHandle) => SemanticRole::Slider,
        (UiNodeKind::Group | UiNodeKind::Panel, _) => SemanticRole::Group,
        (_, InteractionRole::Custom(_)) => SemanticRole::Custom,
        _ => SemanticRole::Generic,
    };
    let mut semantics = Semantics::new(role);
    if let Some(text) = node.text.as_ref() {
        if matches!(role, SemanticRole::Text | SemanticRole::Label) {
            semantics.value = Some(text.to_string());
            semantics.text.character_count = Some(text.chars().count());
        } else {
            semantics.name = Some(text.to_string());
        }
    }
    semantics
}