use crate::core::Style;
use std::sync::atomic::{AtomicU64, Ordering};
static ELEMENT_ID_COUNTER: AtomicU64 = AtomicU64::new(1);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ElementId(u64);
impl ElementId {
pub fn new() -> Self {
let id = ELEMENT_ID_COUNTER
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |current| {
current.checked_add(1).filter(|next| *next != 0)
})
.unwrap_or_else(|_| {
panic!("ElementId counter exhausted; cannot allocate more element IDs")
});
Self(id)
}
pub const fn root() -> Self {
Self(0)
}
pub fn as_u64(&self) -> u64 {
self.0
}
}
impl Default for ElementId {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ElementType {
Root,
Box,
Text,
VirtualText,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AccessibilityRole {
#[default]
Generic,
Text,
Button,
TextInput,
TextArea,
Select,
MultiSelect,
Option,
Dialog,
Menu,
ColorPicker,
FilePicker,
Viewport,
Status,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccessibilityProps {
pub role: AccessibilityRole,
pub label: Option<String>,
pub description: Option<String>,
pub disabled: bool,
pub read_only: bool,
pub focusable: bool,
pub selected: Option<bool>,
pub value: Option<String>,
}
impl Default for AccessibilityProps {
fn default() -> Self {
Self::new(AccessibilityRole::Generic)
}
}
impl AccessibilityProps {
pub fn new(role: AccessibilityRole) -> Self {
Self {
role,
label: None,
description: None,
disabled: false,
read_only: false,
focusable: false,
selected: None,
value: None,
}
}
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn read_only(mut self, read_only: bool) -> Self {
self.read_only = read_only;
self
}
pub fn focusable(mut self, focusable: bool) -> Self {
self.focusable = focusable;
self
}
pub fn selected(mut self, selected: bool) -> Self {
self.selected = Some(selected);
self
}
pub fn value(mut self, value: impl Into<String>) -> Self {
self.value = Some(value.into());
self
}
}
#[derive(Debug, Clone, Default)]
pub struct Children(Vec<Element>);
impl Children {
pub fn new() -> Self {
Self(Vec::new())
}
pub fn push(&mut self, element: Element) {
self.0.push(element);
}
pub fn iter(&self) -> impl Iterator<Item = &Element> {
self.0.iter()
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Element> {
self.0.iter_mut()
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn get(&self, index: usize) -> Option<&Element> {
self.0.get(index)
}
pub fn get_mut(&mut self, index: usize) -> Option<&mut Element> {
self.0.get_mut(index)
}
}
impl IntoIterator for Children {
type Item = Element;
type IntoIter = std::vec::IntoIter<Element>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<'a> IntoIterator for &'a Children {
type Item = &'a Element;
type IntoIter = std::slice::Iter<'a, Element>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl FromIterator<Element> for Children {
fn from_iter<I: IntoIterator<Item = Element>>(iter: I) -> Self {
Self(iter.into_iter().collect())
}
}
pub use crate::components::text::Line;
#[derive(Debug)]
pub struct Element {
pub id: ElementId,
pub element_type: ElementType,
pub style: Style,
pub children: Children,
pub text_content: Option<String>,
pub spans: Option<Vec<Line>>,
pub key: Option<String>,
pub accessibility: Option<AccessibilityProps>,
pub scroll_offset_x: Option<u16>,
pub scroll_offset_y: Option<u16>,
}
impl Clone for Element {
fn clone(&self) -> Self {
Self {
id: ElementId::new(), element_type: self.element_type,
style: self.style.clone(),
children: self.children.clone(),
text_content: self.text_content.clone(),
spans: self.spans.clone(),
key: self.key.clone(),
accessibility: self.accessibility.clone(),
scroll_offset_x: self.scroll_offset_x,
scroll_offset_y: self.scroll_offset_y,
}
}
}
impl Element {
pub fn new(element_type: ElementType) -> Self {
Self {
id: ElementId::new(),
element_type,
style: Style::new(),
children: Children::new(),
text_content: None,
spans: None,
key: None,
accessibility: None,
scroll_offset_x: None,
scroll_offset_y: None,
}
}
pub fn root() -> Self {
Self {
id: ElementId::root(),
element_type: ElementType::Root,
style: Style::new(),
children: Children::new(),
text_content: None,
spans: None,
key: None,
accessibility: None,
scroll_offset_x: None,
scroll_offset_y: None,
}
}
pub fn box_element() -> Self {
Self::new(ElementType::Box)
}
pub fn text(content: impl Into<String>) -> Self {
let mut element = Self::new(ElementType::Text);
element.text_content = Some(content.into());
element
}
pub fn with_key(mut self, key: impl Into<String>) -> Self {
self.key = Some(key.into());
self
}
pub fn with_accessibility(mut self, props: AccessibilityProps) -> Self {
self.accessibility = Some(props);
self
}
pub fn accessibility(&self) -> Option<&AccessibilityProps> {
self.accessibility.as_ref()
}
pub fn accessibility_mut(&mut self) -> Option<&mut AccessibilityProps> {
self.accessibility.as_mut()
}
pub fn add_child(&mut self, child: Element) {
self.children.push(child);
}
pub fn is_text(&self) -> bool {
matches!(
self.element_type,
ElementType::Text | ElementType::VirtualText
)
}
pub fn is_box(&self) -> bool {
matches!(self.element_type, ElementType::Box)
}
pub fn is_root(&self) -> bool {
matches!(self.element_type, ElementType::Root)
}
pub fn get_text(&self) -> Option<&str> {
self.text_content.as_deref()
}
pub fn accessible_text(&self) -> String {
let mut parts = Vec::new();
if let Some(accessibility) = &self.accessibility {
push_unique_part(&mut parts, accessibility.label.as_deref());
push_unique_part(&mut parts, accessibility.value.as_deref());
push_unique_part(&mut parts, accessibility.description.as_deref());
}
if parts.is_empty() {
push_unique_part(&mut parts, self.text_content.as_deref());
}
for child in &self.children {
let text = child.accessible_text();
push_unique_part(&mut parts, (!text.is_empty()).then_some(text.as_str()));
}
parts.join(" ")
}
}
fn push_unique_part(parts: &mut Vec<String>, value: Option<&str>) {
let Some(value) = value else {
return;
};
let trimmed = value.trim();
if trimmed.is_empty() || parts.iter().any(|part| part == trimmed) {
return;
}
parts.push(trimmed.to_string());
}
impl Default for Element {
fn default() -> Self {
Self::new(ElementType::Box)
}
}
#[macro_export]
macro_rules! impl_into_element {
($($ty:ty),* $(,)?) => {
$(
impl From<$ty> for $crate::core::Element {
fn from(c: $ty) -> Self {
c.into_element()
}
}
)*
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_element_id_unique() {
let id1 = ElementId::new();
let id2 = ElementId::new();
assert_ne!(id1, id2);
}
#[test]
fn test_root_element_id() {
let root = ElementId::root();
assert_eq!(root.as_u64(), 0);
}
#[test]
fn test_element_creation() {
let element = Element::box_element();
assert_eq!(element.element_type, ElementType::Box);
assert!(element.children.is_empty());
}
#[test]
fn test_text_element() {
let element = Element::text("Hello");
assert_eq!(element.element_type, ElementType::Text);
assert_eq!(element.get_text(), Some("Hello"));
}
#[test]
fn test_add_child() {
let mut parent = Element::box_element();
let child = Element::text("Child");
parent.add_child(child);
assert_eq!(parent.children.len(), 1);
}
#[test]
fn test_children_iterator() {
let mut parent = Element::box_element();
parent.add_child(Element::text("A"));
parent.add_child(Element::text("B"));
let texts: Vec<_> = parent
.children
.iter()
.filter_map(|e| e.get_text())
.collect();
assert_eq!(texts, vec!["A", "B"]);
}
#[test]
fn test_clone_creates_new_id() {
let original = Element::text("Hello");
let cloned = original.clone();
assert_ne!(original.id, cloned.id);
assert_eq!(original.get_text(), cloned.get_text());
assert_eq!(original.element_type, cloned.element_type);
}
#[test]
fn test_accessibility_metadata_and_fallback_text() {
let mut element = Element::box_element().with_accessibility(
AccessibilityProps::new(AccessibilityRole::Button)
.label("Submit")
.description("Saves the form")
.focusable(true),
);
element.add_child(Element::text("Ctrl+S"));
let Some(props) = element.accessibility() else {
panic!("accessibility metadata missing");
};
assert_eq!(props.role, AccessibilityRole::Button);
assert!(props.focusable);
assert_eq!(element.accessible_text(), "Submit Saves the form Ctrl+S");
}
#[test]
fn test_accessible_text_uses_descendants_when_unlabelled() {
let mut element = Element::box_element();
element.add_child(Element::text("First"));
element.add_child(Element::text("Second"));
assert_eq!(element.accessible_text(), "First Second");
}
}