Skip to main content

max / makeover-immediate

A token says what it is, instead of being painted and nothing else The fourth instance of the same defect and the widest: `token` allocates its rect and paints its text straight onto it, so nothing about it reached the accessibility tree at all. An interactive chip was a control a mouse could press and nothing else could find, and a badge was a word on screen nobody could read out. Painting a thing is not saying it. The filter panel's twenty-four key pills are the site that found it -- a whole axis of filtering, absent for anyone not using a pointer. A chip announces as a button and carries `latched` as `selected`, which is what a screen reader says as pressed and is exactly what latched means. A badge announces as the label it is and claims no press. Stays in the unpublished 0.34.0 rather than taking a number of its own: nothing has consumed that release yet, and the three fixes in it are one finding read four ways.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-22 18:15 UTC
Signed with PGP, not checked
Commit: 3f17c538144359a2732f93cb9acac1cf4c2bdd92
Parent: af9a551
1 file changed, +117 insertions, -0 deletions
M src/widget.rs +117
@@ -187,6 +187,31 @@
187 187 }
188 188 ui.painter()
189 189 .galley(rect.center() - galley.size() / 2.0, galley, ink);
190 +
191 + // Say what was drawn, because painting it says nothing.
192 + //
193 + // A token allocates its rect and paints the text straight onto it, so
194 + // nothing reached the accessibility tree at all until 2026-08-22: an
195 + // interactive chip was a control a mouse could press and a screen reader
196 + // could not find, and a badge was text nobody could read out. The filter
197 + // panel's twenty-four key pills were the site -- a whole way of filtering,
198 + // absent.
199 + //
200 + // A chip that latches says so through `selected`, which is what a screen
201 + // reader announces as pressed. That is `latched`'s whole meaning: the key
202 + // is held down.
203 + let role = if kind.interactive() {
204 + egui::WidgetType::Button
205 + } else {
206 + egui::WidgetType::Label
207 + };
208 + response.widget_info(|| {
209 + let mut info = egui::WidgetInfo::labeled(role, ui.is_enabled(), label);
210 + if kind.interactive() {
211 + info.selected = Some(latched);
212 + }
213 + info
214 + });
190 215 response
191 216 }
192 217
@@ -256,6 +281,98 @@
256 281 mod tests {
257 282 use super::*;
258 283
284 + /// What the accessibility tree says a widget drew.
285 + fn announced(
286 + draw: impl FnMut(&mut Ui),
287 + ) -> Vec<(
288 + egui::accesskit::Role,
289 + String,
290 + Option<egui::accesskit::Toggled>,
291 + )> {
292 + let ctx = egui::Context::default();
293 + ctx.enable_accesskit();
294 + let mut draw = draw;
295 + let input = || egui::RawInput {
296 + screen_rect: Some(egui::Rect::from_min_size(
297 + egui::Pos2::ZERO,
298 + egui::vec2(600.0, 400.0),
299 + )),
300 + ..Default::default()
301 + };
302 + let _ = ctx.run_ui(input(), &mut draw);
303 + let out = ctx.run_ui(input(), &mut draw);
304 + out.platform_output
305 + .accesskit_update
306 + .expect("accesskit is on")
307 + .nodes
308 + .iter()
309 + .map(|(_, node)| {
310 + (
311 + node.role(),
312 + node.label()
313 + .or_else(|| node.value())
314 + .unwrap_or_default()
315 + .to_owned(),
316 + node.toggled(),
317 + )
318 + })
319 + .collect()
320 + }
321 +
322 + #[test]
323 + fn a_chip_is_announced_as_a_control_and_says_whether_it_is_held() {
324 + // A token paints its own text onto its own rect, so before 2026-08-22
325 + // it reached the tree as nothing: pressable by a mouse and invisible to
326 + // everything else.
327 + let p = palette();
328 + let style = WidgetStyle::default();
329 + let drawn = announced(|ui| {
330 + token(
331 + ui,
332 + "C#",
333 + Token::Chip { removable: false },
334 + Tone::Neutral,
335 + true,
336 + &p,
337 + &style,
338 + );
339 + });
340 +
341 + let chip = drawn
342 + .iter()
343 + .find(|(role, name, _)| *role == egui::accesskit::Role::Button && name == "C#")
344 + .unwrap_or_else(|| panic!("the chip is not in the tree: {drawn:?}"));
345 + assert_eq!(
346 + chip.2,
347 + Some(egui::accesskit::Toggled::True),
348 + "a latched chip is held down and says so: {drawn:?}"
349 + );
350 + }
351 +
352 + #[test]
353 + fn a_badge_is_announced_as_the_text_it_is() {
354 + // Not a control, and not nothing either: a badge is a word on the
355 + // screen and painting it is not the same as saying it.
356 + let p = palette();
357 + let style = WidgetStyle::default();
358 + let drawn = announced(|ui| {
359 + token(ui, "wav", Token::Badge, Tone::Neutral, false, &p, &style);
360 + });
361 +
362 + assert!(
363 + drawn
364 + .iter()
365 + .any(|(role, name, _)| *role == egui::accesskit::Role::Label && name == "wav"),
366 + "{drawn:?}"
367 + );
368 + assert!(
369 + !drawn
370 + .iter()
371 + .any(|(role, _, _)| *role == egui::accesskit::Role::Button),
372 + "a badge answers nothing and must not claim to: {drawn:?}"
373 + );
374 + }
375 +
259 376 fn palette() -> Palette {
260 377 use egui::Color32;
261 378 Palette {