Skip to content
Portal Control Protocol

Element

An Element is a single UI component drawn from the AT-SPI2 accessibility tree. Buttons, text fields, menu items, sliders, tables, and every other widget all become Element primitives. For applications without AT-SPI2 support, the compositor generates fallback elements from surface metadata.

Elements form a tree. Each element has a parent (except roots) and zero or more children. The root of the tree for a given surface is referenced by Surface.atspi2_root.

The Element primitive is the unit of interaction for PCP. When System Intelligence needs to click a button or read text from a field, it addresses the target by element ID.

{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Element",
"type": "object",
"required": ["id", "surface_id", "app_id", "role", "name", "states", "geometry", "actions"],
"properties": {
"id": {
"type": "string",
"pattern": "^(atspi|surf|native):[a-z0-9-]+$",
"description": "Globally unique element identifier. Prefix indicates source: atspi: (AT-SPI2), surf: (compositor-only), native: (Tier 1 app-defined)."
},
"surface_id": { "type": "string" },
"app_id": { "type": "string" },
"role": {
"type": "string",
"description": "AT-SPI2 role enum string.",
"examples": ["push_button", "entry", "document_text", "table", "list", "window"]
},
"name": {
"type": ["string", "null"],
"description": "Accessible name (label). May be null for decorative elements."
},
"description": {
"type": ["string", "null"],
"description": "Accessible description. Supplementary to name."
},
"states": {
"type": "array",
"items": { "type": "string" },
"description": "Current AT-SPI2 states.",
"examples": ["enabled", "visible", "focusable", "focused", "editable", "selected"]
},
"geometry": {
"type": "object",
"required": ["x", "y", "w", "h"],
"properties": {
"x": { "type": "integer" },
"y": { "type": "integer" },
"w": { "type": "integer", "minimum": 0 },
"h": { "type": "integer", "minimum": 0 }
}
},
"parent": {
"type": ["string", "null"],
"description": "Parent element ID. null for root elements."
},
"children": {
"type": "array",
"items": { "type": "string" },
"description": "Child element IDs. Populated when requesting the tree; omitted in element.get responses for non-leaf queries."
},
"actions": {
"type": "array",
"items": { "type": "string" },
"description": "AT-SPI2 supported actions."
},
"text": {
"type": ["string", "null"],
"description": "Current text content for text/entry/document roles. null for non-text elements."
},
"text_selection": {
"type": ["object", "null"],
"properties": {
"start": { "type": "integer" },
"end": { "type": "integer" },
"text": { "type": "string" }
},
"description": "Current text selection, if any."
},
"caret_position": {
"type": ["integer", "null"],
"description": "Current caret/cursor position in text content."
},
"value": {
"type": ["object", "null"],
"properties": {
"current": { "type": "number" },
"minimum": { "type": "number" },
"maximum": { "type": "number" },
"step": { "type": ["number", "null"] },
"text": { "type": ["string", "null"] }
},
"description": "Current value for slider/progress/spin_button/checkbox roles."
},
"table_info": {
"type": ["object", "null"],
"properties": {
"rows": { "type": "integer" },
"columns": { "type": "integer" },
"selected_rows": { "type": "array", "items": { "type": "integer" } },
"selected_columns": { "type": "array", "items": { "type": "integer" } }
},
"description": "Table/grid metadata for table roles."
},
"relationships": {
"type": "object",
"properties": {
"labelled_by": { "type": ["string", "null"] },
"label_for": { "type": ["string", "null"] },
"controller_of": { "type": ["string", "null"] },
"controlled_by": { "type": ["string", "null"] },
"member_of": { "type": ["array", "null"], "items": { "type": "string" } },
"node_parent_of": { "type": ["string", "null"] }
}
},
"fallback_note": {
"type": ["string", "null"],
"description": "Set when this element is a compositor-only fallback (no AT-SPI2 data). Describes the limitation."
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Element {
pub id: ElementId,
pub surface_id: SurfaceId,
pub app_id: AppId,
pub role: AtspiRole,
pub name: Option<String>,
pub description: Option<String>,
pub states: Vec<AtspiState>,
pub geometry: Geometry,
pub parent: Option<ElementId>,
pub children: Vec<ElementId>,
pub actions: Vec<String>,
pub text: Option<String>,
pub text_selection: Option<TextSelection>,
pub caret_position: Option<usize>,
pub value: Option<ValueInfo>,
pub table_info: Option<TableInfo>,
pub relationships: ElementRelationships,
pub fallback_note: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ElementId(pub String);

Every element has exactly one role. The role determines what actions are available and what additional fields are populated.

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum AtspiRole {
// Window/container roles
Window,
Frame,
Dialog,
Alert,
Panel,
ScrollPane,
// Interactive controls
PushButton,
ToggleButton,
CheckBox,
RadioButton,
ComboBox,
SpinButton,
Slider,
ScrollBar,
Dial,
PageTab,
PageTabList,
// Text elements
Entry,
Text,
Paragraph,
Heading,
DocumentText,
DocumentWeb,
DocumentSpreadsheet,
DocumentPresentation,
DocumentEmail,
Link,
Terminal,
Static,
Label,
// Collection elements
List,
ListBox,
ListItem,
Tree,
TreeTable,
TreeItem,
Table,
TableCell,
TableColumnHeader,
TableRowHeader,
Grid,
// Menu elements
MenuBar,
Menu,
MenuItem,
CheckMenuItem,
RadioMenuItem,
Separator,
// Specialized
Canvas,
Animation,
Icon,
Image,
Chart,
ProgressBar,
StatusBar,
ToolBar,
ToolTip,
Calendar,
ColorChooser,
FileChooser,
FontChooser,
// Application
Application,
Embedded,
Section,
// Fallback
Unknown,
}

An element can have any combination of states. The states array reflects the current runtime state of the widget.

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum AtspiState {
Enabled,
Disabled,
Visible,
Invisible,
Showing,
Hidden,
Focusable,
Focused,
Selected,
Selectable,
Checked,
Unchecked,
Indeterminate,
Editable,
ReadOnly,
Expandable,
Expanded,
Collapsed,
Multiselectable,
Required,
InvalidEntry,
SupportsAutocompletion,
Transient,
Vertical,
Horizontal,
Modal,
MultiLine,
Protected,
Stale,
Busy,
Resizable,
Movable,
Sizeable,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TextSelection {
pub start: usize,
pub end: usize,
pub text: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValueInfo {
pub current: f64,
pub minimum: f64,
pub maximum: f64,
pub step: Option<f64>,
pub text: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TableInfo {
pub rows: usize,
pub columns: usize,
pub selected_rows: Vec<usize>,
pub selected_columns: Vec<usize>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ElementRelationships {
pub labelled_by: Option<ElementId>,
pub label_for: Option<ElementId>,
pub controller_of: Option<ElementId>,
pub controlled_by: Option<ElementId>,
pub member_of: Option<Vec<ElementId>>,
}

Three prefixes indicate the element’s source:

Prefix Source Description
atspi: AT-SPI2 accessibility tree Standard GTK/Qt applications
surf: Compositor-only fallback Apps without accessibility support
native: Tier 1 app-defined Applications that define their own element IDs

Element relationships express semantic connections that go beyond the parent/child tree structure:

Relationship Meaning
labelled_by Another element provides the accessible label for this one
label_for This element serves as a label for another element
controller_of This element controls the state of another element
controlled_by This element’s state is controlled by another element
member_of This element belongs to a named group

Only populated for text-entry and document roles when a selection exists. The start and end indices are character offsets into the text field.

Only populated for roles that represent a scalar value: Slider, ProgressBar, SpinButton, CheckBox. The text field provides a human-readable representation when available.

Only populated for Table, Grid, and TreeTable roles. Tracks the table dimensions and which rows and columns are currently selected.

Present only on compositor-only fallback elements. Describes the specific limitation (no AT-SPI2 support, partial tree, etc.). When this field is set, interactions with the element require input simulation rather than AT-SPI2 action invocation.

Example: AT-SPI2 Button (Thunderbird Compose)

Section titled “Example: AT-SPI2 Button (Thunderbird Compose)”
{
"id": "atspi:47-w1-btn-compose",
"surface_id": "surf:47-w1",
"app_id": "app:wl-47",
"role": "push_button",
"name": "Write",
"description": "Compose a new email message",
"states": ["enabled", "visible", "focusable"],
"geometry": { "x": 150, "y": 598, "w": 120, "h": 36 },
"parent": "atspi:47-w1-toolbar",
"children": [],
"actions": ["activate"],
"text": null,
"text_selection": null,
"caret_position": null,
"value": null,
"table_info": null,
"relationships": {
"labelled_by": null,
"label_for": null,
"controller_of": "atspi:47-w1-compose-window",
"controlled_by": null,
"member_of": null
},
"fallback_note": null
}

This is a fully accessible button from Thunderbird’s AT-SPI2 tree. It has a role of push_button, one supported action (activate), and a controller_of relationship pointing to the compose window it opens.

{
"id": "surf:47-w1-structural",
"surface_id": "surf:47-w1",
"app_id": "app:wl-47",
"role": "window",
"name": "Some Game — Main Menu",
"states": ["visible"],
"geometry": { "x": 0, "y": 0, "w": 1920, "h": 1080 },
"parent": null,
"children": [],
"actions": ["focus", "close", "minimize", "maximize", "move", "resize"],
"text": null,
"text_selection": null,
"caret_position": null,
"value": null,
"table_info": null,
"relationships": {},
"fallback_note": "structural-only: no AT-SPI2 support. Input simulation required for interaction."
}

This is a fallback element for a game with no accessibility support. The surf: prefix and the fallback_note field both indicate that the compositor generated this element from surface metadata. The available actions are limited to basic window management operations. Interacting with anything inside this window requires input simulation (mouse clicks, keyboard events) rather than AT-SPI2 action invocation.

Last updated: