T4 round 0: iced is eliminated, and the probe that cleared it proved nothing
Round 0 is the ladder's cheapest hard gate: for each candidate, a desk survey and a demonstrated accessibility route -- a window exposing one node with a role and a name, read back through the platform adapter. It is first precisely so that a candidate with no accessibility story costs one round instead of four. C1 (egui/eframe 0.35.0) passes on a first-party route: egui-winit's accesskit feature is literally dep:accesskit_winit, so the chain runs eframe -> egui-winit -> accesskit_winit -> accesskit_unix and inherits that crate's window-lifecycle handling. C2 (vello 0.9.0 + winit 0.30.13) passes on the manual accesskit_winit route the contract names, wired into the same ApplicationHandler that owns the vello renderer. C3 (iced 0.14.0) FAILS, and the interesting part is that it first reported as a pass. The probe registered a hand-built two-node tree through accesskit_unix::Adapter, which takes no window handle at all -- only handlers -- and registers with AT-SPI from process identity. It read back cleanly. It also happened to label its button exactly as iced's own view() labelled a button, so the transcript looked as though iced had produced it. Deleting iced from that probe would produce the identical readback. Round 0 asks whether the CANDIDATE exposes a route; a process-level side channel answers a different question. The verdict is recorded with dual attribution, because two distinct things went wrong. The probe-design defect is the false positive above. The candidate limitation -- which alone fails the round -- is that iced 0.14 ships no accessibility integration anywhere (accesskit appears in no iced crate manifest) and its stock runner hands application code neither the winit ActiveEventLoop nor a pre-visibility Window, both of which every accesskit_winit::Adapter constructor requires. That scoping matters and is deliberate: iced_winit documents a conversion module for users implementing a custom event loop, so a hand-built shell remains conceivable but unproven, and would mean owning the shell. Upstream iced #552 is still open. The evidence file keeps the verifier's factual READBACK: PASS beneath a ROUND-0 RESULT: FAIL annotation rather than being rewritten. A corrected record that erases the false positive teaches nobody why it was false. Two findings carried forward. C1's frame node is unnamed -- its readback path is application:'probe-egui' / frame:'' / button:'...' where C2 names its frame -- which is non-disqualifying here but means a screen-reader user hears an unnamed window, and round 3 must check it. And AT-SPI application registration is gated behind two settings that are off by default; without both, probes connect to the bus and enumerate zero applications, which would read as a candidate failure rather than the environment absence it is. The spike workspace lives outside the root workspace with its own lockfile; the root gains one line, exclude = ["spikes"], and nothing else. Round 0's probes depend on no epiphany crate, which is what makes this round independent of the pinned source baseline the later rounds need. Root gate unchanged: fmt clean, clippy -D warnings clean, 1371 tests passed 0 failed. Spike workspace gated separately: fmt clean, all probes build --locked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RSX4zSLgKvtiXaPjnMqLGz
This commit is contained in:
parent
0c8b8db1a5
commit
496dfd5640
|
|
@ -1,5 +1,6 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
exclude = ["spikes"]
|
||||||
members = [
|
members = [
|
||||||
"crates/epiphany-determinism",
|
"crates/epiphany-determinism",
|
||||||
"crates/epiphany-core",
|
"crates/epiphany-core",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
/target
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,22 @@
|
||||||
|
[workspace]
|
||||||
|
resolver = "2"
|
||||||
|
members = [
|
||||||
|
"probe-egui",
|
||||||
|
"probe-vello",
|
||||||
|
"probe-iced",
|
||||||
|
]
|
||||||
|
|
||||||
|
# a11y-verifier is a standalone Python script (a11y-verifier/verify.py), not
|
||||||
|
# a Cargo crate: it uses gi.repository.Atspi (the official AT-SPI2
|
||||||
|
# GObject-introspection binding), an "equivalent AT-SPI client" under the
|
||||||
|
# contract's Round 0 evidence rule. It is not a workspace member because it
|
||||||
|
# is not Rust.
|
||||||
|
|
||||||
|
# This workspace is deliberately outside the repo-root workspace (see the
|
||||||
|
# root Cargo.toml's `exclude = ["spikes"]`, and CONTRACT_EDITOR_T4_SPIKE.md
|
||||||
|
# pin 1). It is throwaway spike code for the T4 toolkit decision and is never
|
||||||
|
# intended to ship. It has its own committed Cargo.lock.
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
@ -0,0 +1,139 @@
|
||||||
|
# T4 toolkit spike — decisions and findings log
|
||||||
|
|
||||||
|
Governed by `spec/CONTRACT_EDITOR_T4_SPIKE.md`. This file records
|
||||||
|
implementation decisions and named deviations, per round.
|
||||||
|
|
||||||
|
## Round 0 — accessibility route + desk survey
|
||||||
|
|
||||||
|
**Environment prerequisite, not obvious from the contract:** on this
|
||||||
|
machine (sway, AT-SPI2 via `at-spi-bus-launcher` + `at-spi2-registryd`),
|
||||||
|
AT-SPI application registration is gated behind two settings that are
|
||||||
|
*off* by default even though the bus itself is always up:
|
||||||
|
|
||||||
|
```
|
||||||
|
gsettings set org.gnome.desktop.interface toolkit-accessibility true
|
||||||
|
gdbus call --session --dest org.a11y.Bus --object-path /org/a11y/bus \
|
||||||
|
--method org.freedesktop.DBus.Properties.Set org.a11y.Status \
|
||||||
|
ScreenReaderEnabled "<true>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Without both, `Atspi.get_desktop(0)` enumerates **zero** applications even
|
||||||
|
while a probe process is alive, rendering, and actually connected to the
|
||||||
|
AT-SPI D-Bus (confirmed separately via `busctl --address
|
||||||
|
unix:path=$XDG_RUNTIME_DIR/at-spi/bus list`). This is a real environment
|
||||||
|
absence, not a candidate defect, and any later round run in a fresh
|
||||||
|
sandbox/session must redo both steps before trusting a `NOT RUN` verdict on
|
||||||
|
accessibility.
|
||||||
|
|
||||||
|
**Verifier substitution.** The contract's Round 0 evidence rule allows "a
|
||||||
|
small verifier binary using the `atspi` crate, **or an equivalent AT-SPI
|
||||||
|
client**". `a11y-verifier/verify.py` uses `gi.repository.Atspi` (the
|
||||||
|
official AT-SPI2 GObject-introspection binding — the same library behind
|
||||||
|
Orca and Accerciser) instead of the Rust `atspi` crate. This was a
|
||||||
|
deliberate substitution: the Rust crate's async zbus proxy API would have
|
||||||
|
had to be learned from source rather than from any working example, and
|
||||||
|
`pyatspi` was already confirmed reachable on this machine. It is a
|
||||||
|
standalone process, external to every probe, and performs a real tree walk
|
||||||
|
from the AT-SPI registry — it satisfies "a real client query of the tree",
|
||||||
|
not "printing your own struct".
|
||||||
|
|
||||||
|
**C1 (egui).** First-party route, the full chain being
|
||||||
|
`eframe` → `egui-winit` → `accesskit_winit` → `accesskit_unix`: `egui-winit`'s
|
||||||
|
`accesskit` feature is literally `dep:accesskit_winit`
|
||||||
|
(`egui-winit-0.35.0/Cargo.toml:55`), on by default through `eframe` in 0.35, and
|
||||||
|
`accesskit_winit` delegates to the platform crate. So C1 gets the
|
||||||
|
window-lifecycle handling that C3's bypass would have forfeited. No manual
|
||||||
|
wiring was needed. `probe-egui` draws one button; readback: **PASS**. See
|
||||||
|
`round0-evidence/c1-egui-readback.txt`.
|
||||||
|
|
||||||
|
**Carry forward — C1's frame node is unnamed.** C1's readback path is
|
||||||
|
`application:'probe-egui' / frame:'' / button:'EpiphanyProbeButton'`, where C2
|
||||||
|
and C3 both name their frame. The window title does not reach the AT-SPI frame
|
||||||
|
node under `eframe` 0.35's default wiring. **Non-disqualifying** — round 0
|
||||||
|
requires one node with a role *and* a name, and the button carries both — but
|
||||||
|
it is a real gap: a screen-reader user hears an unnamed window. Round 3
|
||||||
|
(accessibility semantics) must check it, since window identity is part of
|
||||||
|
navigation, and it should not be rediscovered there as a surprise.
|
||||||
|
|
||||||
|
**C2 (vello + winit).** Manual `accesskit_winit` route, exactly as named by
|
||||||
|
the contract: `probe-vello` builds the accessibility tree by hand
|
||||||
|
(`accesskit::TreeUpdate`) and drives it through
|
||||||
|
`accesskit_winit::Adapter::with_event_loop_proxy`, wired into the same
|
||||||
|
`winit::application::ApplicationHandler` that owns the vello
|
||||||
|
`RenderContext`/`Renderer`/`Scene` (the vello render pass is real, not a
|
||||||
|
stub — it draws a filled rounded rect every frame, following vello's own
|
||||||
|
`examples/simple` pattern at `linebender/vello@main`). Readback: **PASS**.
|
||||||
|
See `round0-evidence/c2-vello-readback.txt`.
|
||||||
|
|
||||||
|
**C3 (iced) — ROUND-0 RESULT: FAIL. Eliminated at round 0, adjudicated
|
||||||
|
2026-07-28 by coordinator review; no waiver sought or granted.** The initial
|
||||||
|
report recorded this as "PASS with a flagged deviation". That adjudication was
|
||||||
|
wrong and is corrected here. Under pin 14(c) C3's disqualifying set is not
|
||||||
|
passed; keeping it would require an explicit recorded ruling amendment, which
|
||||||
|
was declined.
|
||||||
|
|
||||||
|
**Dual attribution, because the two failures are different in kind.**
|
||||||
|
|
||||||
|
*Candidate limitation — this alone fails the round.* iced 0.14 ships **no
|
||||||
|
accessibility integration at all**: `accesskit` appears in no iced crate
|
||||||
|
manifest (verified across every `iced*` crate in the 0.14 tree). And its
|
||||||
|
**stock runner** exposes to application code neither a
|
||||||
|
`winit::event_loop::ActiveEventLoop` nor a pre-visibility
|
||||||
|
`winit::window::Window`; both appear only inside `iced_winit`'s own private
|
||||||
|
`ApplicationHandler` impl, with `create_window` at `iced_winit-0.14.0/src/lib.rs:350`
|
||||||
|
inside iced's runner. Every `accesskit_winit::Adapter` constructor requires
|
||||||
|
both and panics if the window is already visible.
|
||||||
|
**Scope this to the stock runner, deliberately:** `iced_winit`'s own docs offer
|
||||||
|
a `conversion` module "for users that decide to implement a custom event loop",
|
||||||
|
so a hand-built shell carrying a real route remains **conceivable but
|
||||||
|
unproven** — and it would mean owning the shell. Upstream iced #552 remains
|
||||||
|
open. "Provably closed" applies to the stock runner, not to iced in principle.
|
||||||
|
|
||||||
|
*Probe-design defect — why the first report read PASS.*
|
||||||
|
`accesskit_unix::Adapter::new()` takes **no window handle**, only handlers, and
|
||||||
|
registers with AT-SPI from process identity
|
||||||
|
(`accesskit_unix-0.22.1/src/context.rs`; `app_name()` reads
|
||||||
|
`std::env::current_exe()`). `probe-iced` therefore registered a **hand-built
|
||||||
|
static tree**, decoupled from iced's window, focus, and event lifecycle, with
|
||||||
|
every action discarded — while `view()` happened to label its button
|
||||||
|
identically, which is what made the transcript read as though iced produced it.
|
||||||
|
**Deleting iced from the probe would produce the identical readback.** That is
|
||||||
|
the disqualifying fact: round 0 asks whether the *candidate* exposes a route,
|
||||||
|
and a process-level side channel answers a different question. That
|
||||||
|
`accesskit_unix` sits one layer beneath `accesskit_winit` does not make it a
|
||||||
|
route *for the candidate* — that was the reasoning error, and it is recorded as
|
||||||
|
a probe defect rather than folded into the candidate's result.
|
||||||
|
|
||||||
|
Evidence is preserved rather than rewritten: `round0-evidence/c3-iced-readback.txt`
|
||||||
|
keeps the verifier's factual `READBACK: PASS` under a `ROUND-0 RESULT: FAIL`
|
||||||
|
annotation, so the false positive stays visible alongside its adjudication.
|
||||||
|
|
||||||
|
One consequence survives the corrected verdict and is worth carrying, because
|
||||||
|
it would apply to any future hand-built route: bypassing `accesskit_winit`
|
||||||
|
forfeits that crate's window-lifecycle handling — deactivation on window close,
|
||||||
|
multi-window disambiguation, focus-driven activation. Any real iced integration
|
||||||
|
would have to build and maintain that wiring itself rather than inheriting it,
|
||||||
|
which is a maintenance-surface fact, not merely a round-0 curiosity.
|
||||||
|
|
||||||
|
## Round 0 — desk survey
|
||||||
|
|
||||||
|
All version/date/MSRV figures were fetched live (crates.io API + GitHub
|
||||||
|
`Cargo.toml` at the released tag), not from memory or the contract's
|
||||||
|
2026-07-23 snapshot. See the Round 0 report for the full table.
|
||||||
|
|
||||||
|
Fill-rule documentation, quoted verbatim from source (not inferred from
|
||||||
|
behavior — that is round 1's job):
|
||||||
|
|
||||||
|
- **C1 (`lyon_tessellation` 1.0.20, via `lyon_path` 1.0.19):**
|
||||||
|
`pub enum FillRule { EvenOdd, NonZero }`, with
|
||||||
|
`DEFAULT_FILL_RULE: FillRule = FillRule::EvenOdd` and an explicit
|
||||||
|
`is_in(winding_number)` implementation for both.
|
||||||
|
- **C2 (`peniko` 0.6.1, vello's fill-style type):** `pub enum Fill { NonZero, EvenOdd }`,
|
||||||
|
each with a full doc comment ("All regions where the winding number of
|
||||||
|
the path is not zero will be filled" / "... is odd will be filled").
|
||||||
|
- **C3 (`iced_graphics` 0.14.0, `geometry::fill`):**
|
||||||
|
`pub enum Rule { NonZero, EvenOdd }`, doc pointing at the SVG
|
||||||
|
`fill-rule` spec, default `NonZero`.
|
||||||
|
|
||||||
|
All three document both rules explicitly. No candidate is eliminated on
|
||||||
|
this desk-survey item; round 1 is where it is actually tested.
|
||||||
|
|
@ -0,0 +1,166 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Round 0 accessibility readback verifier.
|
||||||
|
|
||||||
|
An AT-SPI client, independent of any candidate's own process, that walks
|
||||||
|
the live platform accessibility tree (via the AT-SPI2 registry over D-Bus)
|
||||||
|
looking for an accessible node with a given role and name. This is a real
|
||||||
|
client query of the tree, per CONTRACT_EDITOR_T4_SPIKE.md Round 0: "Setting
|
||||||
|
the node in your own process and printing your own struct is NOT a
|
||||||
|
readback."
|
||||||
|
|
||||||
|
Uses gi.repository.Atspi, the official GObject-introspection binding for
|
||||||
|
AT-SPI2 (the same library backing Orca and Accerciser). This is used in
|
||||||
|
place of the `atspi` Rust crate as an "equivalent AT-SPI client" (the
|
||||||
|
contract's own wording) — chosen because its API is stable, documented, and
|
||||||
|
already verified reachable on this machine, rather than reverse-engineering
|
||||||
|
an unfamiliar async zbus proxy API under this round's timebox. That
|
||||||
|
substitution is a named deviation, reported as such.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
verify.py --role "push button" --name "EpiphanyProbeButton" [--app-name SUBSTR] [--max-depth N] [--timeout SECONDS]
|
||||||
|
|
||||||
|
Exit code 0 and prints "READBACK: PASS" with the path from desktop root to
|
||||||
|
the matched node, if found within the timeout. Exit code 1 and prints
|
||||||
|
"READBACK: FAIL" with a dump of what *was* found, if the bus is reachable
|
||||||
|
but no match appears before the timeout. Exit code 2 and prints
|
||||||
|
"READBACK: NOT RUN" if the AT-SPI bus itself cannot be reached at all.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
try:
|
||||||
|
import gi
|
||||||
|
|
||||||
|
gi.require_version("Atspi", "2.0")
|
||||||
|
from gi.repository import Atspi
|
||||||
|
except Exception as exc: # pragma: no cover - environment probe
|
||||||
|
print(f"READBACK: NOT RUN — could not import gi.repository.Atspi: {exc}")
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
|
||||||
|
def walk(node, role, name, app_name_substr, max_depth, path, found, all_seen):
|
||||||
|
if node is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
node_name = node.get_name()
|
||||||
|
except Exception:
|
||||||
|
node_name = "<error>"
|
||||||
|
try:
|
||||||
|
node_role = node.get_role_name()
|
||||||
|
except Exception:
|
||||||
|
node_role = "<error>"
|
||||||
|
all_seen.append(" / ".join(path + [f"{node_role}:{node_name!r}"]))
|
||||||
|
if node_role == role and node_name == name:
|
||||||
|
found.append(list(path) + [f"{node_role}:{node_name!r}"])
|
||||||
|
return
|
||||||
|
if max_depth <= 0:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
n = node.get_child_count()
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
for i in range(n):
|
||||||
|
try:
|
||||||
|
child = node.get_child_at_index(i)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
walk(
|
||||||
|
child,
|
||||||
|
role,
|
||||||
|
name,
|
||||||
|
app_name_substr,
|
||||||
|
max_depth - 1,
|
||||||
|
path + [f"{node_role}:{node_name!r}"],
|
||||||
|
found,
|
||||||
|
all_seen,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--role", required=True)
|
||||||
|
ap.add_argument("--name", required=True)
|
||||||
|
ap.add_argument("--app-name", default=None, help="only descend into apps whose name contains this substring")
|
||||||
|
ap.add_argument("--max-depth", type=int, default=12)
|
||||||
|
ap.add_argument("--timeout", type=float, default=20.0)
|
||||||
|
ap.add_argument("--poll-interval", type=float, default=0.5)
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
Atspi.init()
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"READBACK: NOT RUN — Atspi.init() failed: {exc}")
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
deadline = time.monotonic() + args.timeout
|
||||||
|
last_seen = []
|
||||||
|
attempt = 0
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
attempt += 1
|
||||||
|
try:
|
||||||
|
desktop = Atspi.get_desktop(0)
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"READBACK: NOT RUN — Atspi.get_desktop(0) failed: {exc}")
|
||||||
|
sys.exit(2)
|
||||||
|
if desktop is None:
|
||||||
|
print("READBACK: NOT RUN — Atspi.get_desktop(0) returned None (no AT-SPI registry?)")
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
found = []
|
||||||
|
all_seen = []
|
||||||
|
try:
|
||||||
|
n_apps = desktop.get_child_count()
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"READBACK: NOT RUN — desktop.get_child_count() failed: {exc}")
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
for i in range(n_apps):
|
||||||
|
try:
|
||||||
|
app = desktop.get_child_at_index(i)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if app is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
app_name = app.get_name()
|
||||||
|
except Exception:
|
||||||
|
app_name = "<error>"
|
||||||
|
if args.app_name and args.app_name not in app_name:
|
||||||
|
continue
|
||||||
|
walk(
|
||||||
|
app,
|
||||||
|
args.role,
|
||||||
|
args.name,
|
||||||
|
args.app_name,
|
||||||
|
args.max_depth,
|
||||||
|
[f"desktop"],
|
||||||
|
found,
|
||||||
|
all_seen,
|
||||||
|
)
|
||||||
|
|
||||||
|
last_seen = all_seen
|
||||||
|
if found:
|
||||||
|
print("READBACK: PASS")
|
||||||
|
print(f"attempt: {attempt}, elapsed: {args.timeout - (deadline - time.monotonic()):.2f}s")
|
||||||
|
print("path: " + " / ".join(found[0]))
|
||||||
|
print(f"apps enumerated: {n_apps}")
|
||||||
|
print("full tree (role:name) seen during the matching walk:")
|
||||||
|
for line in all_seen:
|
||||||
|
print(" " + line)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
time.sleep(args.poll_interval)
|
||||||
|
|
||||||
|
print("READBACK: FAIL")
|
||||||
|
print(f"no node with role={args.role!r} name={args.name!r} found within {args.timeout}s ({attempt} attempts)")
|
||||||
|
print("nodes actually seen (role:name), last attempt:")
|
||||||
|
if not last_seen:
|
||||||
|
print(" <none — desktop had 0 matching/enumerable apps>")
|
||||||
|
for line in last_seen:
|
||||||
|
print(" " + line)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
[package]
|
||||||
|
name = "probe-egui"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
|
||||||
|
# Round 0 desk survey + accessibility-route probe for candidate C1
|
||||||
|
# (modern egui + lyon-tessellated meshes). See
|
||||||
|
# CONTRACT_EDITOR_T4_SPIKE.md Round 0. This probe does NOT depend on any
|
||||||
|
# epiphany crate (pin: Round 0 probes must not).
|
||||||
|
#
|
||||||
|
# accesskit is a default eframe feature (egui-winit/accesskit), so this is
|
||||||
|
# egui's first-party integration route, not a manual accesskit_winit wiring.
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
eframe = "0.35"
|
||||||
|
egui = "0.35"
|
||||||
|
# Pulled in only so C1's desk-survey dependency count reflects the actual
|
||||||
|
# candidate pairing named by the contract (pin 16 step 3: "C1 = egui + lyon").
|
||||||
|
# Round 0 does not tessellate anything with it yet (that is round 1); it is
|
||||||
|
# declared here so `cargo tree` measures the real candidate.
|
||||||
|
lyon = "1.0"
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
//! Round 0 accessibility-route probe, candidate C1 (modern egui).
|
||||||
|
//!
|
||||||
|
//! Opens a minimal window containing exactly one interactive widget (a
|
||||||
|
//! button) with a distinctive accessible name. egui's first-party AccessKit
|
||||||
|
//! integration (the `accesskit` feature, on by default in eframe 0.35) is
|
||||||
|
//! relied upon to publish that widget into the platform accessibility tree.
|
||||||
|
//! No manual accesskit_winit wiring is used here — the whole point of this
|
||||||
|
//! probe is to test the first-party route.
|
||||||
|
//!
|
||||||
|
//! This binary does not exit on its own: it is meant to be launched, given
|
||||||
|
//! a moment to register with AT-SPI, queried by the `a11y-verifier` script,
|
||||||
|
//! and then killed by the harness.
|
||||||
|
|
||||||
|
fn main() -> eframe::Result {
|
||||||
|
let options = eframe::NativeOptions {
|
||||||
|
viewport: egui::ViewportBuilder::default()
|
||||||
|
.with_inner_size([320.0, 180.0])
|
||||||
|
.with_title("EpiphanyProbeEgui"),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
eframe::run_native(
|
||||||
|
"EpiphanyProbeEgui",
|
||||||
|
options,
|
||||||
|
Box::new(|_cc| Ok(Box::new(ProbeApp))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ProbeApp;
|
||||||
|
|
||||||
|
impl eframe::App for ProbeApp {
|
||||||
|
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||||
|
egui::CentralPanel::default().show(ui, |ui| {
|
||||||
|
// The one accessible node round 0 needs: a button with a
|
||||||
|
// distinctive, greppable name. egui buttons get AT-SPI role
|
||||||
|
// "push button" and their name from the label text.
|
||||||
|
let _ = ui.button("EpiphanyProbeButton");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
[package]
|
||||||
|
name = "probe-iced"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
|
||||||
|
# Round 0 desk survey + accessibility-route probe for candidate C3 (iced).
|
||||||
|
# See CONTRACT_EDITOR_T4_SPIKE.md Round 0, and the candidate set's own
|
||||||
|
# framing of C3: "AccessKit lists egui among integrated projects and does
|
||||||
|
# not list iced, and iced's upstream accessibility issue (#552) remains
|
||||||
|
# open. A manual accesskit_winit route may exist; round 0 demonstrates it
|
||||||
|
# or C3 fails there."
|
||||||
|
#
|
||||||
|
# ROUND-0 RESULT: FAIL. C3 is eliminated at round 0 (adjudicated 2026-07-28,
|
||||||
|
# no waiver). This crate is retained only as the evidence.
|
||||||
|
#
|
||||||
|
# Candidate limitation — the fact that fails the round: iced 0.14 ships no
|
||||||
|
# accessibility integration at all (`accesskit` appears in no iced crate
|
||||||
|
# manifest), and its STOCK RUNNER exposes to application code neither a
|
||||||
|
# `winit::event_loop::ActiveEventLoop` nor a pre-visibility
|
||||||
|
# `winit::window::Window`; both appear only inside `iced_winit`'s own private
|
||||||
|
# `ApplicationHandler` impl, with `create_window` at `iced_winit-0.14.0/
|
||||||
|
# src/lib.rs:350` inside iced's runner. Every `accesskit_winit::Adapter`
|
||||||
|
# constructor requires them, and panics if the window is already visible.
|
||||||
|
# Scoped deliberately to the stock runner: `iced_winit`'s docs offer a
|
||||||
|
# `conversion` module "for users that decide to implement a custom event
|
||||||
|
# loop", so a hand-built shell is CONCEIVABLE BUT UNPROVEN — and would mean
|
||||||
|
# owning the shell. Upstream iced #552 remains open.
|
||||||
|
#
|
||||||
|
# Probe-design defect — why the first report read PASS: `accesskit_unix::
|
||||||
|
# Adapter::new()` takes no window handle, so this probe registered a
|
||||||
|
# hand-built static tree at process level, decoupled from iced's window,
|
||||||
|
# focus, and event lifecycle, with actions discarded. It reads back cleanly
|
||||||
|
# and proves nothing about iced: DELETING ICED FROM THIS PROBE WOULD PRODUCE
|
||||||
|
# THE IDENTICAL READBACK. `accesskit_unix` being the layer beneath
|
||||||
|
# `accesskit_winit` does not make it a route *for the candidate* — that was
|
||||||
|
# the reasoning error.
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
iced = "0.14"
|
||||||
|
accesskit = "0.24"
|
||||||
|
accesskit_unix = "0.22"
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
//! Round 0 accessibility-route probe, candidate C3 (iced).
|
||||||
|
//!
|
||||||
|
//! **ROUND-0 RESULT: FAIL. This probe does NOT satisfy round 0, and is kept
|
||||||
|
//! only as the evidence of why.** Adjudicated 2026-07-28 by coordinator
|
||||||
|
//! review; see `../DECISIONS.md`.
|
||||||
|
//!
|
||||||
|
//! It drives `accesskit_unix::Adapter` directly, which takes no window handle
|
||||||
|
//! -- only handlers -- and registers with AT-SPI from process identity. The
|
||||||
|
//! tree below is hand-built here: one window node, one button node. It is NOT
|
||||||
|
//! derived from iced's widget tree, does not track iced or real focus changes
|
||||||
|
//! (it sets one static focus at startup and never revises it), and routes no
|
||||||
|
//! actions.
|
||||||
|
//! The button in `view()` carries the same label purely by construction, which
|
||||||
|
//! is what made the transcript read as if iced had produced it.
|
||||||
|
//!
|
||||||
|
//! **Deleting iced from this probe would produce the identical readback.**
|
||||||
|
//! That is the disqualifying fact: round 0 asks whether the *candidate*
|
||||||
|
//! exposes an accessibility route, and a process-level side channel answers a
|
||||||
|
//! different question. A probe that proves nothing about its subject is a
|
||||||
|
//! probe-design defect, and it is recorded as one.
|
||||||
|
//!
|
||||||
|
//! The candidate limitation underneath it is separate and is what actually
|
||||||
|
//! fails the round: iced 0.14 ships no accessibility integration (`accesskit`
|
||||||
|
//! appears in no iced crate manifest), and its **stock runner** hands
|
||||||
|
//! application code neither the winit `ActiveEventLoop` nor a pre-visibility
|
||||||
|
//! `winit::window::Window`, both of which every `accesskit_winit::Adapter`
|
||||||
|
//! constructor requires. Scope that to the stock runner: `iced_winit`'s own
|
||||||
|
//! docs note a `conversion` module "for users that decide to implement a
|
||||||
|
//! custom event loop", so a hand-built shell remains **conceivable but
|
||||||
|
//! unproven** -- and it would mean owning the shell. Upstream iced #552 is
|
||||||
|
//! still open.
|
||||||
|
|
||||||
|
use accesskit::{Action, ActionHandler, ActionRequest, ActivationHandler, DeactivationHandler};
|
||||||
|
use accesskit::{Node as AccessNode, NodeId as AccessNodeId, Role, Tree, TreeId, TreeUpdate};
|
||||||
|
use accesskit_unix::Adapter as UnixAdapter;
|
||||||
|
|
||||||
|
use iced::widget::{button, column, text};
|
||||||
|
use iced::Element;
|
||||||
|
|
||||||
|
const WINDOW_NODE_ID: AccessNodeId = AccessNodeId(0);
|
||||||
|
const BUTTON_NODE_ID: AccessNodeId = AccessNodeId(1);
|
||||||
|
const BUTTON_NAME: &str = "EpiphanyProbeButton";
|
||||||
|
const WINDOW_TITLE: &str = "EpiphanyProbeIced";
|
||||||
|
|
||||||
|
fn build_tree() -> TreeUpdate {
|
||||||
|
let mut root = AccessNode::new(Role::Window);
|
||||||
|
root.set_children(vec![BUTTON_NODE_ID]);
|
||||||
|
root.set_label(WINDOW_TITLE);
|
||||||
|
|
||||||
|
let mut button_node = AccessNode::new(Role::Button);
|
||||||
|
button_node.set_label(BUTTON_NAME);
|
||||||
|
button_node.add_action(Action::Focus);
|
||||||
|
|
||||||
|
TreeUpdate {
|
||||||
|
nodes: vec![(WINDOW_NODE_ID, root), (BUTTON_NODE_ID, button_node)],
|
||||||
|
tree: Some(Tree::new(WINDOW_NODE_ID)),
|
||||||
|
tree_id: TreeId::ROOT,
|
||||||
|
focus: BUTTON_NODE_ID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the full static tree synchronously, so no event-loop plumbing
|
||||||
|
/// is needed to answer AT-SPI's initial tree request.
|
||||||
|
struct StaticActivationHandler;
|
||||||
|
impl ActivationHandler for StaticActivationHandler {
|
||||||
|
fn request_initial_tree(&mut self) -> Option<TreeUpdate> {
|
||||||
|
Some(build_tree())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Round 0 does not exercise actions (that is round 3's job); accept and
|
||||||
|
/// discard.
|
||||||
|
struct NoopActionHandler;
|
||||||
|
impl ActionHandler for NoopActionHandler {
|
||||||
|
fn do_action(&mut self, _request: ActionRequest) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NoopDeactivationHandler;
|
||||||
|
impl DeactivationHandler for NoopDeactivationHandler {
|
||||||
|
fn deactivate_accessibility(&mut self) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ProbeState {
|
||||||
|
// Held for its lifetime, not read again: dropping it would tear down
|
||||||
|
// the AT-SPI registration.
|
||||||
|
_adapter: UnixAdapter,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ProbeState {
|
||||||
|
fn default() -> Self {
|
||||||
|
let mut adapter = UnixAdapter::new(
|
||||||
|
StaticActivationHandler,
|
||||||
|
NoopActionHandler,
|
||||||
|
NoopDeactivationHandler,
|
||||||
|
);
|
||||||
|
// Force the tree to materialize now rather than waiting for an
|
||||||
|
// AT-SPI client's first request, and mark it focused so a reading
|
||||||
|
// client sees a live, focused application rather than an inert one.
|
||||||
|
adapter.update_if_active(build_tree);
|
||||||
|
adapter.update_window_focus_state(true);
|
||||||
|
Self { _adapter: adapter }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
enum Message {
|
||||||
|
Noop,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(_state: &mut ProbeState, _message: Message) {}
|
||||||
|
|
||||||
|
fn view(_state: &ProbeState) -> Element<'_, Message> {
|
||||||
|
column![
|
||||||
|
text(WINDOW_TITLE),
|
||||||
|
button(BUTTON_NAME).on_press(Message::Noop),
|
||||||
|
]
|
||||||
|
.padding(20)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> iced::Result {
|
||||||
|
iced::run(update, view)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
[package]
|
||||||
|
name = "probe-vello"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
|
||||||
|
|
||||||
|
# Round 0 desk survey + accessibility-route probe for candidate C2
|
||||||
|
# (vello behind a winit shell). See CONTRACT_EDITOR_T4_SPIKE.md Round 0.
|
||||||
|
# This probe does NOT depend on any epiphany crate.
|
||||||
|
#
|
||||||
|
# vello ships no widget toolkit and no accessibility integration of its own
|
||||||
|
# (contract, candidate set: "the open questions are windowing and UI chrome
|
||||||
|
# ... accessibility wiring, and maturity"), so this is a MANUAL
|
||||||
|
# accesskit_winit route: we build the accessibility tree by hand and drive
|
||||||
|
# accesskit_winit::Adapter ourselves, wired into the same winit event loop
|
||||||
|
# that drives the vello RenderContext/Renderer/Scene.
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
vello = "0.9"
|
||||||
|
winit = "0.30"
|
||||||
|
accesskit = "0.24"
|
||||||
|
accesskit_winit = "0.33"
|
||||||
|
pollster = "0.4"
|
||||||
|
anyhow = "1"
|
||||||
|
|
@ -0,0 +1,263 @@
|
||||||
|
//! Round 0 accessibility-route probe, candidate C2 (vello + winit).
|
||||||
|
//!
|
||||||
|
//! vello ships no widget toolkit and no accessibility integration of its
|
||||||
|
//! own, so unlike C1 this is a MANUAL accesskit_winit wiring: the
|
||||||
|
//! accessibility tree (one window node containing one button node with a
|
||||||
|
//! distinctive name) is built by hand and pushed through
|
||||||
|
//! `accesskit_winit::Adapter`, driven from the same winit
|
||||||
|
//! `ApplicationHandler` that owns the vello `RenderContext`/`Renderer`/
|
||||||
|
//! `Scene`. This mirrors AccessKit's own upstream `winit` adapter example
|
||||||
|
//! (`adapters/winit/examples/simple.rs` at AccessKit/accesskit@main),
|
||||||
|
//! adapted to also drive a real vello render pass so the probe is a
|
||||||
|
//! genuine instance of "vello behind a winit shell", not accessibility
|
||||||
|
//! wiring alone.
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use accesskit::{
|
||||||
|
Action, Node as AccessNode, NodeId as AccessNodeId, Role, Tree, TreeId, TreeUpdate,
|
||||||
|
};
|
||||||
|
use accesskit_winit::{Adapter, Event as AccessKitEvent, WindowEvent as AccessKitWindowEvent};
|
||||||
|
|
||||||
|
use vello::kurbo::{Affine, RoundedRect};
|
||||||
|
use vello::peniko::Color;
|
||||||
|
use vello::util::{RenderContext, RenderSurface};
|
||||||
|
use vello::wgpu::{self, CurrentSurfaceTexture};
|
||||||
|
use vello::{AaConfig, Renderer, RendererOptions, Scene};
|
||||||
|
|
||||||
|
use winit::application::ApplicationHandler;
|
||||||
|
use winit::dpi::LogicalSize;
|
||||||
|
use winit::event::WindowEvent;
|
||||||
|
use winit::event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy};
|
||||||
|
use winit::window::{Window, WindowId};
|
||||||
|
|
||||||
|
const WINDOW_TITLE: &str = "EpiphanyProbeVello";
|
||||||
|
const WINDOW_NODE_ID: AccessNodeId = AccessNodeId(0);
|
||||||
|
const BUTTON_NODE_ID: AccessNodeId = AccessNodeId(1);
|
||||||
|
const BUTTON_NAME: &str = "EpiphanyProbeButton";
|
||||||
|
|
||||||
|
fn build_button() -> AccessNode {
|
||||||
|
let mut node = AccessNode::new(Role::Button);
|
||||||
|
node.set_label(BUTTON_NAME);
|
||||||
|
node.add_action(Action::Focus);
|
||||||
|
node
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_root() -> AccessNode {
|
||||||
|
let mut node = AccessNode::new(Role::Window);
|
||||||
|
node.set_children(vec![BUTTON_NODE_ID]);
|
||||||
|
node.set_label(WINDOW_TITLE);
|
||||||
|
node
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_initial_tree() -> TreeUpdate {
|
||||||
|
TreeUpdate {
|
||||||
|
nodes: vec![
|
||||||
|
(WINDOW_NODE_ID, build_root()),
|
||||||
|
(BUTTON_NODE_ID, build_button()),
|
||||||
|
],
|
||||||
|
tree: Some(Tree::new(WINDOW_NODE_ID)),
|
||||||
|
tree_id: TreeId::ROOT,
|
||||||
|
focus: BUTTON_NODE_ID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RenderState {
|
||||||
|
Active {
|
||||||
|
surface: Box<RenderSurface<'static>>,
|
||||||
|
valid_surface: bool,
|
||||||
|
window: Arc<Window>,
|
||||||
|
adapter: Adapter,
|
||||||
|
},
|
||||||
|
Suspended(Option<Arc<Window>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ProbeApp {
|
||||||
|
event_loop_proxy: EventLoopProxy<AccessKitEvent>,
|
||||||
|
context: RenderContext,
|
||||||
|
renderers: Vec<Option<Renderer>>,
|
||||||
|
state: RenderState,
|
||||||
|
scene: Scene,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProbeApp {
|
||||||
|
fn new(event_loop_proxy: EventLoopProxy<AccessKitEvent>) -> Self {
|
||||||
|
Self {
|
||||||
|
event_loop_proxy,
|
||||||
|
context: RenderContext::new(),
|
||||||
|
renderers: vec![],
|
||||||
|
state: RenderState::Suspended(None),
|
||||||
|
scene: Scene::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApplicationHandler<AccessKitEvent> for ProbeApp {
|
||||||
|
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||||
|
let RenderState::Suspended(cached_window) = &mut self.state else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let window = cached_window.take().unwrap_or_else(|| {
|
||||||
|
let attr = Window::default_attributes()
|
||||||
|
.with_inner_size(LogicalSize::new(320, 180))
|
||||||
|
.with_title(WINDOW_TITLE);
|
||||||
|
Arc::new(event_loop.create_window(attr).unwrap())
|
||||||
|
});
|
||||||
|
|
||||||
|
// Manual accesskit_winit wiring: one adapter per window, driven by
|
||||||
|
// the same event loop proxy that feeds this ApplicationHandler.
|
||||||
|
let adapter =
|
||||||
|
Adapter::with_event_loop_proxy(event_loop, &window, self.event_loop_proxy.clone());
|
||||||
|
|
||||||
|
let size = window.inner_size();
|
||||||
|
let surface_future = self.context.create_surface(
|
||||||
|
window.clone(),
|
||||||
|
size.width,
|
||||||
|
size.height,
|
||||||
|
wgpu::PresentMode::AutoVsync,
|
||||||
|
);
|
||||||
|
let surface = pollster::block_on(surface_future).expect("Error creating vello surface");
|
||||||
|
|
||||||
|
self.renderers
|
||||||
|
.resize_with(self.context.devices.len(), || None);
|
||||||
|
self.renderers[surface.dev_id].get_or_insert_with(|| {
|
||||||
|
Renderer::new(
|
||||||
|
&self.context.devices[surface.dev_id].device,
|
||||||
|
RendererOptions::default(),
|
||||||
|
)
|
||||||
|
.expect("Couldn't create vello renderer")
|
||||||
|
});
|
||||||
|
|
||||||
|
self.state = RenderState::Active {
|
||||||
|
surface: Box::new(surface),
|
||||||
|
valid_surface: true,
|
||||||
|
window,
|
||||||
|
adapter,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
|
||||||
|
if let RenderState::Active { window, .. } = &self.state {
|
||||||
|
self.state = RenderState::Suspended(Some(window.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_event(
|
||||||
|
&mut self,
|
||||||
|
event_loop: &ActiveEventLoop,
|
||||||
|
window_id: WindowId,
|
||||||
|
event: WindowEvent,
|
||||||
|
) {
|
||||||
|
let (surface, valid_surface, window, adapter) = match &mut self.state {
|
||||||
|
RenderState::Active {
|
||||||
|
surface,
|
||||||
|
valid_surface,
|
||||||
|
window,
|
||||||
|
adapter,
|
||||||
|
} if window.id() == window_id => (surface, valid_surface, window, adapter),
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
adapter.process_event(window, &event);
|
||||||
|
|
||||||
|
match event {
|
||||||
|
WindowEvent::CloseRequested => event_loop.exit(),
|
||||||
|
WindowEvent::Resized(size) => {
|
||||||
|
if size.width != 0 && size.height != 0 {
|
||||||
|
self.context
|
||||||
|
.resize_surface(surface, size.width, size.height);
|
||||||
|
*valid_surface = true;
|
||||||
|
} else {
|
||||||
|
*valid_surface = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WindowEvent::RedrawRequested => {
|
||||||
|
if !*valid_surface {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.scene.reset();
|
||||||
|
let rect = RoundedRect::new(20.0, 20.0, 200.0, 64.0, 6.0);
|
||||||
|
self.scene.fill(
|
||||||
|
vello::peniko::Fill::NonZero,
|
||||||
|
Affine::IDENTITY,
|
||||||
|
Color::new([0.7, 0.85, 1.0, 1.0]),
|
||||||
|
None,
|
||||||
|
&rect,
|
||||||
|
);
|
||||||
|
|
||||||
|
let width = surface.config.width;
|
||||||
|
let height = surface.config.height;
|
||||||
|
let device_handle = &self.context.devices[surface.dev_id];
|
||||||
|
|
||||||
|
self.renderers[surface.dev_id]
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.render_to_texture(
|
||||||
|
&device_handle.device,
|
||||||
|
&device_handle.queue,
|
||||||
|
&self.scene,
|
||||||
|
&surface.target_view,
|
||||||
|
&vello::RenderParams {
|
||||||
|
base_color: Color::new([0.05, 0.05, 0.08, 1.0]),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
antialiasing_method: AaConfig::Msaa16,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("failed to render");
|
||||||
|
|
||||||
|
let surface_texture = match surface.surface.get_current_texture() {
|
||||||
|
CurrentSurfaceTexture::Success(t) => t,
|
||||||
|
CurrentSurfaceTexture::Outdated | CurrentSurfaceTexture::Suboptimal(_) => {
|
||||||
|
self.context.configure_surface(surface);
|
||||||
|
window.request_redraw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CurrentSurfaceTexture::Occluded | CurrentSurfaceTexture::Timeout => {
|
||||||
|
window.request_redraw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CurrentSurfaceTexture::Lost => panic!("Surface was lost"),
|
||||||
|
CurrentSurfaceTexture::Validation => panic!("Validation error getting surface"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut encoder =
|
||||||
|
device_handle
|
||||||
|
.device
|
||||||
|
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||||
|
label: Some("Surface Blit"),
|
||||||
|
});
|
||||||
|
surface.blitter.copy(
|
||||||
|
&device_handle.device,
|
||||||
|
&mut encoder,
|
||||||
|
&surface.target_view,
|
||||||
|
&surface_texture
|
||||||
|
.texture
|
||||||
|
.create_view(&wgpu::TextureViewDescriptor::default()),
|
||||||
|
);
|
||||||
|
device_handle.queue.submit([encoder.finish()]);
|
||||||
|
surface_texture.present();
|
||||||
|
device_handle.device.poll(wgpu::PollType::Poll).unwrap();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn user_event(&mut self, _event_loop: &ActiveEventLoop, user_event: AccessKitEvent) {
|
||||||
|
let RenderState::Active { adapter, .. } = &mut self.state else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let AccessKitWindowEvent::InitialTreeRequested = user_event.window_event {
|
||||||
|
adapter.update_if_active(build_initial_tree);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
let event_loop = EventLoop::<AccessKitEvent>::with_user_event().build()?;
|
||||||
|
let mut app = ProbeApp::new(event_loop.create_proxy());
|
||||||
|
event_loop.run_app(&mut app)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
|
||||||
|
(process:2403337): dbind-WARNING **: 18:07:00.684: AT-SPI: Error in GetItems, sender=:1.9, error=Unknown object '/org/a11y/atspi/cache'
|
||||||
|
READBACK: PASS
|
||||||
|
attempt: 1, elapsed: 0.00s
|
||||||
|
path: desktop / application:'probe-egui' / frame:'' / button:'EpiphanyProbeButton'
|
||||||
|
apps enumerated: 1
|
||||||
|
full tree (role:name) seen during the matching walk:
|
||||||
|
desktop / application:'probe-egui'
|
||||||
|
desktop / application:'probe-egui' / frame:''
|
||||||
|
desktop / application:'probe-egui' / frame:'' / button:'EpiphanyProbeButton'
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
READBACK: PASS
|
||||||
|
attempt: 1, elapsed: 0.00s
|
||||||
|
path: desktop / application:'probe-vello' / frame:'EpiphanyProbeVello' / button:'EpiphanyProbeButton'
|
||||||
|
apps enumerated: 2
|
||||||
|
full tree (role:name) seen during the matching walk:
|
||||||
|
desktop / application:'probe-vello'
|
||||||
|
desktop / application:'probe-vello' / frame:'EpiphanyProbeVello'
|
||||||
|
desktop / application:'probe-vello' / frame:'EpiphanyProbeVello' / button:'EpiphanyProbeButton'
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
ROUND-0 RESULT: FAIL
|
||||||
|
ATTRIBUTION: candidate limitation -- no qualifying iced 0.14 accessibility route demonstrated.
|
||||||
|
RAW NON-QUALIFYING SIDE-CHANNEL READBACK: PASS
|
||||||
|
|
||||||
|
Adjudicated 2026-07-28 by coordinator review. The raw verifier output below is
|
||||||
|
factually correct and is preserved unedited: an AT-SPI tree WAS registered and
|
||||||
|
read back. It does not qualify, because the tree is hand-built by the probe and
|
||||||
|
registered process-level via accesskit_unix, independent of iced -- deleting
|
||||||
|
iced from the probe would produce the identical readback. Round 0 asks whether
|
||||||
|
the CANDIDATE exposes an accessibility route; this answers a different question.
|
||||||
|
|
||||||
|
--- raw verifier output, unedited ---
|
||||||
|
READBACK: PASS
|
||||||
|
attempt: 1, elapsed: 0.02s
|
||||||
|
path: desktop / application:'probe-iced' / frame:'EpiphanyProbeIced' / button:'EpiphanyProbeButton'
|
||||||
|
apps enumerated: 2
|
||||||
|
full tree (role:name) seen during the matching walk:
|
||||||
|
desktop / application:'probe-iced'
|
||||||
|
desktop / application:'probe-iced' / frame:'EpiphanyProbeIced'
|
||||||
|
desktop / application:'probe-iced' / frame:'EpiphanyProbeIced' / button:'EpiphanyProbeButton'
|
||||||
Loading…
Reference in New Issue