Another project
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(ui): widgets emit a11y nodes

Lewis: May this revision serve well! <lu5a@proton.me>

+605 -90
+21 -1
crates/bone-ui/src/widgets/button.rs
··· 1 1 use serde::{Deserialize, Serialize}; 2 2 3 + use crate::a11y::{AccessNode, Role}; 3 4 use crate::frame::{FrameCtx, InteractDeclaration}; 4 5 use crate::hit_test::{Interaction, Sense}; 5 6 use crate::layout::LayoutRect; ··· 94 95 let interaction = ctx.interact( 95 96 InteractDeclaration::new(button.id, button.rect, Sense::INTERACTIVE) 96 97 .focusable(interactive) 97 - .disabled(!interactive), 98 + .disabled(!interactive) 99 + .a11y( 100 + AccessNode::new(Role::Button) 101 + .with_label(button.label) 102 + .with_disabled(!interactive), 103 + ), 98 104 ); 99 105 let live_focused = ctx.is_focused(button.id); 100 106 let activated_via_pointer = interactive && interaction.click(); ··· 312 318 activations: &mut Vec<bool>| { 313 319 hits.clear(); 314 320 { 321 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 315 322 let mut ctx = FrameCtx::new( 316 323 theme.clone(), 317 324 snap, ··· 320 327 StringTable::empty(), 321 328 hits, 322 329 state, 330 + &mut a11y, 323 331 ); 324 332 let response = show_button(&mut ctx, button); 325 333 activations.push(response.activated); ··· 410 418 )]); 411 419 let button = Button::new(id("ok"), rect(), LABEL, ButtonVariant::Primary); 412 420 let response = { 421 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 413 422 let mut ctx = FrameCtx::new( 414 423 theme, 415 424 &mut input, ··· 418 427 StringTable::empty(), 419 428 &mut hits, 420 429 &state, 430 + &mut a11y, 421 431 ); 422 432 show_button(&mut ctx, button) 423 433 }; ··· 437 447 )]); 438 448 let button = Button::new(id("ok"), rect(), LABEL, ButtonVariant::Primary); 439 449 let response = { 450 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 440 451 let mut ctx = FrameCtx::new( 441 452 theme, 442 453 &mut input, ··· 445 456 StringTable::empty(), 446 457 &mut hits, 447 458 &state, 459 + &mut a11y, 448 460 ); 449 461 show_button(&mut ctx, button) 450 462 }; ··· 463 475 input.keys_pressed.push(event); 464 476 let button = Button::new(id("ok"), rect(), LABEL, ButtonVariant::Primary); 465 477 let response = { 478 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 466 479 let mut ctx = FrameCtx::new( 467 480 theme, 468 481 &mut input, ··· 471 484 StringTable::empty(), 472 485 &mut hits, 473 486 &state, 487 + &mut a11y, 474 488 ); 475 489 show_button(&mut ctx, button) 476 490 }; ··· 497 511 let (mut input, mut focus) = focused_input_with(vec![other]); 498 512 let button = Button::new(id("ok"), rect(), LABEL, ButtonVariant::Primary); 499 513 let _ = { 514 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 500 515 let mut ctx = FrameCtx::new( 501 516 theme, 502 517 &mut input, ··· 505 520 StringTable::empty(), 506 521 &mut hits, 507 522 &state, 523 + &mut a11y, 508 524 ); 509 525 show_button(&mut ctx, button) 510 526 }; ··· 549 565 let (mut input, mut focus) = focused_input_with(vec![tab]); 550 566 let button = Button::new(id("ok"), rect(), LABEL, ButtonVariant::Primary); 551 567 let response = { 568 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 552 569 let mut ctx = FrameCtx::new( 553 570 theme, 554 571 &mut input, ··· 557 574 StringTable::empty(), 558 575 &mut hits, 559 576 &state, 577 + &mut a11y, 560 578 ); 561 579 show_button(&mut ctx, button) 562 580 }; ··· 582 600 focus.end_frame(); 583 601 let button = Button::new(id("ok"), rect(), LABEL, ButtonVariant::Primary); 584 602 let response = { 603 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 585 604 let mut ctx = FrameCtx::new( 586 605 theme, 587 606 &mut input, ··· 590 609 StringTable::empty(), 591 610 &mut hits, 592 611 &state, 612 + &mut a11y, 593 613 ); 594 614 show_button(&mut ctx, button) 595 615 };
+21 -1
crates/bone-ui/src/widgets/checkbox.rs
··· 1 1 use serde::Serialize; 2 2 3 + use crate::a11y::{AccessNode, Role, Toggled}; 3 4 use crate::frame::{FrameCtx, InteractDeclaration}; 4 5 use crate::hit_test::{Interaction, Sense}; 5 6 use crate::layout::LayoutRect; ··· 75 76 #[must_use] 76 77 pub fn show_checkbox(ctx: &mut FrameCtx<'_>, checkbox: Checkbox) -> CheckboxResponse { 77 78 let interactive = !checkbox.disabled; 79 + let toggled_state = match checkbox.state { 80 + CheckboxState::Unchecked => Toggled::False, 81 + CheckboxState::Checked => Toggled::True, 82 + CheckboxState::Indeterminate => Toggled::Mixed, 83 + }; 78 84 let interaction = ctx.interact( 79 85 InteractDeclaration::new(checkbox.id, checkbox.rect, Sense::INTERACTIVE) 80 86 .focusable(interactive) 81 87 .disabled(!interactive) 82 - .active(checkbox.state.is_active()), 88 + .active(checkbox.state.is_active()) 89 + .a11y( 90 + AccessNode::new(Role::CheckBox) 91 + .with_label(checkbox.label) 92 + .with_disabled(!interactive) 93 + .with_toggled(toggled_state), 94 + ), 83 95 ); 84 96 let live_focused = ctx.is_focused(checkbox.id); 85 97 let toggled = ··· 187 199 )); 188 200 let checkbox = Checkbox::new(id_widget(), rect(), LABEL, CheckboxState::Unchecked); 189 201 let response = { 202 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 190 203 let mut ctx = FrameCtx::new( 191 204 theme, 192 205 &mut input, ··· 195 208 StringTable::empty(), 196 209 &mut hits, 197 210 &state, 211 + &mut a11y, 198 212 ); 199 213 show_checkbox(&mut ctx, checkbox) 200 214 }; ··· 216 230 hits.clear(); 217 231 let checkbox = Checkbox::new(id_widget(), rect(), LABEL, current); 218 232 let response = { 233 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 219 234 let mut ctx = FrameCtx::new( 220 235 theme.clone(), 221 236 &mut input, ··· 224 239 StringTable::empty(), 225 240 &mut hits, 226 241 &state, 242 + &mut a11y, 227 243 ); 228 244 show_checkbox(&mut ctx, checkbox) 229 245 }; ··· 243 259 let mut input = InputSnapshot::idle(FrameInstant::ZERO); 244 260 let checkbox = Checkbox::new(id_widget(), rect(), LABEL, CheckboxState::Indeterminate); 245 261 let response = { 262 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 246 263 let mut ctx = FrameCtx::new( 247 264 theme, 248 265 &mut input, ··· 251 268 StringTable::empty(), 252 269 &mut hits, 253 270 &state, 271 + &mut a11y, 254 272 ); 255 273 show_checkbox(&mut ctx, checkbox) 256 274 }; ··· 282 300 let checkbox = 283 301 Checkbox::new(id_widget(), rect(), LABEL, CheckboxState::Unchecked).disabled(true); 284 302 let response = { 303 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 285 304 let mut ctx = FrameCtx::new( 286 305 theme, 287 306 &mut input, ··· 290 309 StringTable::empty(), 291 310 &mut hits, 292 311 &state, 312 + &mut a11y, 293 313 ); 294 314 show_checkbox(&mut ctx, checkbox) 295 315 };
+32 -25
crates/bone-ui/src/widgets/dialog.rs
··· 1 + use crate::a11y::{AccessNode, Role}; 1 2 use crate::focus::FocusScopeKind; 2 3 use crate::frame::FrameCtx; 3 4 use crate::input::NamedKey; ··· 10 11 use super::keys::{TakeKey, take_key}; 11 12 use super::paint::{LabelText, WidgetPaint}; 12 13 13 - #[derive(Copy, Clone, Debug, PartialEq, Eq)] 14 - pub enum BackdropStyle { 15 - Scrim, 16 - None, 17 - } 18 - 19 14 #[derive(Copy, Clone, Debug, PartialEq)] 20 15 pub struct Modal { 21 16 pub id: WidgetId, 22 17 pub viewport: LayoutRect, 23 18 pub size: LayoutSize, 24 - pub backdrop: BackdropStyle, 19 + pub label: StringKey, 25 20 } 26 21 27 22 impl Modal { 28 23 #[must_use] 29 - pub const fn new(id: WidgetId, viewport: LayoutRect, size: LayoutSize) -> Self { 24 + pub const fn new( 25 + id: WidgetId, 26 + viewport: LayoutRect, 27 + size: LayoutSize, 28 + label: StringKey, 29 + ) -> Self { 30 30 Self { 31 31 id, 32 32 viewport, 33 33 size, 34 - backdrop: BackdropStyle::Scrim, 34 + label, 35 35 } 36 - } 37 - 38 - #[must_use] 39 - pub const fn backdrop(self, backdrop: BackdropStyle) -> Self { 40 - Self { backdrop, ..self } 41 36 } 42 37 } 43 38 ··· 54 49 F: FnOnce(&mut FrameCtx<'_>, LayoutRect, &mut Vec<WidgetPaint>) -> R, 55 50 { 56 51 ctx.focus.push_scope(FocusScopeKind::Modal); 57 - let mut paint = Vec::new(); 58 - if matches!(modal.backdrop, BackdropStyle::Scrim) { 59 - paint.push(WidgetPaint::Surface { 60 - rect: modal.viewport, 61 - fill: Color::TRANSPARENT.with_alpha(0.45), 62 - border: None, 63 - radius: ctx.theme().radius.none, 64 - elevation: None, 65 - }); 66 - } 52 + let mut paint = vec![WidgetPaint::Surface { 53 + rect: modal.viewport, 54 + fill: Color::TRANSPARENT.with_alpha(0.45), 55 + border: None, 56 + radius: ctx.theme().radius.none, 57 + elevation: None, 58 + }]; 67 59 let body_rect = center_rect(modal.viewport, modal.size); 68 60 paint.push(WidgetPaint::Surface { 69 61 rect: body_rect, ··· 75 67 radius: ctx.theme().radius.md, 76 68 elevation: Some(ctx.theme().elevation.level1), 77 69 }); 70 + ctx.a11y.push( 71 + modal.id, 72 + body_rect, 73 + AccessNode::new(Role::Dialog).with_label(modal.label), 74 + ); 78 75 let dismissed = take_key(ctx.input, &[TakeKey::named(NamedKey::Escape)]).is_some(); 79 76 let extras = body(ctx, body_rect, &mut paint); 80 77 ctx.focus.pop_scope(); ··· 193 190 let strip_rect = button_strip_rect_of(surface); 194 191 let buttons = dialog.buttons; 195 192 let title = dialog.title; 196 - let modal = Modal::new(dialog.id, dialog.viewport, dialog.size); 193 + let modal = Modal::new(dialog.id, dialog.viewport, dialog.size, dialog.title); 197 194 let (modal_response, (activated, extras)) = show_modal(ctx, modal, |ctx, _surface, paint| { 198 195 paint.push(WidgetPaint::Label { 199 196 rect: title_label_rect(title_rect), ··· 464 461 let table = HotkeyTable::new(); 465 462 let mut hits = HitFrame::new(); 466 463 let response = { 464 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 467 465 let mut ctx = FrameCtx::new( 468 466 theme, 469 467 snap, ··· 472 470 StringTable::empty(), 473 471 &mut hits, 474 472 prev, 473 + &mut a11y, 475 474 ); 476 475 let (response, ()) = show_dialog( 477 476 &mut ctx, ··· 503 502 ModifierMask::NONE, 504 503 )); 505 504 let response = { 505 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 506 506 let mut ctx = FrameCtx::new( 507 507 theme, 508 508 &mut snap, ··· 511 511 StringTable::empty(), 512 512 &mut hits, 513 513 &prev, 514 + &mut a11y, 514 515 ); 515 516 let (response, ()) = show_modal( 516 517 &mut ctx, ··· 518 519 modal_id(), 519 520 viewport(), 520 521 LayoutSize::new(LayoutPx::new(300.0), LayoutPx::new(200.0)), 522 + StringKey::new("test.modal"), 521 523 ), 522 524 |_ctx, _body_rect, _paint| {}, 523 525 ); ··· 546 548 let prev = HitState::new(); 547 549 let mut snap = InputSnapshot::idle(FrameInstant::ZERO); 548 550 { 551 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 549 552 let mut ctx = FrameCtx::new( 550 553 theme, 551 554 &mut snap, ··· 554 557 StringTable::empty(), 555 558 &mut hits, 556 559 &prev, 560 + &mut a11y, 557 561 ); 558 562 let (_response, ()) = show_modal( 559 563 &mut ctx, ··· 561 565 modal_id(), 562 566 viewport(), 563 567 LayoutSize::new(LayoutPx::new(300.0), LayoutPx::new(200.0)), 568 + StringKey::new("test.modal"), 564 569 ), 565 570 |_ctx, _body_rect, _paint| {}, 566 571 ); ··· 594 599 .for_each(|mut snap| { 595 600 let mut hits = HitFrame::new(); 596 601 let response = { 602 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 597 603 let mut ctx = FrameCtx::new( 598 604 theme.clone(), 599 605 &mut snap, ··· 602 608 StringTable::empty(), 603 609 &mut hits, 604 610 &prev, 611 + &mut a11y, 605 612 ); 606 613 show_confirmation( 607 614 &mut ctx,
+4
crates/bone-ui/src/widgets/dimensioned_input.rs
··· 157 157 let mut input = InputSnapshot::idle(FrameInstant::ZERO); 158 158 let mut clipboard = MemoryClipboard::default(); 159 159 let widget = DimensionedInput::<Length>::new(id_widget(), rect(), PLACEHOLDER, &mut state); 160 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 160 161 let mut ctx = FrameCtx::new( 161 162 theme, 162 163 &mut input, ··· 165 166 StringTable::empty(), 166 167 &mut hits, 167 168 &prev, 169 + &mut a11y, 168 170 ); 169 171 show_parsed_input(&mut ctx, widget, &mut clipboard) 170 172 } ··· 182 184 let mut input = InputSnapshot::idle(FrameInstant::ZERO); 183 185 let mut clipboard = MemoryClipboard::default(); 184 186 let widget = DimensionedInput::<Angle>::new(id_widget(), rect(), PLACEHOLDER, &mut state); 187 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 185 188 let mut ctx = FrameCtx::new( 186 189 theme, 187 190 &mut input, ··· 190 193 StringTable::empty(), 191 194 &mut hits, 192 195 &prev, 196 + &mut a11y, 193 197 ); 194 198 show_parsed_input(&mut ctx, widget, &mut clipboard) 195 199 }
+32 -3
crates/bone-ui/src/widgets/dropdown.rs
··· 1 1 use core::time::Duration; 2 2 3 + use crate::a11y::{AccessNode, Role}; 3 4 use crate::frame::{FrameCtx, InteractDeclaration}; 4 5 use crate::hit_test::{Interaction, Sense, ZLayer}; 5 6 use crate::input::{FrameInstant, KeyCode, KeyEvent, ModifierMask, NamedKey}; ··· 126 127 InteractDeclaration::new(id, trigger_rect, Sense::INTERACTIVE) 127 128 .focusable(interactive) 128 129 .disabled(!interactive) 129 - .active(state.open), 130 + .active(state.open) 131 + .a11y( 132 + AccessNode::new(Role::ComboBox) 133 + .with_label(placeholder) 134 + .with_disabled(!interactive) 135 + .with_expanded(state.open), 136 + ), 130 137 ); 131 138 let live_focused = ctx.is_focused(id); 132 139 if state.open && state.focus_seated && !live_focused { ··· 181 188 id, 182 189 popup_rect, 183 190 item_height, 191 + label: placeholder, 184 192 items: &items, 185 193 initial_selected: initial_selected.as_ref(), 186 194 state, ··· 203 211 id: WidgetId, 204 212 popup_rect: LayoutRect, 205 213 item_height: LayoutPx, 214 + label: StringKey, 206 215 items: &'a [DropdownItem<T>], 207 216 initial_selected: Option<&'a T>, 208 217 state: &'a mut DropdownState, ··· 220 229 id, 221 230 popup_rect, 222 231 item_height, 232 + label, 223 233 items, 224 234 initial_selected, 225 235 state, ··· 232 242 } 233 243 typeahead_step(ctx, items, state); 234 244 } 245 + ctx.a11y.push( 246 + id.child(WidgetKey::new("popup")), 247 + popup_rect, 248 + AccessNode::new(Role::ListBox).with_label(label), 249 + ); 235 250 paint.push(WidgetPaint::Surface { 236 251 rect: popup_rect, 237 252 fill: ctx.theme().colors.surface(crate::theme::SurfaceLevel::L1), ··· 250 265 let item_interaction = ctx.interact( 251 266 InteractDeclaration::new(item_id, item_rect, Sense::INTERACTIVE) 252 267 .at_z(ZLayer::POPUP) 253 - .active(Some(&item.value) == initial_selected), 268 + .active(Some(&item.value) == initial_selected) 269 + .a11y( 270 + AccessNode::new(Role::ListBoxOption) 271 + .with_label(item.label) 272 + .with_selected(Some(&item.value) == initial_selected), 273 + ), 254 274 ); 255 275 if item_interaction.hover() { 256 276 current_hovered = Some(index); ··· 656 676 state, 657 677 ); 658 678 let response = { 679 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 659 680 let mut ctx = FrameCtx::new( 660 681 theme, 661 682 snap, ··· 664 685 StringTable::empty(), 665 686 &mut hits, 666 687 prev, 688 + &mut a11y, 667 689 ); 668 690 show_dropdown(&mut ctx, widget) 669 691 }; ··· 903 925 PLACEHOLDER, 904 926 state, 905 927 ); 906 - let mut ctx = FrameCtx::new(theme, snap, focus, &table, strings, &mut hits, prev); 928 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 929 + let mut ctx = FrameCtx::new( 930 + theme, snap, focus, &table, strings, &mut hits, prev, &mut a11y, 931 + ); 907 932 show_dropdown(&mut ctx, widget) 908 933 } 909 934 ··· 1148 1173 ) 1149 1174 .disabled(true); 1150 1175 let _ = { 1176 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 1151 1177 let mut ctx = FrameCtx::new( 1152 1178 theme.clone(), 1153 1179 &mut snap, ··· 1156 1182 StringTable::empty(), 1157 1183 &mut hits, 1158 1184 &prev, 1185 + &mut a11y, 1159 1186 ); 1160 1187 show_dropdown(&mut ctx, widget) 1161 1188 }; ··· 1187 1214 ); 1188 1215 let sibling_id = WidgetId::ROOT.child(WidgetKey::new("sibling_under_popup")); 1189 1216 { 1217 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 1190 1218 let mut ctx = FrameCtx::new( 1191 1219 theme, 1192 1220 &mut snap, ··· 1195 1223 StringTable::empty(), 1196 1224 &mut hits, 1197 1225 &prev, 1226 + &mut a11y, 1198 1227 ); 1199 1228 let _ = show_dropdown(&mut ctx, widget); 1200 1229 let _ = ctx.interact(InteractDeclaration::new(
+5
crates/bone-ui/src/widgets/file_picker.rs
··· 141 141 ListView::new( 142 142 id.child(WidgetKey::new("list")), 143 143 list_rect, 144 + StringKey::new("file_picker.list"), 144 145 &list_items, 145 146 &mut state.list, 146 147 ), ··· 284 285 ModifierMask::NONE, 285 286 )); 286 287 let response = { 288 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 287 289 let mut ctx = FrameCtx::new( 288 290 theme, 289 291 &mut snap, ··· 292 294 StringTable::empty(), 293 295 &mut hits, 294 296 &prev, 297 + &mut a11y, 295 298 ); 296 299 show_file_picker( 297 300 &mut ctx, ··· 320 323 let prev = HitState::new(); 321 324 let mut snap = InputSnapshot::idle(FrameInstant::ZERO); 322 325 let response = { 326 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 323 327 let mut ctx = FrameCtx::new( 324 328 theme, 325 329 &mut snap, ··· 328 332 StringTable::empty(), 329 333 &mut hits, 330 334 &prev, 335 + &mut a11y, 331 336 ); 332 337 show_file_picker( 333 338 &mut ctx,
+11 -1
crates/bone-ui/src/widgets/hotkey_capture.rs
··· 1 + use crate::a11y::{AccessNode, Role}; 1 2 use crate::frame::{FrameCtx, InteractDeclaration}; 2 3 use crate::hit_test::{Interaction, Sense}; 3 4 use crate::hotkey::KeyChord; ··· 72 73 InteractDeclaration::new(id, rect, Sense::INTERACTIVE) 73 74 .focusable(interactive) 74 75 .disabled(!interactive) 75 - .active(state.recording), 76 + .active(state.recording) 77 + .a11y( 78 + AccessNode::new(Role::Button) 79 + .with_label(placeholder) 80 + .with_disabled(!interactive), 81 + ), 76 82 ); 77 83 let click = interactive && interaction.click(); 78 84 let live_focused = ctx.is_focused(id); ··· 226 232 let mut hits = HitFrame::new(); 227 233 let prev = HitState::new(); 228 234 let widget = HotkeyCapture::new(id_widget(), rect(), PLACEHOLDER, state); 235 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 229 236 let mut ctx = FrameCtx::new( 230 237 theme, 231 238 snap, ··· 234 241 StringTable::empty(), 235 242 &mut hits, 236 243 &prev, 244 + &mut a11y, 237 245 ); 238 246 show_hotkey_capture(&mut ctx, widget) 239 247 } ··· 462 470 let mut hits = HitFrame::new(); 463 471 let widget = HotkeyCapture::new(id_widget(), rect(), PLACEHOLDER, &mut state); 464 472 { 473 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 465 474 let mut ctx = FrameCtx::new( 466 475 theme.clone(), 467 476 &mut snap, ··· 470 479 StringTable::empty(), 471 480 &mut hits, 472 481 &prev, 482 + &mut a11y, 473 483 ); 474 484 let _ = show_hotkey_capture(&mut ctx, widget); 475 485 }
+74 -8
crates/bone-ui/src/widgets/menu.rs
··· 1 + use crate::a11y::{AccessNode, Role}; 1 2 use crate::frame::{FrameCtx, InteractDeclaration}; 2 3 use crate::hit_test::{Sense, ZLayer}; 3 4 use crate::input::{KeyCode, NamedKey}; ··· 78 79 pub struct Menu<'a, 'state> { 79 80 pub id: WidgetId, 80 81 pub origin: LayoutPos, 82 + pub label: StringKey, 81 83 pub items: &'a [MenuItem], 82 84 pub metrics: MenuMetrics, 83 85 pub state: &'state mut MenuState, ··· 88 90 pub fn new( 89 91 id: WidgetId, 90 92 origin: LayoutPos, 93 + label: StringKey, 91 94 items: &'a [MenuItem], 92 95 state: &'state mut MenuState, 93 96 ) -> Self { 94 97 Self { 95 98 id, 96 99 origin, 100 + label, 97 101 items, 98 102 metrics: MenuMetrics::standard(), 99 103 state, ··· 119 123 let Menu { 120 124 id, 121 125 origin, 126 + label, 122 127 items, 123 128 metrics, 124 129 state, 125 130 } = menu; 126 131 let rect = menu_rect(origin, items, metrics); 132 + ctx.a11y 133 + .push(id, rect, AccessNode::new(Role::Menu).with_label(label)); 127 134 let mut paint = vec![WidgetPaint::Surface { 128 135 rect, 129 136 fill: ctx.theme().colors.surface(crate::theme::SurfaceLevel::L1), ··· 221 228 }; 222 229 let active = items.iter().zip(layouts.iter()).find_map(|(item, rect)| { 223 230 matches!(item, MenuItem::Submenu { id: iid, .. } if *iid == sid).then(|| match item { 224 - MenuItem::Submenu { items: sub, .. } => (*rect, sub.as_slice()), 231 + MenuItem::Submenu { 232 + items: sub, label, .. 233 + } => (*rect, sub.as_slice(), *label), 225 234 _ => unreachable!(), 226 235 }) 227 236 }); 228 - let Some((item_rect, subitems)) = active else { 237 + let Some((item_rect, subitems, sub_label)) = active else { 229 238 state.open_submenu = None; 230 239 state.submenu = None; 231 240 return; ··· 240 249 .get_or_insert_with(|| Box::new(MenuState::default())); 241 250 let sub_response = show_menu( 242 251 ctx, 243 - Menu::new(sub_id, sub_origin, subitems, sub_state).metrics(metrics), 252 + Menu::new(sub_id, sub_origin, sub_label, subitems, sub_state).metrics(metrics), 244 253 ); 245 254 paint.extend(sub_response.paint); 246 255 if let Some(a) = sub_response.activated { ··· 288 297 InteractDeclaration::new(*id, args.rect, Sense::INTERACTIVE) 289 298 .at_z(ZLayer::POPUP) 290 299 .focusable(false) 291 - .disabled(*disabled), 300 + .disabled(*disabled) 301 + .a11y( 302 + AccessNode::new(Role::MenuItem) 303 + .with_label(*label) 304 + .with_disabled(*disabled), 305 + ), 292 306 ); 293 307 let activated = (!*disabled && interaction.click()).then_some(*id); 294 308 let mut paint = ··· 323 337 InteractDeclaration::new(*id, args.rect, Sense::INTERACTIVE) 324 338 .at_z(ZLayer::POPUP) 325 339 .focusable(false) 326 - .active(args.is_open_submenu), 340 + .active(args.is_open_submenu) 341 + .a11y( 342 + AccessNode::new(Role::MenuItem) 343 + .with_label(*label) 344 + .with_expanded(args.is_open_submenu), 345 + ), 327 346 ); 328 347 let opened = (interaction.click() || (interaction.hover() && !args.is_open_submenu)) 329 348 .then_some(*id); ··· 543 562 pub struct ContextMenu<'a, 'state> { 544 563 pub id: WidgetId, 545 564 pub anchor: LayoutPos, 565 + pub label: StringKey, 546 566 pub items: &'a [MenuItem], 547 567 pub state: &'state mut MenuState, 548 568 pub metrics: MenuMetrics, ··· 553 573 pub fn at_cursor( 554 574 id: WidgetId, 555 575 anchor: LayoutPos, 576 + label: StringKey, 556 577 items: &'a [MenuItem], 557 578 state: &'state mut MenuState, 558 579 ) -> Self { 559 580 Self { 560 581 id, 561 582 anchor, 583 + label, 562 584 items, 563 585 state, 564 586 metrics: MenuMetrics::standard(), ··· 571 593 let ContextMenu { 572 594 id, 573 595 anchor, 596 + label, 574 597 items, 575 598 state, 576 599 metrics, ··· 580 603 Menu { 581 604 id, 582 605 origin: anchor, 606 + label, 583 607 items, 584 608 metrics, 585 609 state, ··· 602 626 603 627 #[derive(Debug, PartialEq)] 604 628 pub struct MenuBar<'a, 'state> { 629 + pub id: WidgetId, 605 630 pub rect: LayoutRect, 631 + pub label: StringKey, 606 632 pub entries: &'a [MenuBarEntry], 607 633 pub state: &'state mut MenuBarState, 608 634 pub item_width: LayoutPx, ··· 612 638 impl<'a, 'state> MenuBar<'a, 'state> { 613 639 #[must_use] 614 640 pub const fn new( 641 + id: WidgetId, 615 642 rect: LayoutRect, 643 + label: StringKey, 616 644 entries: &'a [MenuBarEntry], 617 645 state: &'state mut MenuBarState, 618 646 ) -> Self { 619 647 Self { 648 + id, 620 649 rect, 650 + label, 621 651 entries, 622 652 state, 623 653 item_width: LayoutPx::new(56.0), ··· 635 665 #[must_use] 636 666 pub fn show_menu_bar(ctx: &mut FrameCtx<'_>, bar: MenuBar<'_, '_>) -> MenuBarResponse { 637 667 let MenuBar { 668 + id, 638 669 rect, 670 + label, 639 671 entries, 640 672 state, 641 673 item_width, 642 674 item_padding, 643 675 } = bar; 676 + ctx.a11y.push( 677 + id, 678 + rect, 679 + AccessNode::new(Role::MenuBar).with_label(label), 680 + ); 644 681 let mut paint = vec![WidgetPaint::Surface { 645 682 rect, 646 683 fill: ctx.theme().colors.surface(crate::theme::SurfaceLevel::L0), ··· 679 716 let interaction = ctx.interact( 680 717 InteractDeclaration::new(entry.id, entry_rect, Sense::INTERACTIVE) 681 718 .focusable(true) 682 - .active(is_open), 719 + .active(is_open) 720 + .a11y( 721 + AccessNode::new(Role::MenuItem) 722 + .with_label(entry.label) 723 + .with_expanded(is_open), 724 + ), 683 725 ); 684 726 let live_focused = ctx.is_focused(entry.id); 685 727 let pointer_toggled = interaction.click(); ··· 763 805 Menu::new( 764 806 entry.id.child(WidgetKey::new("menu")), 765 807 menu_origin, 808 + entry.label, 766 809 &entry.items, 767 810 &mut state.menu, 768 811 ), ··· 820 863 WidgetId::ROOT.child(WidgetKey::new("menu")) 821 864 } 822 865 866 + fn menu_bar_id() -> WidgetId { 867 + WidgetId::ROOT.child(WidgetKey::new("menu_bar")) 868 + } 869 + 823 870 fn action(name: &'static str) -> MenuItem { 824 871 MenuItem::Action { 825 872 id: menu_root().child(WidgetKey::new(name)), ··· 857 904 let table = HotkeyTable::new(); 858 905 let mut hits = HitFrame::new(); 859 906 let response = { 907 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 860 908 let mut ctx = FrameCtx::new( 861 909 theme, 862 910 snap, ··· 865 913 StringTable::empty(), 866 914 &mut hits, 867 915 prev, 916 + &mut a11y, 868 917 ); 869 918 show_menu( 870 919 &mut ctx, 871 - Menu::new(menu_root(), LayoutPos::ORIGIN, items, state), 920 + Menu::new( 921 + menu_root(), 922 + LayoutPos::ORIGIN, 923 + StringKey::new("test.menu"), 924 + items, 925 + state, 926 + ), 872 927 ) 873 928 }; 874 929 let next = resolve(prev, &hits, snap, focus.focused()); ··· 1031 1086 .for_each(|mut snap| { 1032 1087 let mut hits = HitFrame::new(); 1033 1088 { 1089 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 1034 1090 let mut ctx = FrameCtx::new( 1035 1091 theme.clone(), 1036 1092 &mut snap, ··· 1039 1095 StringTable::empty(), 1040 1096 &mut hits, 1041 1097 &prev, 1098 + &mut a11y, 1042 1099 ); 1043 - let _ = show_menu_bar(&mut ctx, MenuBar::new(bar_rect, &entries, &mut state)); 1100 + let _ = show_menu_bar( 1101 + &mut ctx, 1102 + MenuBar::new( 1103 + menu_bar_id(), 1104 + bar_rect, 1105 + StringKey::new("test.menu_bar"), 1106 + &entries, 1107 + &mut state, 1108 + ), 1109 + ); 1044 1110 } 1045 1111 prev = resolve(&prev, &hits, &snap, focus.focused()); 1046 1112 });
+10
crates/bone-ui/src/widgets/numeric_input.rs
··· 94 94 input.text_committed = String::new(); 95 95 let mut clipboard = MemoryClipboard::default(); 96 96 let widget = NumericInput::<T>::new(id_widget(), rect(), PLACEHOLDER, state); 97 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 97 98 let mut ctx = FrameCtx::new( 98 99 theme, 99 100 &mut input, ··· 102 103 StringTable::empty(), 103 104 &mut hits, 104 105 &prev, 106 + &mut a11y, 105 107 ); 106 108 show_parsed_input(&mut ctx, widget, &mut clipboard) 107 109 } ··· 213 215 let mut clipboard = MemoryClipboard::default(); 214 216 let widget = NumericInput::<i32>::new(id_widget(), rect(), PLACEHOLDER, &mut state); 215 217 let response = { 218 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 216 219 let mut ctx = FrameCtx::new( 217 220 theme, 218 221 &mut input, ··· 221 224 StringTable::empty(), 222 225 &mut hits, 223 226 &prev, 227 + &mut a11y, 224 228 ); 225 229 show_parsed_input(&mut ctx, widget, &mut clipboard) 226 230 }; ··· 240 244 let mut input = InputSnapshot::idle(FrameInstant::ZERO); 241 245 let mut clipboard = MemoryClipboard::default(); 242 246 let widget = NumericInput::<i32>::new(id_widget(), rect(), PLACEHOLDER, &mut state); 247 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 243 248 let mut ctx = FrameCtx::new( 244 249 theme, 245 250 &mut input, ··· 248 253 StringTable::empty(), 249 254 &mut hits, 250 255 &prev, 256 + &mut a11y, 251 257 ); 252 258 let response = show_parsed_input(&mut ctx, widget, &mut clipboard); 253 259 assert_eq!(response.committed, Some(42)); ··· 274 280 let prev = HitState::new(); 275 281 let mut input = InputSnapshot::idle(FrameInstant::ZERO); 276 282 let widget = NumericInput::<i32>::new(id_widget(), rect(), PLACEHOLDER, &mut state); 283 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 277 284 let mut ctx = FrameCtx::new( 278 285 theme.clone(), 279 286 &mut input, ··· 282 289 StringTable::empty(), 283 290 &mut hits, 284 291 &prev, 292 + &mut a11y, 285 293 ); 286 294 let response = show_parsed_input(&mut ctx, widget, &mut clipboard); 287 295 assert!( ··· 295 303 let prev = HitState::new(); 296 304 let mut input = InputSnapshot::idle(FrameInstant::ZERO); 297 305 let widget = NumericInput::<i32>::new(id_widget(), rect(), PLACEHOLDER, &mut state); 306 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 298 307 let mut ctx = FrameCtx::new( 299 308 theme, 300 309 &mut input, ··· 303 312 StringTable::empty(), 304 313 &mut hits, 305 314 &prev, 315 + &mut a11y, 306 316 ); 307 317 let response = show_parsed_input(&mut ctx, widget, &mut clipboard); 308 318 assert_eq!(
+15 -1
crates/bone-ui/src/widgets/panel.rs
··· 1 + use crate::a11y::{AccessNode, Role}; 1 2 use crate::frame::{FrameCtx, InteractDeclaration}; 2 3 use crate::hit_test::Sense; 3 4 use crate::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize}; ··· 89 90 titlebar, 90 91 state, 91 92 } = panel; 93 + let host = match titlebar { 94 + Some(bar) => AccessNode::new(Role::Pane).with_label(bar.label), 95 + None => AccessNode::new(Role::Pane), 96 + }; 97 + ctx.a11y.push(id, rect, host); 92 98 let mut paint = panel_surface(ctx, rect, variant); 93 99 let body_origin_y = match titlebar { 94 100 None => rect.origin.y, ··· 159 165 let toggle_rect = title_rect; 160 166 if bar.collapsible { 161 167 let interaction = ctx.interact( 162 - InteractDeclaration::new(toggle_id, toggle_rect, Sense::INTERACTIVE).focusable(true), 168 + InteractDeclaration::new(toggle_id, toggle_rect, Sense::INTERACTIVE) 169 + .focusable(true) 170 + .a11y( 171 + AccessNode::new(Role::Button) 172 + .with_label(bar.label) 173 + .with_expanded(!state.collapsed), 174 + ), 163 175 ); 164 176 let live_focused = ctx.is_focused(toggle_id); 165 177 if interaction.click() { ··· 280 292 let table = HotkeyTable::new(); 281 293 let mut hits = HitFrame::new(); 282 294 let response = { 295 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 283 296 let mut ctx = FrameCtx::new( 284 297 theme, 285 298 snap, ··· 288 301 StringTable::empty(), 289 302 &mut hits, 290 303 prev, 304 + &mut a11y, 291 305 ); 292 306 let mut p = Panel::new(panel_id(), panel_rect(), state).variant(PanelVariant::Card); 293 307 if let Some(t) = bar {
+25 -6
crates/bone-ui/src/widgets/property_grid.rs
··· 1 1 use uom::si::f64::{Angle, Length}; 2 2 3 + use crate::a11y::{AccessNode, Role}; 3 4 use crate::frame::FrameCtx; 4 5 use crate::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize}; 5 6 use crate::strings::StringKey; ··· 41 42 pub struct PropertyGrid<'a, 'rows> { 42 43 pub id: WidgetId, 43 44 pub rect: LayoutRect, 45 + pub label: StringKey, 44 46 pub rows: &'a mut [PropertyRow<'rows>], 45 47 pub row_height: LayoutPx, 46 48 pub label_width: LayoutPx, ··· 49 51 50 52 impl<'a, 'rows> PropertyGrid<'a, 'rows> { 51 53 #[must_use] 52 - pub fn new(id: WidgetId, rect: LayoutRect, rows: &'a mut [PropertyRow<'rows>]) -> Self { 54 + pub fn new( 55 + id: WidgetId, 56 + rect: LayoutRect, 57 + label: StringKey, 58 + rows: &'a mut [PropertyRow<'rows>], 59 + ) -> Self { 53 60 Self { 54 61 id, 55 62 rect, 63 + label, 56 64 rows, 57 65 row_height: LayoutPx::new(28.0), 58 66 label_width: LayoutPx::new(120.0), ··· 74 82 clipboard: &mut dyn Clipboard, 75 83 ) -> PropertyGridResponse { 76 84 let PropertyGrid { 77 - id: _, 85 + id, 78 86 rect, 87 + label, 79 88 rows, 80 89 row_height, 81 90 label_width, 82 91 padding, 83 92 } = grid; 93 + ctx.a11y 94 + .push(id, rect, AccessNode::new(Role::Form).with_label(label)); 84 95 let mut paint = vec![WidgetPaint::Surface { 85 96 rect, 86 97 fill: ctx.theme().colors.surface(crate::theme::SurfaceLevel::L0), ··· 496 507 .for_each(|mut snap| { 497 508 let mut hits = HitFrame::new(); 498 509 let response = { 510 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 499 511 let mut ctx = FrameCtx::new( 500 512 theme.clone(), 501 513 &mut snap, ··· 504 516 StringTable::empty(), 505 517 &mut hits, 506 518 &prev, 519 + &mut a11y, 507 520 ); 508 521 let mut rows = [PropertyRow { 509 522 id: row_id, ··· 513 526 }]; 514 527 show_property_grid( 515 528 &mut ctx, 516 - PropertyGrid::new(grid_id(), rect(), &mut rows), 529 + PropertyGrid::new(grid_id(), rect(), StringKey::new("test.grid"), &mut rows), 517 530 &mut clipboard, 518 531 ) 519 532 }; ··· 539 552 let table = HotkeyTable::new(); 540 553 let mut hits = HitFrame::new(); 541 554 { 555 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 542 556 let mut ctx = FrameCtx::new( 543 557 theme.clone(), 544 558 &mut snap, ··· 547 561 StringTable::empty(), 548 562 &mut hits, 549 563 &prev, 564 + &mut a11y, 550 565 ); 551 566 let mut rows = [PropertyRow { 552 567 id: row_id, ··· 556 571 }]; 557 572 let _ = show_property_grid( 558 573 &mut ctx, 559 - PropertyGrid::new(grid_id(), rect(), &mut rows), 574 + PropertyGrid::new(grid_id(), rect(), StringKey::new("test.grid"), &mut rows), 560 575 &mut clipboard, 561 576 ); 562 577 } ··· 564 579 let mut snap = InputSnapshot::idle(FrameInstant::ZERO); 565 580 let mut hits = HitFrame::new(); 566 581 { 582 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 567 583 let mut ctx = FrameCtx::new( 568 584 theme, 569 585 &mut snap, ··· 572 588 StringTable::empty(), 573 589 &mut hits, 574 590 &prev, 591 + &mut a11y, 575 592 ); 576 593 let mut rows = [PropertyRow { 577 594 id: row_id, ··· 581 598 }]; 582 599 let _ = show_property_grid( 583 600 &mut ctx, 584 - PropertyGrid::new(grid_id(), rect(), &mut rows), 601 + PropertyGrid::new(grid_id(), rect(), StringKey::new("test.grid"), &mut rows), 585 602 &mut clipboard, 586 603 ); 587 604 } ··· 605 622 let table = HotkeyTable::new(); 606 623 let mut hits = HitFrame::new(); 607 624 let response = { 625 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 608 626 let mut ctx = FrameCtx::new( 609 627 theme, 610 628 &mut snap, ··· 613 631 StringTable::empty(), 614 632 &mut hits, 615 633 &prev, 634 + &mut a11y, 616 635 ); 617 636 let mut rows = [ 618 637 PropertyRow { ··· 630 649 ]; 631 650 show_property_grid( 632 651 &mut ctx, 633 - PropertyGrid::new(grid_id(), rect(), &mut rows), 652 + PropertyGrid::new(grid_id(), rect(), StringKey::new("test.grid"), &mut rows), 634 653 &mut clipboard, 635 654 ) 636 655 };
+20 -1
crates/bone-ui/src/widgets/radio_group.rs
··· 1 + use crate::a11y::{AccessNode, Role}; 1 2 use crate::frame::{FrameCtx, InteractDeclaration}; 2 3 use crate::hit_test::Sense; 3 4 use crate::input::{KeyCode, NamedKey}; ··· 99 100 InteractDeclaration::new(option.id, option.rect, Sense::INTERACTIVE) 100 101 .focusable(false) 101 102 .disabled(disabled) 102 - .active(active), 103 + .active(active) 104 + .a11y( 105 + AccessNode::new(Role::RadioButton) 106 + .with_label(option.label) 107 + .with_disabled(disabled) 108 + .with_selected(active), 109 + ), 103 110 ); 104 111 if !disabled && interaction.click() { 105 112 clicked = Some(option.value); ··· 298 305 hits.clear(); 299 306 let group = RadioGroup::new(three_options(), selected); 300 307 let response = { 308 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 301 309 let mut ctx = FrameCtx::new( 302 310 theme.clone(), 303 311 &mut input, ··· 306 314 StringTable::empty(), 307 315 &mut hits, 308 316 &state, 317 + &mut a11y, 309 318 ); 310 319 show_radio_group(&mut ctx, group) 311 320 }; ··· 340 349 input.keys_pressed = events; 341 350 let group = RadioGroup::new(options, selected).orientation(orientation); 342 351 let response = { 352 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 343 353 let mut ctx = FrameCtx::new( 344 354 theme, 345 355 &mut input, ··· 348 358 StringTable::empty(), 349 359 &mut hits, 350 360 &prev, 361 + &mut a11y, 351 362 ); 352 363 show_radio_group(&mut ctx, group) 353 364 }; ··· 511 522 let first_id = two[0].id; 512 523 let group = RadioGroup::new(two, Pick::C); 513 524 { 525 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 514 526 let mut ctx = FrameCtx::new( 515 527 theme, 516 528 &mut input, ··· 519 531 StringTable::empty(), 520 532 &mut hits, 521 533 &state, 534 + &mut a11y, 522 535 ); 523 536 let _ = show_radio_group(&mut ctx, group); 524 537 } ··· 540 553 let options = three_options(); 541 554 let group = RadioGroup::new(options.clone(), Pick::B); 542 555 { 556 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 543 557 let mut ctx = FrameCtx::new( 544 558 theme, 545 559 &mut input, ··· 548 562 StringTable::empty(), 549 563 &mut hits, 550 564 &state, 565 + &mut a11y, 551 566 ); 552 567 let _ = show_radio_group(&mut ctx, group); 553 568 } ··· 572 587 input.keys_pressed = vec![arrow, space]; 573 588 let group = RadioGroup::new(three_options(), Pick::A); 574 589 { 590 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 575 591 let mut ctx = FrameCtx::new( 576 592 theme, 577 593 &mut input, ··· 580 596 StringTable::empty(), 581 597 &mut hits, 582 598 &state, 599 + &mut a11y, 583 600 ); 584 601 let _ = show_radio_group(&mut ctx, group); 585 602 } ··· 604 621 hits.clear(); 605 622 let group = RadioGroup::new(three_options(), selected).disabled(true); 606 623 let response = { 624 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 607 625 let mut ctx = FrameCtx::new( 608 626 theme.clone(), 609 627 &mut input, ··· 612 630 StringTable::empty(), 613 631 &mut hits, 614 632 &state, 633 + &mut a11y, 615 634 ); 616 635 show_radio_group(&mut ctx, group) 617 636 };
+35 -4
crates/bone-ui/src/widgets/ribbon.rs
··· 1 1 use std::collections::BTreeMap; 2 2 3 + use crate::a11y::{AccessNode, Role}; 3 4 use crate::frame::FrameCtx; 4 5 use crate::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize}; 5 6 use crate::strings::StringKey; 6 7 use crate::theme::{Border, Step12, StrokeWidth}; 7 - use crate::widget_id::WidgetId; 8 + use crate::widget_id::{WidgetId, WidgetKey}; 8 9 9 10 use super::paint::{LabelText, WidgetPaint}; 10 11 use super::tabs::{Tab, Tabs, TabsOrientation, show_tabs}; ··· 50 51 pub struct Ribbon<'a, 'state> { 51 52 pub id: WidgetId, 52 53 pub rect: LayoutRect, 54 + pub label: StringKey, 53 55 pub tabs: &'a [RibbonTab], 54 56 pub active: WidgetId, 55 57 pub state: &'state mut RibbonState, ··· 64 66 pub const fn new( 65 67 id: WidgetId, 66 68 rect: LayoutRect, 69 + label: StringKey, 67 70 tabs: &'a [RibbonTab], 68 71 active: WidgetId, 69 72 state: &'state mut RibbonState, ··· 71 74 Self { 72 75 id, 73 76 rect, 77 + label, 74 78 tabs, 75 79 active, 76 80 state, ··· 95 99 let Ribbon { 96 100 id, 97 101 rect, 102 + label, 98 103 tabs, 99 104 active, 100 105 state, ··· 118 123 ), 119 124 ); 120 125 let tab_views: Vec<Tab> = build_tab_strip(tabs, strip_rect); 126 + ctx.a11y.push( 127 + id, 128 + rect, 129 + AccessNode::new(Role::TabPanel).with_label(label), 130 + ); 121 131 let mut paint = vec![WidgetPaint::Surface { 122 132 rect, 123 133 fill: ctx.theme().colors.surface(crate::theme::SurfaceLevel::L1), ··· 130 140 }]; 131 141 let tabs_response = show_tabs( 132 142 ctx, 133 - Tabs::new(id, TabsOrientation::Top, tab_views.as_slice(), active), 143 + Tabs::new( 144 + id.child(WidgetKey::new("tabs")), 145 + TabsOrientation::Top, 146 + label, 147 + tab_views.as_slice(), 148 + active, 149 + ), 134 150 ); 135 151 paint.extend(tabs_response.paint); 136 152 let mut activated_tool: Option<WidgetId> = None; ··· 221 237 .iter() 222 238 .zip(layouts.iter()) 223 239 .for_each(|(group, group_rect)| { 240 + ctx.a11y.push( 241 + group.id, 242 + *group_rect, 243 + AccessNode::new(Role::Group).with_label(group.label), 244 + ); 224 245 paint.push(WidgetPaint::Surface { 225 246 rect: *group_rect, 226 247 fill: ctx.theme().colors.surface(crate::theme::SurfaceLevel::L1), ··· 236 257 let response = show_toolbar( 237 258 ctx, 238 259 Toolbar::horizontal( 239 - group.id, 260 + group.id.child(WidgetKey::new("toolbar")), 240 261 toolbar_rect, 262 + group.label, 241 263 &group.items, 242 264 group.icon_size.item_px(), 243 265 LayoutPx::new(4.0), ··· 361 383 LayoutSize::new(LayoutPx::new(800.0), LayoutPx::new(120.0)), 362 384 ); 363 385 let response = { 386 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 364 387 let mut ctx = FrameCtx::new( 365 388 theme, 366 389 snap, ··· 369 392 StringTable::empty(), 370 393 &mut hits, 371 394 prev, 395 + &mut a11y, 372 396 ); 373 397 show_ribbon( 374 398 &mut ctx, 375 - Ribbon::new(ribbon_id(), rect, tabs, active, state), 399 + Ribbon::new( 400 + ribbon_id(), 401 + rect, 402 + StringKey::new("test.ribbon"), 403 + tabs, 404 + active, 405 + state, 406 + ), 376 407 ) 377 408 }; 378 409 let next = resolve(prev, &hits, snap, focus.focused());
+46 -4
crates/bone-ui/src/widgets/slider.rs
··· 1 1 use core::fmt::Debug; 2 2 use core::num::NonZeroU8; 3 3 4 + use crate::a11y::{AccessNode, AccessRange, Role}; 4 5 use crate::frame::{FrameCtx, InteractDeclaration}; 5 6 use crate::hit_test::{Interaction, Sense}; 6 7 use crate::input::{KeyCode, KeyEvent, ModifierMask, NamedKey}; 7 8 use crate::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize}; 9 + use crate::strings::StringKey; 8 10 use crate::theme::{Border, Step12, StrokeWidth}; 9 11 use crate::widget_id::WidgetId; 10 12 ··· 17 19 fn to_unit(self, range: SliderRange<Self>) -> f64; 18 20 #[must_use] 19 21 fn from_unit(unit: f64, range: SliderRange<Self>) -> Self; 22 + #[must_use] 23 + fn to_f64(self) -> f64; 20 24 #[must_use] 21 25 fn step_by(self, step: SliderStep<Self>, sign: i32) -> Self; 22 26 #[must_use] ··· 105 109 pub struct Slider<T: SliderScalar> { 106 110 pub id: WidgetId, 107 111 pub rect: LayoutRect, 112 + pub label: StringKey, 108 113 pub value: T, 109 114 pub range: SliderRange<T>, 110 115 pub step: SliderStep<T>, ··· 117 122 pub fn new( 118 123 id: WidgetId, 119 124 rect: LayoutRect, 125 + label: StringKey, 120 126 value: T, 121 127 range: SliderRange<T>, 122 128 step: SliderStep<T>, ··· 124 130 Self { 125 131 id, 126 132 rect, 133 + label, 127 134 value, 128 135 range, 129 136 step, ··· 158 165 let Slider { 159 166 id, 160 167 rect, 168 + label, 161 169 value: initial_value, 162 170 range, 163 171 step, ··· 198 206 } 199 207 } 200 208 209 + ctx.a11y.push( 210 + id, 211 + rect, 212 + AccessNode::new(Role::Slider) 213 + .with_label(label) 214 + .with_disabled(!interactive) 215 + .with_range(AccessRange { 216 + value: value.to_f64(), 217 + min: range.min().to_f64(), 218 + max: range.max().to_f64(), 219 + step: step.value().to_f64(), 220 + }), 221 + ); 222 + 201 223 let paint = build_paint(ctx, rect, range, disabled, value, interaction, live_focused); 202 224 SliderResponse { 203 225 interaction, ··· 353 375 range.min + (range.max - range.min) * unit.clamp(0.0, 1.0) 354 376 } 355 377 378 + fn to_f64(self) -> f64 { 379 + self 380 + } 381 + 356 382 fn step_by(self, step: SliderStep<Self>, sign: i32) -> Self { 357 383 self + step.value() * f64::from(sign) 358 384 } ··· 375 401 #[allow(clippy::cast_possible_truncation)] 376 402 fn from_unit(unit: f64, range: SliderRange<Self>) -> Self { 377 403 range.min + (range.max - range.min) * unit.clamp(0.0, 1.0) as f32 404 + } 405 + 406 + fn to_f64(self) -> f64 { 407 + f64::from(self) 378 408 } 379 409 380 410 fn step_by(self, step: SliderStep<Self>, sign: i32) -> Self { ··· 405 435 result 406 436 } 407 437 438 + fn to_f64(self) -> f64 { 439 + f64::from(self) 440 + } 441 + 408 442 fn from_unit(unit: f64, range: SliderRange<Self>) -> Self { 409 443 let span = i64::from(range.max) - i64::from(range.min); 410 444 #[allow( ··· 464 498 use crate::hotkey::HotkeyTable; 465 499 use crate::input::{FrameInstant, InputSnapshot, KeyCode, KeyEvent, ModifierMask, NamedKey}; 466 500 use crate::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize}; 467 - use crate::strings::StringTable; 501 + use crate::strings::{StringKey, StringTable}; 468 502 use crate::theme::Theme; 469 503 use crate::widget_id::{WidgetId, WidgetKey}; 504 + 505 + const LABEL: StringKey = StringKey::new("slider.label"); 470 506 471 507 fn rect() -> LayoutRect { 472 508 LayoutRect::new( ··· 504 540 let prev = HitState::new(); 505 541 let mut input = InputSnapshot::idle(FrameInstant::ZERO); 506 542 input.keys_pressed = events; 507 - let widget = Slider::new(slider_id(), rect(), value, unit_range(), unit_step()); 543 + let widget = Slider::new(slider_id(), rect(), LABEL, value, unit_range(), unit_step()); 544 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 508 545 let mut ctx = FrameCtx::new( 509 546 theme, 510 547 &mut input, ··· 513 550 StringTable::empty(), 514 551 &mut hits, 515 552 &prev, 553 + &mut a11y, 516 554 ); 517 555 show_slider(&mut ctx, widget) 518 556 } ··· 698 736 snap.buttons_pressed = pressed; 699 737 let mut hits = HitFrame::new(); 700 738 let response = { 701 - let widget = Slider::new(slider_id(), rect(), value, unit_range(), step); 739 + let widget = Slider::new(slider_id(), rect(), LABEL, value, unit_range(), step); 740 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 702 741 let mut ctx = FrameCtx::new( 703 742 theme.clone(), 704 743 &mut snap, ··· 707 746 StringTable::empty(), 708 747 &mut hits, 709 748 &prev_state, 749 + &mut a11y, 710 750 ); 711 751 show_slider(&mut ctx, widget) 712 752 }; ··· 824 864 let prev = HitState::new(); 825 865 let mut input = InputSnapshot::idle(FrameInstant::ZERO); 826 866 input.keys_pressed = events; 827 - let widget = Slider::new(slider_id(), rect(), value, unit_range(), step); 867 + let widget = Slider::new(slider_id(), rect(), LABEL, value, unit_range(), step); 868 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 828 869 let mut ctx = FrameCtx::new( 829 870 theme, 830 871 &mut input, ··· 833 874 StringTable::empty(), 834 875 &mut hits, 835 876 &prev, 877 + &mut a11y, 836 878 ); 837 879 show_slider(&mut ctx, widget) 838 880 }
+35 -5
crates/bone-ui/src/widgets/status_bar.rs
··· 1 + use crate::a11y::{AccessNode, Role}; 1 2 use crate::frame::{FrameCtx, InteractDeclaration}; 2 3 use crate::hit_test::Sense; 3 4 use crate::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize}; ··· 58 59 59 60 #[derive(Copy, Clone, Debug, PartialEq)] 60 61 pub struct StatusBar<'a> { 62 + pub id: WidgetId, 61 63 pub rect: LayoutRect, 64 + pub label: StringKey, 62 65 pub items: &'a [StatusItem], 63 66 } 64 67 65 68 impl<'a> StatusBar<'a> { 66 69 #[must_use] 67 - pub const fn new(rect: LayoutRect, items: &'a [StatusItem]) -> Self { 68 - Self { rect, items } 70 + pub const fn new( 71 + id: WidgetId, 72 + rect: LayoutRect, 73 + label: StringKey, 74 + items: &'a [StatusItem], 75 + ) -> Self { 76 + Self { 77 + id, 78 + rect, 79 + label, 80 + items, 81 + } 69 82 } 70 83 } 71 84 ··· 77 90 78 91 #[must_use] 79 92 pub fn show_status_bar(ctx: &mut FrameCtx<'_>, bar: StatusBar<'_>) -> StatusBarResponse { 93 + ctx.a11y.push( 94 + bar.id, 95 + bar.rect, 96 + AccessNode::new(Role::Status).with_label(bar.label), 97 + ); 80 98 let mut paint = vec![WidgetPaint::Surface { 81 99 rect: bar.rect, 82 100 fill: ctx.theme().colors.neutral.step(Step12::SUBTLE_BG), ··· 107 125 ) -> Vec<WidgetPaint> { 108 126 let mut paint = Vec::new(); 109 127 let live_focused = if item.interactive { 110 - let interaction = ctx 111 - .interact(InteractDeclaration::new(item.id, rect, Sense::INTERACTIVE).focusable(true)); 128 + let interaction = ctx.interact( 129 + InteractDeclaration::new(item.id, rect, Sense::INTERACTIVE) 130 + .focusable(true) 131 + .a11y(AccessNode::new(Role::Button).with_label(item.label)), 132 + ); 112 133 let focused = ctx.is_focused(item.id); 113 134 let activated_via_pointer = interaction.click(); 114 135 let activated_via_key = focused && take_activation(ctx.input); ··· 272 293 WidgetId::ROOT.child(WidgetKey::new(name)) 273 294 } 274 295 296 + fn bar_id() -> WidgetId { 297 + WidgetId::ROOT.child(WidgetKey::new("status_bar")) 298 + } 299 + 275 300 fn items() -> Vec<StatusItem> { 276 301 vec![ 277 302 StatusItem::new( ··· 307 332 let table = HotkeyTable::new(); 308 333 let mut hits = HitFrame::new(); 309 334 let response = { 335 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 310 336 let mut ctx = FrameCtx::new( 311 337 theme, 312 338 snap, ··· 315 341 StringTable::empty(), 316 342 &mut hits, 317 343 prev, 344 + &mut a11y, 318 345 ); 319 - show_status_bar(&mut ctx, StatusBar::new(bar_rect(), items)) 346 + show_status_bar( 347 + &mut ctx, 348 + StatusBar::new(bar_id(), bar_rect(), StringKey::new("test.status"), items), 349 + ) 320 350 }; 321 351 let next = resolve(prev, &hits, snap, focus.focused()); 322 352 (response, next)
+61 -8
crates/bone-ui/src/widgets/table.rs
··· 1 1 use std::collections::{BTreeMap, BTreeSet}; 2 2 3 + use crate::a11y::{AccessNode, Role}; 3 4 use crate::frame::{FrameCtx, InteractDeclaration}; 4 5 use crate::hit_test::Sense; 5 6 use crate::input::{KeyCode, ModifierMask, NamedKey}; ··· 56 57 pub struct ListView<'a, 'state> { 57 58 pub id: WidgetId, 58 59 pub rect: LayoutRect, 60 + pub label: StringKey, 59 61 pub items: &'a [ListItem], 60 62 pub state: &'state mut ListViewState, 61 63 pub mode: ListSelectionMode, ··· 67 69 pub const fn new( 68 70 id: WidgetId, 69 71 rect: LayoutRect, 72 + label: StringKey, 70 73 items: &'a [ListItem], 71 74 state: &'state mut ListViewState, 72 75 ) -> Self { 73 76 Self { 74 77 id, 75 78 rect, 79 + label, 76 80 items, 77 81 state, 78 82 mode: ListSelectionMode::Single, ··· 95 99 #[must_use] 96 100 pub fn show_list_view(ctx: &mut FrameCtx<'_>, view: ListView<'_, '_>) -> ListViewResponse { 97 101 let ListView { 98 - id: _, 102 + id, 99 103 rect, 104 + label, 100 105 items, 101 106 state, 102 107 mode, 103 108 row_height, 104 109 } = view; 110 + ctx.a11y.push( 111 + id, 112 + rect, 113 + AccessNode::new(Role::ListBox).with_label(label), 114 + ); 105 115 let mut paint = vec![WidgetPaint::Surface { 106 116 rect, 107 117 fill: ctx.theme().colors.surface(crate::theme::SurfaceLevel::L0), ··· 119 129 let mut activated: Option<WidgetId> = None; 120 130 items.iter().enumerate().for_each(|(idx, item)| { 121 131 let row_rect = list_row_rect(rect, idx, row_height); 132 + let selected = state.selection.contains(&item.id); 122 133 let interaction = ctx.interact( 123 134 InteractDeclaration::new(item.id, row_rect, Sense::INTERACTIVE) 124 135 .focusable(false) 125 - .active(state.selection.contains(&item.id)), 136 + .active(selected) 137 + .a11y( 138 + AccessNode::new(Role::ListBoxOption) 139 + .with_label(item.label) 140 + .with_selected(selected), 141 + ), 126 142 ); 127 143 let live_focused = ctx.is_focused(item.id); 128 144 if interaction.click() { ··· 293 309 pub struct Table<'a, 'state, const N: usize> { 294 310 pub id: WidgetId, 295 311 pub rect: LayoutRect, 312 + pub label: StringKey, 296 313 pub columns: &'a [TableColumn; N], 297 314 pub rows: &'a [TableRow<N>], 298 315 pub state: &'state mut TableState, ··· 306 323 pub const fn new( 307 324 id: WidgetId, 308 325 rect: LayoutRect, 326 + label: StringKey, 309 327 columns: &'a [TableColumn; N], 310 328 rows: &'a [TableRow<N>], 311 329 state: &'state mut TableState, ··· 313 331 Self { 314 332 id, 315 333 rect, 334 + label, 316 335 columns, 317 336 rows, 318 337 state, ··· 344 363 table: Table<'_, '_, N>, 345 364 ) -> TableResponse { 346 365 let Table { 347 - id: _, 366 + id, 348 367 rect, 368 + label, 349 369 columns, 350 370 rows, 351 371 state, ··· 353 373 row_height, 354 374 mode, 355 375 } = table; 376 + ctx.a11y 377 + .push(id, rect, AccessNode::new(Role::Table).with_label(label)); 356 378 let (column_widths, column_x) = compute_column_layout(columns, state, rect); 357 379 let mut paint = vec![WidgetPaint::Surface { 358 380 rect, ··· 513 535 LayoutPos::new(rect.origin.x, LayoutPx::new(row_y)), 514 536 LayoutSize::new(rect.size.width, row_height), 515 537 ); 538 + let selected = state.selection.contains(&row.id); 539 + let label = row 540 + .cells 541 + .first() 542 + .copied() 543 + .unwrap_or(StringKey::new("table.row")); 516 544 let interaction = ctx.interact( 517 545 InteractDeclaration::new(row.id, row_rect, Sense::INTERACTIVE) 518 546 .focusable(false) 519 - .active(state.selection.contains(&row.id)), 547 + .active(selected) 548 + .a11y( 549 + AccessNode::new(Role::Row) 550 + .with_label(label) 551 + .with_selected(selected), 552 + ), 520 553 ); 521 554 let live_focused = ctx.is_focused(row.id); 522 555 if interaction.click() { ··· 590 623 LayoutSize::new(LayoutPx::new(RESIZE_HANDLE_PX), header_rect.size.height), 591 624 ); 592 625 let header_interaction = ctx.interact( 593 - InteractDeclaration::new(header_id, header_rect, Sense::INTERACTIVE).focusable(false), 626 + InteractDeclaration::new(header_id, header_rect, Sense::INTERACTIVE) 627 + .focusable(false) 628 + .a11y(AccessNode::new(Role::ColumnHeader).with_label(column.label)), 594 629 ); 595 630 let resize_interaction = ctx.interact( 596 - InteractDeclaration::new(resize_id, resize_rect, Sense::DRAGGABLE).focusable(false), 631 + InteractDeclaration::new(resize_id, resize_rect, Sense::DRAGGABLE) 632 + .focusable(false) 633 + .a11y( 634 + AccessNode::new(Role::Splitter).with_label(StringKey::new("table.column.resize")), 635 + ), 597 636 ); 598 637 if header_interaction.click() && column.sortable { 599 638 let next = match state.sort { ··· 751 790 LayoutSize::new(LayoutPx::new(200.0), LayoutPx::new(400.0)), 752 791 ); 753 792 let response = { 793 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 754 794 let mut ctx = FrameCtx::new( 755 795 theme, 756 796 snap, ··· 759 799 StringTable::empty(), 760 800 &mut hits, 761 801 prev, 802 + &mut a11y, 762 803 ); 763 - show_list_view(&mut ctx, ListView::new(list_id(), rect, items, state)) 804 + show_list_view( 805 + &mut ctx, 806 + ListView::new(list_id(), rect, StringKey::new("test.list"), items, state), 807 + ) 764 808 }; 765 809 let next = resolve(prev, &hits, snap, focus.focused()); 766 810 (response, next) ··· 893 937 LayoutSize::new(LayoutPx::new(220.0), LayoutPx::new(200.0)), 894 938 ); 895 939 let response = { 940 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 896 941 let mut ctx = FrameCtx::new( 897 942 theme, 898 943 snap, ··· 901 946 StringTable::empty(), 902 947 &mut hits, 903 948 prev, 949 + &mut a11y, 904 950 ); 905 951 show_table( 906 952 &mut ctx, 907 - Table::new(table_id(), rect, cols, rows_data, state), 953 + Table::new( 954 + table_id(), 955 + rect, 956 + StringKey::new("test.table"), 957 + cols, 958 + rows_data, 959 + state, 960 + ), 908 961 ) 909 962 }; 910 963 let next = resolve(prev, &hits, snap, focus.focused());
+34 -3
crates/bone-ui/src/widgets/tabs.rs
··· 1 + use crate::a11y::{AccessNode, Role}; 1 2 use crate::frame::{FrameCtx, InteractDeclaration}; 2 3 use crate::hit_test::{Interaction, Sense}; 3 4 use crate::input::{KeyCode, NamedKey}; ··· 52 53 pub struct Tabs<'a> { 53 54 pub id: WidgetId, 54 55 pub orientation: TabsOrientation, 56 + pub label: StringKey, 55 57 pub tabs: &'a [Tab], 56 58 pub active: WidgetId, 57 59 } ··· 61 63 pub const fn new( 62 64 id: WidgetId, 63 65 orientation: TabsOrientation, 66 + label: StringKey, 64 67 tabs: &'a [Tab], 65 68 active: WidgetId, 66 69 ) -> Self { 67 70 Self { 68 71 id, 69 72 orientation, 73 + label, 70 74 tabs, 71 75 active, 72 76 } ··· 88 92 let Tabs { 89 93 id: tabs_id, 90 94 orientation, 95 + label, 91 96 tabs: items, 92 97 active, 93 98 } = tabs; ··· 100 105 if let Some(stop) = tab_stop { 101 106 ctx.focus.register_tab_stop(stop); 102 107 } 108 + if let Some(strip_rect) = items.iter().map(|t| t.rect).reduce(LayoutRect::union) { 109 + ctx.a11y.push( 110 + tabs_id, 111 + strip_rect, 112 + AccessNode::new(Role::TabList).with_label(label), 113 + ); 114 + } 103 115 let mut paint = Vec::new(); 104 116 let folded = items 105 117 .iter() ··· 142 154 InteractDeclaration::new(tab.id, tab.rect, Sense::INTERACTIVE) 143 155 .focusable(false) 144 156 .disabled(!interactive) 145 - .active(is_active), 157 + .active(is_active) 158 + .a11y( 159 + AccessNode::new(Role::Tab) 160 + .with_label(tab.label) 161 + .with_disabled(!interactive) 162 + .with_selected(is_active), 163 + ), 146 164 ); 147 165 if interactive && interaction.click() { 148 166 ctx.focus.request_focus(tab.id); ··· 163 181 let close_interaction = ctx.interact( 164 182 InteractDeclaration::new(close_id, close_rect, Sense::INTERACTIVE) 165 183 .focusable(false) 166 - .disabled(!interactive), 184 + .disabled(!interactive) 185 + .a11y( 186 + AccessNode::new(Role::Button) 187 + .with_label(StringKey::new("tabs.close")) 188 + .with_disabled(!interactive), 189 + ), 167 190 ); 168 191 if interactive && close_interaction.click() { 169 192 closed = Some(tab.id); ··· 384 407 let table = HotkeyTable::new(); 385 408 let mut hits = HitFrame::new(); 386 409 let response = { 410 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 387 411 let mut ctx = FrameCtx::new( 388 412 theme, 389 413 snap, ··· 392 416 StringTable::empty(), 393 417 &mut hits, 394 418 prev, 419 + &mut a11y, 395 420 ); 396 421 show_tabs( 397 422 &mut ctx, 398 - Tabs::new(tabs_id(), TabsOrientation::Top, items, active), 423 + Tabs::new( 424 + tabs_id(), 425 + TabsOrientation::Top, 426 + StringKey::new("test.tabs"), 427 + items, 428 + active, 429 + ), 399 430 ) 400 431 }; 401 432 let next_state = resolve(prev, &hits, snap, focus.focused());
+9 -1
crates/bone-ui/src/widgets/text_input.rs
··· 1 1 use bone_text::SourceByteIndex; 2 2 3 + use crate::a11y::{AccessNode, Role}; 3 4 use crate::frame::{FrameCtx, InteractDeclaration}; 4 5 use crate::hit_test::{Interaction, Sense}; 5 6 use crate::input::{KeyCode, KeyEvent, ModifierMask, NamedKey}; ··· 131 132 let interaction = ctx.interact( 132 133 InteractDeclaration::new(id, rect, Sense::INTERACTIVE) 133 134 .focusable(interactive) 134 - .disabled(!interactive), 135 + .disabled(!interactive) 136 + .a11y( 137 + AccessNode::new(Role::TextInput) 138 + .with_label(placeholder) 139 + .with_disabled(!interactive), 140 + ), 135 141 ); 136 142 let live_focused = ctx.is_focused(id); 137 143 let mut edits = Vec::new(); ··· 548 554 disabled: false, 549 555 validator, 550 556 }; 557 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 551 558 let mut ctx = FrameCtx::new( 552 559 theme, 553 560 input, ··· 556 563 StringTable::empty(), 557 564 &mut hits, 558 565 &prev, 566 + &mut a11y, 559 567 ); 560 568 show_text_input(&mut ctx, widget, clipboard) 561 569 }
+22 -7
crates/bone-ui/src/widgets/toast.rs
··· 1 1 use core::time::Duration; 2 2 3 + use crate::a11y::{AccessNode, Role}; 3 4 use crate::frame::{FrameCtx, InteractDeclaration}; 4 5 use crate::hit_test::Sense; 5 - use crate::input::FrameInstant; 6 + use crate::input::{FrameInstant, NamedKey}; 6 7 use crate::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize}; 7 8 use crate::strings::StringKey; 8 9 use crate::theme::{Border, Color, Step12, StrokeWidth}; 9 10 use crate::widget_id::{WidgetId, WidgetKey}; 10 11 12 + use super::keys::{TakeKey, take_key}; 11 13 use super::paint::{GlyphMark, LabelText, WidgetPaint}; 14 + use super::visuals::push_focus_ring; 12 15 13 16 #[derive(Copy, Clone, Debug, PartialEq, Eq)] 14 17 pub enum ToastKind { ··· 125 128 paint, 126 129 }; 127 130 } 131 + ctx.a11y 132 + .push(id, rect, AccessNode::new(Role::Alert).with_label(message)); 128 133 paint.push(WidgetPaint::Surface { 129 134 rect, 130 135 fill: surface_fill(ctx, kind), ··· 150 155 if dismissible { 151 156 let close_id = id.child(WidgetKey::new("close")); 152 157 let close_rect = close_button_rect(rect); 153 - let interaction = ctx.interact(InteractDeclaration::new( 154 - close_id, 155 - close_rect, 156 - Sense::INTERACTIVE, 157 - )); 158 - if interaction.click() { 158 + let interaction = ctx.interact( 159 + InteractDeclaration::new(close_id, close_rect, Sense::INTERACTIVE) 160 + .focusable(true) 161 + .a11y(AccessNode::new(Role::Button).with_label(StringKey::new("toast.close"))), 162 + ); 163 + let live_focused = ctx.is_focused(close_id); 164 + let key_activated = live_focused 165 + && take_key( 166 + ctx.input, 167 + &[TakeKey::named(NamedKey::Enter), TakeKey::named(NamedKey::Space)], 168 + ) 169 + .is_some(); 170 + if interaction.click() || key_activated { 159 171 state.dismissed = true; 160 172 dismissed_now = true; 161 173 } ··· 175 187 kind: GlyphMark::Close, 176 188 color: ctx.theme().colors.text_secondary(), 177 189 }); 190 + push_focus_ring(ctx, &mut paint, close_rect, ctx.theme().radius.sm, live_focused); 178 191 } 179 192 ToastResponse { 180 193 visible: true, ··· 304 317 let table = HotkeyTable::new(); 305 318 let mut hits = HitFrame::new(); 306 319 let response = { 320 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 307 321 let mut ctx = FrameCtx::new( 308 322 theme, 309 323 snap, ··· 312 326 StringTable::empty(), 313 327 &mut hits, 314 328 prev, 329 + &mut a11y, 315 330 ); 316 331 show_toast( 317 332 &mut ctx,
+22 -1
crates/bone-ui/src/widgets/toggle_button.rs
··· 1 + use crate::a11y::{AccessNode, Role, Toggled}; 1 2 use crate::frame::{FrameCtx, InteractDeclaration}; 2 3 use crate::hit_test::{Interaction, Sense}; 3 4 use crate::layout::LayoutRect; ··· 51 52 InteractDeclaration::new(toggle.id, toggle.rect, Sense::INTERACTIVE) 52 53 .focusable(interactive) 53 54 .disabled(!interactive) 54 - .active(toggle.on), 55 + .active(toggle.on) 56 + .a11y( 57 + AccessNode::new(Role::Switch) 58 + .with_label(toggle.label) 59 + .with_disabled(!interactive) 60 + .with_toggled(if toggle.on { 61 + Toggled::True 62 + } else { 63 + Toggled::False 64 + }), 65 + ), 55 66 ); 56 67 let live_focused = ctx.is_focused(toggle.id); 57 68 let toggled = ··· 130 141 hits.clear(); 131 142 let toggle = ToggleButton::new(id_widget(), rect(), LABEL, *on); 132 143 let response = { 144 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 133 145 let mut ctx = FrameCtx::new( 134 146 theme.clone(), 135 147 snap, ··· 138 150 StringTable::empty(), 139 151 hits, 140 152 state, 153 + &mut a11y, 141 154 ); 142 155 show_toggle_button(&mut ctx, toggle) 143 156 }; ··· 221 234 )); 222 235 let toggle = ToggleButton::new(id_widget(), rect(), LABEL, false); 223 236 let response = { 237 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 224 238 let mut ctx = FrameCtx::new( 225 239 theme, 226 240 &mut input, ··· 229 243 StringTable::empty(), 230 244 &mut hits, 231 245 &state, 246 + &mut a11y, 232 247 ); 233 248 show_toggle_button(&mut ctx, toggle) 234 249 }; ··· 254 269 .map(|mut snap| { 255 270 hits.clear(); 256 271 let response = { 272 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 257 273 let mut ctx = FrameCtx::new( 258 274 theme.clone(), 259 275 &mut snap, ··· 262 278 StringTable::empty(), 263 279 &mut hits, 264 280 &state, 281 + &mut a11y, 265 282 ); 266 283 show_toggle_button(&mut ctx, toggle) 267 284 }; ··· 318 335 let mut input = InputSnapshot::idle(FrameInstant::ZERO); 319 336 let toggle = ToggleButton::new(id_widget(), rect(), LABEL, true); 320 337 let response = { 338 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 321 339 let mut ctx = FrameCtx::new( 322 340 theme.clone(), 323 341 &mut input, ··· 326 344 StringTable::empty(), 327 345 &mut hits, 328 346 &state, 347 + &mut a11y, 329 348 ); 330 349 show_toggle_button(&mut ctx, toggle) 331 350 }; ··· 342 361 )); 343 362 let toggle = ToggleButton::new(id_widget(), rect(), LABEL, false); 344 363 let response = { 364 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 345 365 let mut ctx = FrameCtx::new( 346 366 theme.clone(), 347 367 &mut input, ··· 350 370 StringTable::empty(), 351 371 &mut hits, 352 372 &state, 373 + &mut a11y, 353 374 ); 354 375 show_toggle_button(&mut ctx, toggle) 355 376 };
+28 -2
crates/bone-ui/src/widgets/toolbar.rs
··· 1 + use crate::a11y::{AccessNode, Role}; 1 2 use crate::frame::{FrameCtx, InteractDeclaration}; 2 3 use crate::hit_test::Sense; 3 4 use crate::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize}; ··· 49 50 pub struct Toolbar<'a> { 50 51 pub id: WidgetId, 51 52 pub rect: LayoutRect, 53 + pub label: StringKey, 52 54 pub items: &'a [ToolbarItem], 53 55 pub item_size: LayoutPx, 54 56 pub item_gap: LayoutPx, ··· 60 62 pub const fn horizontal( 61 63 id: WidgetId, 62 64 rect: LayoutRect, 65 + label: StringKey, 63 66 items: &'a [ToolbarItem], 64 67 item_size: LayoutPx, 65 68 item_gap: LayoutPx, ··· 67 70 Self { 68 71 id, 69 72 rect, 73 + label, 70 74 items, 71 75 item_size, 72 76 item_gap, ··· 78 82 pub const fn vertical( 79 83 id: WidgetId, 80 84 rect: LayoutRect, 85 + label: StringKey, 81 86 items: &'a [ToolbarItem], 82 87 item_size: LayoutPx, 83 88 item_gap: LayoutPx, ··· 85 90 Self { 86 91 id, 87 92 rect, 93 + label, 88 94 items, 89 95 item_size, 90 96 item_gap, ··· 111 117 let Toolbar { 112 118 id, 113 119 rect, 120 + label, 114 121 items, 115 122 item_size, 116 123 item_gap, ··· 118 125 } = toolbar; 119 126 let visible_count = compute_visible_count(rect, items.len(), item_size, item_gap, orientation); 120 127 let needs_overflow = visible_count < items.len(); 128 + ctx.a11y.push( 129 + id, 130 + rect, 131 + AccessNode::new(Role::Toolbar).with_label(label), 132 + ); 121 133 let mut paint = Vec::new(); 122 134 let mut activated: Option<WidgetId> = None; 123 135 items ··· 139 151 let interaction = ctx.interact( 140 152 InteractDeclaration::new(overflow_id, overflow_rect, Sense::INTERACTIVE) 141 153 .focusable(true) 142 - .active(*overflow_open), 154 + .active(*overflow_open) 155 + .a11y( 156 + AccessNode::new(Role::Button) 157 + .with_label(StringKey::new("toolbar.overflow")) 158 + .with_expanded(*overflow_open), 159 + ), 143 160 ); 144 161 let live_focused = ctx.is_focused(overflow_id); 145 162 if interaction.click() { ··· 198 215 InteractDeclaration::new(item.id, rect, Sense::INTERACTIVE) 199 216 .focusable(true) 200 217 .disabled(item.disabled) 201 - .active(item.active), 218 + .active(item.active) 219 + .a11y( 220 + AccessNode::new(Role::Button) 221 + .with_label(item.label) 222 + .with_disabled(item.disabled) 223 + .with_selected(item.active), 224 + ), 202 225 ); 203 226 let live_focused = ctx.is_focused(item.id); 204 227 let activated_via_pointer = !item.disabled && interaction.click(); ··· 366 389 let table = HotkeyTable::new(); 367 390 let mut hits = HitFrame::new(); 368 391 let response = { 392 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 369 393 let mut ctx = FrameCtx::new( 370 394 theme, 371 395 snap, ··· 374 398 StringTable::empty(), 375 399 &mut hits, 376 400 prev, 401 + &mut a11y, 377 402 ); 378 403 show_toolbar( 379 404 &mut ctx, 380 405 Toolbar::horizontal( 381 406 toolbar_id(), 382 407 rect, 408 + StringKey::new("test.toolbar"), 383 409 items, 384 410 LayoutPx::new(28.0), 385 411 LayoutPx::new(4.0),
+9 -1
crates/bone-ui/src/widgets/tooltip.rs
··· 2 2 3 3 use serde::Serialize; 4 4 5 + use crate::a11y::{AccessNode, Role}; 5 6 use crate::frame::FrameCtx; 6 7 use crate::hit_test::Interaction; 7 8 use crate::input::FrameInstant; 8 9 use crate::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize}; 9 10 use crate::strings::StringKey; 10 - use crate::widget_id::WidgetId; 11 + use crate::widget_id::{WidgetId, WidgetKey}; 11 12 12 13 use super::paint::{LabelText, WidgetPaint}; 13 14 ··· 81 82 tooltip.placement, 82 83 tooltip.gap, 83 84 tooltip.size, 85 + ); 86 + ctx.a11y.push( 87 + tooltip.anchor.child(WidgetKey::new("tooltip")), 88 + rect, 89 + AccessNode::new(Role::Tooltip).with_label(tooltip.label), 84 90 ); 85 91 vec![WidgetPaint::Tooltip { 86 92 rect, ··· 195 201 let table = HotkeyTable::new(); 196 202 let mut hits = HitFrame::new(); 197 203 let mut input = InputSnapshot::idle(frame); 204 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 198 205 let mut ctx = FrameCtx::new( 199 206 theme, 200 207 &mut input, ··· 203 210 StringTable::empty(), 204 211 &mut hits, 205 212 prev, 213 + &mut a11y, 206 214 ); 207 215 show_tooltip(&mut ctx, tooltip, state) 208 216 }
+34 -7
crates/bone-ui/src/widgets/tree_view.rs
··· 1 1 use std::collections::BTreeSet; 2 2 3 + use crate::a11y::{AccessNode, Role}; 3 4 use crate::frame::{FrameCtx, InteractDeclaration}; 4 5 use crate::hit_test::Sense; 5 6 use crate::input::{KeyCode, ModifierMask, NamedKey}; ··· 80 81 pub struct TreeView<'a, 'state> { 81 82 pub id: WidgetId, 82 83 pub rect: LayoutRect, 84 + pub label: StringKey, 83 85 pub roots: &'a [TreeNode], 84 86 pub state: &'state mut TreeViewState, 85 87 pub mode: TreeSelectionMode, ··· 92 94 pub const fn new( 93 95 id: WidgetId, 94 96 rect: LayoutRect, 97 + label: StringKey, 95 98 roots: &'a [TreeNode], 96 99 state: &'state mut TreeViewState, 97 100 ) -> Self { 98 101 Self { 99 102 id, 100 103 rect, 104 + label, 101 105 roots, 102 106 state, 103 107 mode: TreeSelectionMode::Single, ··· 131 135 #[must_use] 132 136 pub fn show_tree_view(ctx: &mut FrameCtx<'_>, view: TreeView<'_, '_>) -> TreeViewResponse { 133 137 let TreeView { 134 - id: _, 138 + id, 135 139 rect, 140 + label, 136 141 roots, 137 142 state, 138 143 mode, 139 144 row_height, 140 145 indent_step, 141 146 } = view; 147 + ctx.a11y 148 + .push(id, rect, AccessNode::new(Role::Tree).with_label(label)); 142 149 let mut paint = vec![WidgetPaint::Surface { 143 150 rect, 144 151 fill: ctx.theme().colors.surface(crate::theme::SurfaceLevel::L0), ··· 282 289 let indent = LayoutPx::new(row.depth as f32 * indent_step.value()); 283 290 let disclosure_rect = disclosure_rect_at(row_rect, indent); 284 291 let label_rect = label_rect_at(row_rect, indent); 292 + let selected = state.selection.contains(&row.id); 293 + let expanded = state.expanded.contains(&row.id); 285 294 let interaction = ctx.interact( 286 295 InteractDeclaration::new(row.id, row_rect, Sense::DRAGGABLE) 287 296 .focusable(false) 288 - .active(state.selection.contains(&row.id)), 297 + .active(selected) 298 + .a11y({ 299 + let node = AccessNode::new(Role::TreeItem) 300 + .with_label(row.label) 301 + .with_selected(selected); 302 + if row.has_children { 303 + node.with_expanded(expanded) 304 + } else { 305 + node 306 + } 307 + }), 289 308 ); 290 309 let live_focused = ctx.is_focused(row.id); 291 310 apply_row_interaction( ··· 401 420 state: &mut TreeViewState, 402 421 ) -> Vec<WidgetPaint> { 403 422 let disclosure_id = row.id.child(WidgetKey::new("disclosure")); 404 - let disclosure_interaction = ctx.interact(InteractDeclaration::new( 405 - disclosure_id, 406 - disclosure_rect, 407 - Sense::INTERACTIVE, 408 - )); 423 + let disclosure_interaction = ctx.interact( 424 + InteractDeclaration::new(disclosure_id, disclosure_rect, Sense::INTERACTIVE).a11y( 425 + AccessNode::new(Role::DisclosureTriangle) 426 + .with_label(row.label) 427 + .with_expanded(state.expanded.contains(&row.id)), 428 + ), 429 + ); 409 430 if disclosure_interaction.click() { 410 431 toggle_expanded(state, row.id); 411 432 } ··· 691 712 LayoutSize::new(LayoutPx::new(200.0), LayoutPx::new(400.0)), 692 713 ); 693 714 let response = { 715 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 694 716 let mut ctx = FrameCtx::new( 695 717 theme, 696 718 snap, ··· 699 721 StringTable::empty(), 700 722 &mut hits, 701 723 prev, 724 + &mut a11y, 702 725 ); 703 726 show_tree_view( 704 727 &mut ctx, 705 728 TreeView::new( 706 729 WidgetId::ROOT.child(WidgetKey::new("tree")), 707 730 rect, 731 + StringKey::new("test.tree"), 708 732 roots, 709 733 state, 710 734 ), ··· 831 855 snap.modifiers = ModifierMask::CTRL; 832 856 let mut hits = HitFrame::new(); 833 857 { 858 + let mut a11y = crate::a11y::AccessTreeBuilder::new(); 834 859 let mut ctx = FrameCtx::new( 835 860 theme.clone(), 836 861 &mut snap, ··· 839 864 StringTable::empty(), 840 865 &mut hits, 841 866 &prev, 867 + &mut a11y, 842 868 ); 843 869 let _ = show_tree_view( 844 870 &mut ctx, 845 871 TreeView::new( 846 872 WidgetId::ROOT.child(WidgetKey::new("tree")), 847 873 rect, 874 + StringKey::new("test.tree"), 848 875 &roots, 849 876 &mut state, 850 877 )