use crate::constraints::{
ConstraintContext, ConstraintSet, ConstraintSetBuilder, ConstraintVerification,
HasLabelConstraint, KeyboardAccessibleConstraint, MinTouchTargetConstraint,
NoOverflowConstraint, ValidRoleConstraint,
};
use crate::{VerificationReport, Viewport, validators};
use accesskit::{Node, NodeId, TreeUpdate};
use std::collections::HashMap;
use std::marker::PhantomData;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Pending;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Verified;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Rendered;
#[derive(Debug, Clone)]
pub struct Layout<State> {
nodes: HashMap<NodeId, Node>,
root: NodeId,
viewport: Option<Viewport>,
report: Option<VerificationReport>,
_state: PhantomData<State>,
}
impl Layout<Pending> {
pub fn from_update(update: TreeUpdate) -> Self {
let nodes: HashMap<NodeId, Node> = update.nodes.into_iter().collect();
let root = update.focus;
Self {
nodes,
root,
viewport: None,
report: None,
_state: PhantomData,
}
}
#[tracing::instrument(skip(self), fields(root = ?self.root))]
pub fn verify_a(self, viewport: Viewport) -> Result<Layout<Verified>, VerificationReport> {
tracing::debug!("Verifying layout against WCAG Level A");
let mut report = VerificationReport::new();
self.validate_tree_recursive(self.root, &viewport, &mut report, false);
if report.has_errors() {
tracing::error!(error_count = report.error_count(), "Verification failed");
Err(report)
} else {
tracing::info!("Verification successful");
Ok(Layout {
nodes: self.nodes,
root: self.root,
viewport: Some(viewport),
report: Some(report),
_state: PhantomData,
})
}
}
#[tracing::instrument(skip(self), fields(root = ?self.root))]
pub fn verify_aa(self, viewport: Viewport) -> Result<Layout<Verified>, VerificationReport> {
tracing::debug!("Verifying layout against WCAG Level AA");
let mut report = VerificationReport::new();
self.validate_tree_recursive(self.root, &viewport, &mut report, false);
if report.has_errors() {
tracing::error!(error_count = report.error_count(), "Verification failed");
Err(report)
} else {
tracing::info!("Verification successful");
Ok(Layout {
nodes: self.nodes,
root: self.root,
viewport: Some(viewport),
report: Some(report),
_state: PhantomData,
})
}
}
#[tracing::instrument(skip(self), fields(root = ?self.root))]
pub fn verify_aaa(self, viewport: Viewport) -> Result<Layout<Verified>, VerificationReport> {
tracing::debug!("Verifying layout against WCAG Level AAA");
let mut report = VerificationReport::new();
self.validate_tree_recursive(self.root, &viewport, &mut report, true);
if report.has_errors() {
tracing::error!(error_count = report.error_count(), "Verification failed");
Err(report)
} else {
tracing::info!("Verification successful");
Ok(Layout {
nodes: self.nodes,
root: self.root,
viewport: Some(viewport),
report: Some(report),
_state: PhantomData,
})
}
}
#[tracing::instrument(skip(self), fields(root = ?self.root, profile = ?profile))]
pub fn verify_with_profile(
self,
viewport: Viewport,
profile: ConstraintProfile,
) -> Result<Layout<Verified>, ConstraintVerification> {
tracing::debug!(?profile, "Verifying layout with constraint profile");
let constraint_set = profile.to_constraint_set();
let ctx = ConstraintContext {
nodes: &self.nodes,
viewport,
};
let verification = constraint_set.verify(self.root, &ctx);
if verification.is_valid() {
tracing::info!("Constraint verification successful");
Ok(Layout {
nodes: self.nodes,
root: self.root,
viewport: Some(viewport),
report: None,
_state: PhantomData,
})
} else {
tracing::error!(
hard = verification.hard_violations.len(),
structural = verification.structural_violations.len(),
"Constraint verification failed"
);
Err(verification)
}
}
#[tracing::instrument(skip(self, constraint_set), fields(root = ?self.root))]
pub fn verify_custom(
self,
viewport: Viewport,
constraint_set: &ConstraintSet,
) -> Result<Layout<Verified>, ConstraintVerification> {
tracing::debug!("Verifying layout with custom constraint set");
let ctx = ConstraintContext {
nodes: &self.nodes,
viewport,
};
let verification = constraint_set.verify(self.root, &ctx);
if verification.is_valid() {
tracing::info!("Custom constraint verification successful");
Ok(Layout {
nodes: self.nodes,
root: self.root,
viewport: Some(viewport),
report: None,
_state: PhantomData,
})
} else {
tracing::error!(
hard = verification.hard_violations.len(),
"Custom constraint verification failed"
);
Err(verification)
}
}
fn validate_tree_recursive(
&self,
node_id: NodeId,
viewport: &Viewport,
report: &mut VerificationReport,
check_aaa: bool,
) {
if let Err(e) = validators::validate_has_label(&self.nodes, node_id) {
report.add_error(e);
}
if let Err(e) = validators::validate_valid_role(&self.nodes, node_id) {
report.add_error(e);
}
if let Err(e) = validators::validate_keyboard_accessible(&self.nodes, node_id) {
report.add_error(e);
}
if let Err(e) = validators::validate_no_overflow(&self.nodes, node_id, *viewport) {
report.add_error(e);
}
if check_aaa && let Err(e) = validators::validate_min_target_size(&self.nodes, node_id) {
report.add_error(e);
}
if let Some(node) = self.nodes.get(&node_id) {
for child_id in node.children() {
self.validate_tree_recursive(*child_id, viewport, report, check_aaa);
}
}
}
}
impl Layout<Verified> {
pub fn root(&self) -> NodeId {
self.root
}
pub fn viewport(&self) -> Viewport {
self.viewport.expect("Verified layout must have viewport")
}
pub fn report(&self) -> &VerificationReport {
self.report
.as_ref()
.expect("Verified layout must have report")
}
pub fn nodes(&self) -> &HashMap<NodeId, Node> {
&self.nodes
}
#[tracing::instrument(skip(self, backend), fields(root = ?self.root))]
pub fn render<B: crate::RenderBackend>(
self,
backend: &B,
) -> (Layout<Rendered>, crate::RenderStats) {
tracing::debug!("Rendering layout via backend");
let stats = backend.render_tree(&self.nodes, self.root);
let layout = Layout {
nodes: self.nodes,
root: self.root,
viewport: self.viewport,
report: self.report,
_state: PhantomData,
};
(layout, stats)
}
}
impl Layout<Rendered> {
pub fn root(&self) -> NodeId {
self.root
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConstraintProfile {
WcagA,
WcagAA,
WcagAAA,
}
impl ConstraintProfile {
pub fn to_constraint_set(&self) -> ConstraintSet {
match self {
Self::WcagA => ConstraintSetBuilder::default()
.hard(HasLabelConstraint)
.hard(ValidRoleConstraint)
.hard(KeyboardAccessibleConstraint)
.build(),
Self::WcagAA => ConstraintSetBuilder::default()
.hard(HasLabelConstraint)
.hard(ValidRoleConstraint)
.hard(KeyboardAccessibleConstraint)
.hard(NoOverflowConstraint)
.build(),
Self::WcagAAA => ConstraintSetBuilder::default()
.hard(HasLabelConstraint)
.hard(ValidRoleConstraint)
.hard(KeyboardAccessibleConstraint)
.hard(NoOverflowConstraint)
.hard(MinTouchTargetConstraint)
.build(),
}
}
}