max / makeover-immediate
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
1 file changed,
+117 insertions,
-0 deletions
| @@ -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 { |