use super::{
Constraint, ConstraintContext, ConstraintSet, ConstraintSetBuilder, ConstraintVerification,
SpecReference, Violation, WcagLevel,
};
use super::{HasLabelConstraint, KeyboardAccessibleConstraint, ValidRoleConstraint};
use crate::Viewport;
use accesskit::NodeId;
use accesskit::Role;
use std::collections::BTreeMap;
fn is_container_role(role: Role) -> bool {
matches!(
role,
Role::Window
| Role::Group
| Role::GenericContainer
| Role::List
| Role::Table
| Role::TabList
| Role::MenuBar
| Role::Menu
| Role::Toolbar
| Role::Dialog
| Role::Application
| Role::Form
| Role::Grid
| Role::TreeGrid
| Role::Tree
)
}
#[derive(Debug, Clone, Copy)]
pub struct TerminalNoOverflow;
impl Constraint for TerminalNoOverflow {
#[tracing::instrument(level = "debug", skip(self, ctx))]
fn check(&self, node_id: NodeId, ctx: &ConstraintContext<'_>) -> Result<(), Violation> {
let node = match ctx.nodes.get(&node_id) {
Some(n) => n,
None => return Ok(()),
};
if let Some(bounds) = node.bounds() {
let col = bounds.x0 as i32;
let row = bounds.y0 as i32;
let width = bounds.width() as u32;
let height = bounds.height() as u32;
let fits_cols = col >= 0 && (col as u32 + width) <= ctx.viewport.width;
let fits_rows = row >= 0 && (row as u32 + height) <= ctx.viewport.height;
if fits_cols && fits_rows {
Ok(())
} else {
Err(Violation::TerminalOverflow {
element: crate::ElementId::from(node_id),
element_col: col,
element_row: row,
element_cols: width,
element_rows: height,
viewport_cols: ctx.viewport.width,
viewport_rows: ctx.viewport.height,
})
}
} else {
Ok(())
}
}
fn spec_ref(&self) -> SpecReference {
SpecReference::Wcag {
criterion: "1.4.10",
level: WcagLevel::AA,
url: "https://www.w3.org/WAI/WCAG22/Understanding/reflow",
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct MinReadableSize {
pub min_cols: u32,
pub min_rows: u32,
}
impl Default for MinReadableSize {
fn default() -> Self {
Self {
min_cols: 10,
min_rows: 3,
}
}
}
impl Constraint for MinReadableSize {
#[tracing::instrument(level = "debug", skip(self, ctx))]
fn check(&self, node_id: NodeId, ctx: &ConstraintContext<'_>) -> Result<(), Violation> {
let node = match ctx.nodes.get(&node_id) {
Some(n) => n,
None => return Ok(()),
};
if !is_container_role(node.role()) {
return Ok(());
}
if let Some(bounds) = node.bounds() {
let cols = bounds.width() as u32;
let rows = bounds.height() as u32;
if cols >= self.min_cols && rows >= self.min_rows {
Ok(())
} else {
Err(Violation::BelowMinReadableSize {
element: crate::ElementId::from(node_id),
actual_cols: cols,
actual_rows: rows,
min_cols: self.min_cols,
min_rows: self.min_rows,
})
}
} else {
Ok(())
}
}
fn spec_ref(&self) -> SpecReference {
SpecReference::Iso {
standard: "ISO 9241-3",
section: "Visual display requirements — minimum readable area",
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct TerminalAccessible {
pub min_readable: MinReadableSize,
}
impl TerminalAccessible {
pub fn with_min_readable(min_cols: u32, min_rows: u32) -> Self {
Self {
min_readable: MinReadableSize { min_cols, min_rows },
}
}
pub fn to_constraint_set(&self) -> ConstraintSet {
ConstraintSetBuilder::default()
.hard(HasLabelConstraint)
.hard(ValidRoleConstraint)
.hard(KeyboardAccessibleConstraint)
.hard(TerminalNoOverflow)
.hard(self.min_readable)
.build()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BreakpointTier {
Required,
Advisory,
ExpectedFail,
}
impl std::fmt::Display for BreakpointTier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Required => write!(f, "required"),
Self::Advisory => write!(f, "advisory"),
Self::ExpectedFail => write!(f, "expected-fail"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TerminalBreakpoint {
pub name: String,
pub cols: u32,
pub rows: u32,
pub tier: BreakpointTier,
}
impl TerminalBreakpoint {
pub fn new(name: impl Into<String>, cols: u32, rows: u32, tier: BreakpointTier) -> Self {
Self {
name: name.into(),
cols,
rows,
tier,
}
}
pub fn to_viewport(&self) -> Viewport {
Viewport::new(self.cols, self.rows)
}
}
impl std::fmt::Display for TerminalBreakpoint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} ({}×{}, {})",
self.name, self.cols, self.rows, self.tier
)
}
}
#[derive(Debug, Clone)]
pub struct TerminalBreakpointSet {
breakpoints: Vec<TerminalBreakpoint>,
}
impl TerminalBreakpointSet {
pub fn standard() -> Self {
Self {
breakpoints: vec![
TerminalBreakpoint::new("micro", 40, 12, BreakpointTier::ExpectedFail),
TerminalBreakpoint::new("tiny", 60, 20, BreakpointTier::Advisory),
TerminalBreakpoint::new("VT100", 80, 24, BreakpointTier::Required),
TerminalBreakpoint::new("small", 100, 30, BreakpointTier::Required),
TerminalBreakpoint::new("medium", 120, 40, BreakpointTier::Required),
TerminalBreakpoint::new("large", 160, 50, BreakpointTier::Required),
TerminalBreakpoint::new("ultrawide", 200, 60, BreakpointTier::Required),
],
}
}
pub fn breakpoints(&self) -> &[TerminalBreakpoint] {
&self.breakpoints
}
pub fn with_breakpoint(mut self, bp: TerminalBreakpoint) -> Self {
self.breakpoints.push(bp);
self
}
pub fn required(&self) -> Vec<&TerminalBreakpoint> {
self.breakpoints
.iter()
.filter(|bp| bp.tier == BreakpointTier::Required)
.collect()
}
pub fn advisory(&self) -> Vec<&TerminalBreakpoint> {
self.breakpoints
.iter()
.filter(|bp| bp.tier == BreakpointTier::Advisory)
.collect()
}
#[tracing::instrument(skip(self, constraint_set, nodes))]
pub fn verify_all(
&self,
root: NodeId,
nodes: &BTreeMap<NodeId, accesskit::Node>,
constraint_set: &ConstraintSet,
) -> BreakpointReport {
let mut results = Vec::with_capacity(self.breakpoints.len());
for bp in &self.breakpoints {
let ctx = ConstraintContext {
nodes,
viewport: bp.to_viewport(),
};
let verification = constraint_set.verify(root, &ctx);
let outcome = BreakpointOutcome::from_verification(&verification, bp.tier);
results.push(BreakpointResult {
breakpoint: bp.clone(),
outcome,
verification,
});
}
BreakpointReport { results }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BreakpointOutcome {
Pass,
Fail,
Warning,
ExpectedFailure,
}
impl BreakpointOutcome {
fn from_verification(v: &ConstraintVerification, tier: BreakpointTier) -> Self {
if v.is_valid() {
Self::Pass
} else {
match tier {
BreakpointTier::Required => Self::Fail,
BreakpointTier::Advisory => Self::Warning,
BreakpointTier::ExpectedFail => Self::ExpectedFailure,
}
}
}
}
impl std::fmt::Display for BreakpointOutcome {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Pass => write!(f, "✅ pass"),
Self::Fail => write!(f, "❌ fail"),
Self::Warning => write!(f, "⚠️ warning"),
Self::ExpectedFailure => write!(f, "📝 expected-fail"),
}
}
}
#[derive(Debug, Clone)]
pub struct BreakpointResult {
pub breakpoint: TerminalBreakpoint,
pub outcome: BreakpointOutcome,
pub verification: ConstraintVerification,
}
#[derive(Debug, Clone)]
pub struct BreakpointReport {
pub results: Vec<BreakpointResult>,
}
impl BreakpointReport {
pub fn is_valid(&self) -> bool {
self.results
.iter()
.all(|r| r.outcome != BreakpointOutcome::Fail)
}
pub fn count(&self, outcome: BreakpointOutcome) -> usize {
self.results.iter().filter(|r| r.outcome == outcome).count()
}
pub fn failures(&self) -> Vec<&BreakpointResult> {
self.results
.iter()
.filter(|r| r.outcome == BreakpointOutcome::Fail)
.collect()
}
pub fn warnings(&self) -> Vec<&BreakpointResult> {
self.results
.iter()
.filter(|r| r.outcome == BreakpointOutcome::Warning)
.collect()
}
pub fn summary(&self) -> String {
let mut out = String::from("Terminal Breakpoint Report\n");
out.push_str("─────────────────────────────────────────\n");
for r in &self.results {
out.push_str(&format!(
"{:<12} {:>3}×{:<3} [{}] {}\n",
r.breakpoint.name,
r.breakpoint.cols,
r.breakpoint.rows,
r.breakpoint.tier,
r.outcome,
));
}
out.push_str("─────────────────────────────────────────\n");
out.push_str(&format!(
"Result: {} ({} pass, {} fail, {} warn, {} expected-fail)\n",
if self.is_valid() { "PASS" } else { "FAIL" },
self.count(BreakpointOutcome::Pass),
self.count(BreakpointOutcome::Fail),
self.count(BreakpointOutcome::Warning),
self.count(BreakpointOutcome::ExpectedFailure),
));
out
}
}