Add initial accessibility support (#244)

Add AccessKit dependency.
Add accesskit_winit dependency.
Add methods to the Widget trait which create the accessibility tree and
react to accessibility events.
This commit is contained in:
Olivier FAURE 2024-05-04 10:17:49 +00:00 committed by GitHub
parent 392dbf5338
commit 6880515b95
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 1907 additions and 156 deletions

1047
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -34,6 +34,8 @@ smallvec = "1.13.2"
fnv = "1.0.7" fnv = "1.0.7"
instant = "0.1.6" instant = "0.1.6"
bitflags = "2.0.0" bitflags = "2.0.0"
accesskit = "0.14.0"
accesskit_winit = "0.20.0"
[package] [package]
name = "xilem_classic" name = "xilem_classic"

View File

@ -36,7 +36,9 @@ pollster = "0.3.0"
unicode-segmentation = "1.11.0" unicode-segmentation = "1.11.0"
# TODO: Is this still the most up-to-date crate for this? # TODO: Is this still the most up-to-date crate for this?
xi-unicode = "0.3.0" xi-unicode = "0.3.0"
tracing-subscriber = "0.3.18" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
accesskit.workspace = true
accesskit_winit.workspace = true
[dev-dependencies] [dev-dependencies]
float-cmp = { version = "0.8.0", features = ["std"], default-features = false } float-cmp = { version = "0.8.0", features = ["std"], default-features = false }

View File

@ -5,20 +5,22 @@
// On Windows platform, don't show a console when opening the app. // On Windows platform, don't show a console when opening the app.
#![windows_subsystem = "windows"] #![windows_subsystem = "windows"]
#![allow(clippy::single_match)]
use std::sync::Arc; use std::sync::Arc;
use accesskit::{DefaultActionVerb, Role};
use masonry::app_driver::{AppDriver, DriverCtx}; use masonry::app_driver::{AppDriver, DriverCtx};
use masonry::widget::{Align, CrossAxisAlignment, Flex, Label, SizedBox, WidgetRef}; use masonry::widget::{Align, CrossAxisAlignment, Flex, Label, SizedBox, WidgetRef};
use masonry::{ use masonry::{
Action, BoxConstraints, Color, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Point, AccessCtx, AccessEvent, Action, BoxConstraints, Color, EventCtx, LayoutCtx, LifeCycle,
PointerEvent, Size, StatusChange, TextEvent, Widget, WidgetId, WidgetPod, LifeCycleCtx, PaintCtx, Point, PointerEvent, Size, StatusChange, TextEvent, Widget, WidgetId,
WidgetPod,
}; };
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use tracing::{trace, trace_span, Span}; use tracing::{trace, trace_span, Span};
use vello::Scene; use vello::Scene;
use winit::dpi::LogicalSize; use winit::dpi::LogicalSize;
use winit::event_loop::EventLoop;
use winit::window::Window; use winit::window::Window;
#[derive(Clone)] #[derive(Clone)]
@ -170,6 +172,19 @@ impl Widget for CalcButton {
self.inner.on_text_event(ctx, event); self.inner.on_text_event(ctx, event);
} }
fn on_access_event(&mut self, ctx: &mut EventCtx, event: &AccessEvent) {
if event.target == ctx.widget_id() {
match event.action {
accesskit::Action::Default => {
ctx.submit_action(Action::Other(Arc::new(self.action)));
ctx.request_paint();
}
_ => {}
}
}
ctx.skip_child(&mut self.inner);
}
fn on_status_change(&mut self, ctx: &mut LifeCycleCtx, event: &StatusChange) { fn on_status_change(&mut self, ctx: &mut LifeCycleCtx, event: &StatusChange) {
match event { match event {
StatusChange::HotChanged(true) => { StatusChange::HotChanged(true) => {
@ -200,6 +215,23 @@ impl Widget for CalcButton {
self.inner.paint(ctx, scene); self.inner.paint(ctx, scene);
} }
fn accessibility_role(&self) -> Role {
Role::Button
}
fn accessibility(&mut self, ctx: &mut AccessCtx) {
let _name = match self.action {
CalcAction::Digit(digit) => digit.to_string(),
CalcAction::Op(op) => op.to_string(),
};
// We may want to add a name if it doesn't interfere with the child label
// ctx.current_node().set_name(name);
ctx.current_node()
.set_default_action_verb(DefaultActionVerb::Click);
self.inner.accessibility(ctx);
}
fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> { fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> {
smallvec![self.inner.as_dyn()] smallvec![self.inner.as_dyn()]
} }
@ -280,7 +312,7 @@ fn flex_row(
} }
fn build_calc() -> impl Widget { fn build_calc() -> impl Widget {
let display = Label::new("").with_text_size(32.0); let display = Label::new(String::new()).with_text_size(32.0);
Flex::column() Flex::column()
.with_flex_spacer(0.2) .with_flex_spacer(0.2)
.with_child(display) .with_child(display)
@ -338,18 +370,12 @@ fn build_calc() -> impl Widget {
} }
pub fn main() { pub fn main() {
let event_loop = EventLoop::new().unwrap();
let window_size = LogicalSize::new(223., 300.); let window_size = LogicalSize::new(223., 300.);
#[allow(deprecated)] let window_attributes = Window::default_attributes()
let window = event_loop .with_title("Simple Calculator")
.create_window( .with_resizable(true)
Window::default_attributes() .with_min_inner_size(window_size);
.with_title("Simple Calculator")
.with_resizable(true)
.with_min_inner_size(window_size),
)
.unwrap();
let calc_state = CalcState { let calc_state = CalcState {
value: "0".to_string(), value: "0".to_string(),
@ -358,5 +384,5 @@ pub fn main() {
in_num: false, in_num: false,
}; };
masonry::event_loop_runner::run(build_calc(), window, event_loop, calc_state).unwrap(); masonry::event_loop_runner::run(window_attributes, build_calc(), calc_state).unwrap();
} }

View File

@ -7,13 +7,15 @@
// On Windows platform, don't show a console when opening the app. // On Windows platform, don't show a console when opening the app.
#![windows_subsystem = "windows"] #![windows_subsystem = "windows"]
use accesskit::Role;
use kurbo::Stroke; use kurbo::Stroke;
use masonry::app_driver::{AppDriver, DriverCtx}; use masonry::app_driver::{AppDriver, DriverCtx};
use masonry::kurbo::BezPath; use masonry::kurbo::BezPath;
use masonry::widget::{FillStrat, WidgetRef}; use masonry::widget::{FillStrat, WidgetRef};
use masonry::{ use masonry::{
Action, Affine, BoxConstraints, Color, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, AccessCtx, AccessEvent, Action, Affine, BoxConstraints, Color, EventCtx, LayoutCtx, LifeCycle,
Point, PointerEvent, Rect, Size, StatusChange, TextEvent, Widget, WidgetId, LifeCycleCtx, PaintCtx, Point, PointerEvent, Rect, Size, StatusChange, TextEvent, Widget,
WidgetId,
}; };
use parley::layout::Alignment; use parley::layout::Alignment;
use parley::style::{FontFamily, FontStack, StyleProperty}; use parley::style::{FontFamily, FontStack, StyleProperty};
@ -21,7 +23,6 @@ use smallvec::SmallVec;
use tracing::{trace_span, Span}; use tracing::{trace_span, Span};
use vello::peniko::{Brush, Fill, Format, Image}; use vello::peniko::{Brush, Fill, Format, Image};
use vello::Scene; use vello::Scene;
use winit::event_loop::EventLoop;
use winit::window::Window; use winit::window::Window;
struct Driver; struct Driver;
@ -40,6 +41,8 @@ impl Widget for CustomWidget {
fn on_text_event(&mut self, _ctx: &mut EventCtx, _event: &TextEvent) {} fn on_text_event(&mut self, _ctx: &mut EventCtx, _event: &TextEvent) {}
fn on_access_event(&mut self, _ctx: &mut EventCtx, _event: &AccessEvent) {}
fn lifecycle(&mut self, _ctx: &mut LifeCycleCtx, _event: &LifeCycle) {} fn lifecycle(&mut self, _ctx: &mut LifeCycleCtx, _event: &LifeCycle) {}
fn on_status_change(&mut self, _ctx: &mut LifeCycleCtx, _event: &StatusChange) {} fn on_status_change(&mut self, _ctx: &mut LifeCycleCtx, _event: &StatusChange) {}
@ -126,6 +129,17 @@ impl Widget for CustomWidget {
scene.draw_image(&image_data, transform); scene.draw_image(&image_data, transform);
} }
fn accessibility_role(&self) -> Role {
Role::Canvas
}
fn accessibility(&mut self, ctx: &mut AccessCtx) {
let text = &self.0;
ctx.current_node().set_name(
format!("This is a demo of the Masonry Widget trait. Masonry has accessibility tree support. The demo shows colored shapes with the text '{text}'."),
);
}
fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> { fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> {
SmallVec::new() SmallVec::new()
} }
@ -137,13 +151,9 @@ impl Widget for CustomWidget {
pub fn main() { pub fn main() {
let my_string = "Masonry + Vello".to_string(); let my_string = "Masonry + Vello".to_string();
let event_loop = EventLoop::new().unwrap(); let window_attributes = Window::default_attributes().with_title("Fancy colors");
#[allow(deprecated)]
let window = event_loop
.create_window(Window::default_attributes().with_title("Fancy colots"))
.unwrap();
masonry::event_loop_runner::run(CustomWidget(my_string), window, event_loop, Driver).unwrap(); masonry::event_loop_runner::run(window_attributes, CustomWidget(my_string), Driver).unwrap();
} }
fn make_image_data(width: usize, height: usize) -> Vec<u8> { fn make_image_data(width: usize, height: usize) -> Vec<u8> {

View File

@ -12,7 +12,6 @@ use masonry::widget::prelude::*;
use masonry::widget::{Button, Flex, Label}; use masonry::widget::{Button, Flex, Label};
use masonry::Action; use masonry::Action;
use winit::dpi::LogicalSize; use winit::dpi::LogicalSize;
use winit::event_loop::EventLoop;
use winit::window::Window; use winit::window::Window;
const VERTICAL_WIDGET_SPACING: f64 = 20.0; const VERTICAL_WIDGET_SPACING: f64 = 20.0;
@ -33,19 +32,13 @@ impl AppDriver for Driver {
} }
pub fn main() { pub fn main() {
let event_loop = EventLoop::new().unwrap();
let window_size = LogicalSize::new(400.0, 400.0); let window_size = LogicalSize::new(400.0, 400.0);
#[allow(deprecated)] let window_attributes = Window::default_attributes()
let window = event_loop .with_title("Hello World!")
.create_window( .with_resizable(true)
Window::default_attributes() .with_min_inner_size(window_size);
.with_title("Hello World!")
.with_resizable(true)
.with_min_inner_size(window_size),
)
.unwrap();
masonry::event_loop_runner::run(build_root_widget(), window, event_loop, Driver).unwrap(); masonry::event_loop_runner::run(window_attributes, build_root_widget(), Driver).unwrap();
} }
fn build_root_widget() -> impl Widget { fn build_root_widget() -> impl Widget {

View File

@ -13,7 +13,6 @@ use masonry::widget::{FillStrat, Image};
use masonry::{Action, WidgetId}; use masonry::{Action, WidgetId};
use vello::peniko::{Format, Image as ImageBuf}; use vello::peniko::{Format, Image as ImageBuf};
use winit::dpi::LogicalSize; use winit::dpi::LogicalSize;
use winit::event_loop::EventLoop;
use winit::window::Window; use winit::window::Window;
struct Driver; struct Driver;
@ -29,17 +28,11 @@ pub fn main() {
let png_data = ImageBuf::new(image_data.to_vec().into(), Format::Rgba8, width, height); let png_data = ImageBuf::new(image_data.to_vec().into(), Format::Rgba8, width, height);
let image = Image::new(png_data).fill_mode(FillStrat::Contain); let image = Image::new(png_data).fill_mode(FillStrat::Contain);
let event_loop = EventLoop::new().unwrap();
let window_size = LogicalSize::new(650.0, 450.0); let window_size = LogicalSize::new(650.0, 450.0);
#[allow(deprecated)] let window_attributes = Window::default_attributes()
let window = event_loop .with_title("Simple image example")
.create_window( .with_min_inner_size(window_size)
Window::default_attributes() .with_max_inner_size(window_size);
.with_title("Simple image example")
.with_min_inner_size(window_size)
.with_max_inner_size(window_size),
)
.unwrap();
masonry::event_loop_runner::run(image, window, event_loop, Driver).unwrap(); masonry::event_loop_runner::run(window_attributes, image, Driver).unwrap();
} }

View File

@ -6,6 +6,7 @@
use std::any::Any; use std::any::Any;
use std::time::Duration; use std::time::Duration;
use accesskit::{NodeBuilder, TreeUpdate};
use parley::FontContext; use parley::FontContext;
use tracing::{trace, warn}; use tracing::{trace, warn};
use winit::dpi::LogicalPosition; use winit::dpi::LogicalPosition;
@ -86,6 +87,14 @@ pub struct PaintCtx<'a> {
pub(crate) debug_widget: bool, pub(crate) debug_widget: bool,
} }
pub struct AccessCtx<'a> {
pub(crate) global_state: &'a mut RenderRootState,
pub(crate) widget_state: &'a WidgetState,
pub(crate) tree_update: &'a mut TreeUpdate,
pub(crate) current_node: NodeBuilder,
pub(crate) rebuild_all: bool,
}
pub struct WorkerCtx<'a> { pub struct WorkerCtx<'a> {
// TODO // TODO
#[allow(dead_code)] #[allow(dead_code)]
@ -100,6 +109,7 @@ impl_context_method!(
LifeCycleCtx<'_>, LifeCycleCtx<'_>,
PaintCtx<'_>, PaintCtx<'_>,
LayoutCtx<'_>, LayoutCtx<'_>,
AccessCtx<'_>,
{ {
/// get the `WidgetId` of the current widget. /// get the `WidgetId` of the current widget.
pub fn widget_id(&self) -> WidgetId { pub fn widget_id(&self) -> WidgetId {
@ -365,6 +375,12 @@ impl_context_method!(WidgetCtx<'_>, EventCtx<'_>, LifeCycleCtx<'_>, {
self.widget_state.needs_layout = true; self.widget_state.needs_layout = true;
} }
pub fn request_accessibility_update(&mut self) {
trace!("request_accessibility_update");
self.widget_state.needs_accessibility_update = true;
self.widget_state.request_accessibility_update = true;
}
/// Request an animation frame. /// Request an animation frame.
pub fn request_anim_frame(&mut self) { pub fn request_anim_frame(&mut self) {
trace!("request_anim_frame"); trace!("request_anim_frame");
@ -662,3 +678,19 @@ impl PaintCtx<'_> {
self.depth self.depth
} }
} }
impl AccessCtx<'_> {
pub fn current_node(&mut self) -> &mut NodeBuilder {
&mut self.current_node
}
/// Report whether accessibility was requested on this widget.
///
/// This method is primarily intended for containers. The `accessibility`
/// method will be called on a widget when it or any of its descendants
/// have seen a request. However, in many cases a container need not push
/// a node for itself.
pub fn is_requested(&self) -> bool {
self.widget_state.needs_accessibility_update
}
}

View File

@ -9,6 +9,7 @@ use crate::WidgetId;
use std::{collections::HashSet, path::PathBuf}; use std::{collections::HashSet, path::PathBuf};
use accesskit::{Action, ActionData};
use winit::dpi::{LogicalPosition, PhysicalPosition, PhysicalSize}; use winit::dpi::{LogicalPosition, PhysicalPosition, PhysicalSize};
use winit::event::{Ime, KeyEvent, Modifiers, MouseButton}; use winit::event::{Ime, KeyEvent, Modifiers, MouseButton};
use winit::keyboard::ModifiersState; use winit::keyboard::ModifiersState;
@ -56,6 +57,14 @@ pub enum TextEvent {
FocusChange(bool), FocusChange(bool),
} }
#[derive(Debug, Clone)]
pub struct AccessEvent {
// TODO - Split out widget id from AccessEvent
pub target: WidgetId,
pub action: Action,
pub data: Option<ActionData>,
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PointerState { pub struct PointerState {
// TODO // TODO
@ -230,6 +239,20 @@ impl PointerEvent {
PointerEvent::HoverFileCancel(_) => "HoverFileCancel", PointerEvent::HoverFileCancel(_) => "HoverFileCancel",
} }
} }
pub fn is_high_density(&self) -> bool {
match self {
PointerEvent::PointerDown(_, _) => false,
PointerEvent::PointerUp(_, _) => false,
PointerEvent::PointerMove(_) => true,
PointerEvent::PointerEnter(_) => false,
PointerEvent::PointerLeave(_) => false,
PointerEvent::MouseWheel(_, _) => true,
PointerEvent::HoverFile(_, _) => true,
PointerEvent::DropFile(_, _) => false,
PointerEvent::HoverFileCancel(_) => false,
}
}
} }
impl TextEvent { impl TextEvent {
@ -241,6 +264,48 @@ impl TextEvent {
TextEvent::FocusChange(_) => "FocusChange", TextEvent::FocusChange(_) => "FocusChange",
} }
} }
pub fn is_high_density(&self) -> bool {
match self {
TextEvent::KeyboardKey(event, _) => event.repeat,
TextEvent::Ime(_) => false,
TextEvent::ModifierChange(_) => false,
TextEvent::FocusChange(_) => false,
}
}
}
impl AccessEvent {
pub fn short_name(&self) -> &'static str {
match self.action {
accesskit::Action::Default => "Default",
accesskit::Action::Focus => "Focus",
accesskit::Action::Blur => "Blur",
accesskit::Action::Collapse => "Collapse",
accesskit::Action::Expand => "Expand",
accesskit::Action::CustomAction => "CustomAction",
accesskit::Action::Decrement => "Decrement",
accesskit::Action::Increment => "Increment",
accesskit::Action::HideTooltip => "HideTooltip",
accesskit::Action::ShowTooltip => "ShowTooltip",
accesskit::Action::ReplaceSelectedText => "ReplaceSelectedText",
accesskit::Action::ScrollBackward => "ScrollBackward",
accesskit::Action::ScrollDown => "ScrollDown",
accesskit::Action::ScrollForward => "ScrollForward",
accesskit::Action::ScrollLeft => "ScrollLeft",
accesskit::Action::ScrollRight => "ScrollRight",
accesskit::Action::ScrollUp => "ScrollUp",
accesskit::Action::ScrollIntoView => "ScrollIntoView",
accesskit::Action::ScrollToPoint => "ScrollToPoint",
accesskit::Action::SetScrollOffset => "SetScrollOffset",
accesskit::Action::SetTextSelection => "SetTextSelection",
accesskit::Action::SetSequentialFocusNavigationStartingPoint => {
"SetSequentialFocusNavigationStartingPoint"
}
accesskit::Action::SetValue => "SetValue",
accesskit::Action::ShowContextMenu => "ShowContextMenu",
}
}
} }
impl PointerState { impl PointerState {

View File

@ -4,8 +4,9 @@
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use std::sync::Arc; use std::sync::Arc;
use accesskit_winit::Adapter;
use tracing::subscriber::SetGlobalDefaultError; use tracing::subscriber::SetGlobalDefaultError;
use tracing::warn; use tracing::{debug, warn};
use vello::kurbo::Affine; use vello::kurbo::Affine;
use vello::util::{RenderContext, RenderSurface}; use vello::util::{RenderContext, RenderSurface};
use vello::{peniko::Color, AaSupport, RenderParams, Renderer, RendererOptions, Scene}; use vello::{peniko::Color, AaSupport, RenderParams, Renderer, RendererOptions, Scene};
@ -15,7 +16,7 @@ use winit::dpi::LogicalPosition;
use winit::error::EventLoopError; use winit::error::EventLoopError;
use winit::event::WindowEvent as WinitWindowEvent; use winit::event::WindowEvent as WinitWindowEvent;
use winit::event_loop::{ActiveEventLoop, EventLoop}; use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::window::{Window, WindowId}; use winit::window::{Window, WindowAttributes, WindowId};
use crate::app_driver::{AppDriver, DriverCtx}; use crate::app_driver::{AppDriver, DriverCtx};
use crate::event::{PointerState, WindowEvent}; use crate::event::{PointerState, WindowEvent};
@ -30,12 +31,33 @@ struct MainState<'a> {
renderer: Option<Renderer>, renderer: Option<Renderer>,
pointer_state: PointerState, pointer_state: PointerState,
app_driver: Box<dyn AppDriver>, app_driver: Box<dyn AppDriver>,
accesskit_adapter: Adapter,
} }
pub fn run( pub fn run(
window_attributes: WindowAttributes,
root_widget: impl Widget, root_widget: impl Widget,
app_driver: impl AppDriver + 'static,
) -> Result<(), EventLoopError> {
let visible = window_attributes.visible;
let window_attributes = window_attributes.with_visible(false);
let event_loop = EventLoop::with_user_event().build()?;
#[allow(deprecated)]
let window = event_loop.create_window(window_attributes).unwrap();
let event_loop_proxy = event_loop.create_proxy();
let adapter = Adapter::with_event_loop_proxy(&window, event_loop_proxy);
window.set_visible(visible);
run_with(window, event_loop, adapter, root_widget, app_driver)
}
pub fn run_with(
window: Window, window: Window,
event_loop: EventLoop<()>, event_loop: EventLoop<accesskit_winit::Event>,
accesskit_adapter: Adapter,
root_widget: impl Widget,
app_driver: impl AppDriver + 'static, app_driver: impl AppDriver + 'static,
) -> Result<(), EventLoopError> { ) -> Result<(), EventLoopError> {
let window = Arc::new(window); let window = Arc::new(window);
@ -57,6 +79,7 @@ pub fn run(
renderer: None, renderer: None,
pointer_state: PointerState::empty(), pointer_state: PointerState::empty(),
app_driver: Box::new(app_driver), app_driver: Box::new(app_driver),
accesskit_adapter,
}; };
// If there is no default tracing subscriber, we set our own. If one has // If there is no default tracing subscriber, we set our own. If one has
@ -68,12 +91,14 @@ pub fn run(
event_loop.run_app(&mut main_state) event_loop.run_app(&mut main_state)
} }
impl ApplicationHandler for MainState<'_> { impl ApplicationHandler<accesskit_winit::Event> for MainState<'_> {
fn resumed(&mut self, _event_loop: &ActiveEventLoop) { fn resumed(&mut self, _event_loop: &ActiveEventLoop) {
// FIXME: initialize window in this handler because initializing it before running the event loop is deprecated // FIXME: initialize window in this handler because initializing it before running the event loop is deprecated
} }
fn window_event(&mut self, event_loop: &ActiveEventLoop, _: WindowId, event: WinitWindowEvent) { fn window_event(&mut self, event_loop: &ActiveEventLoop, _: WindowId, event: WinitWindowEvent) {
self.accesskit_adapter.process_event(&self.window, &event);
match event { match event {
WinitWindowEvent::RedrawRequested => { WinitWindowEvent::RedrawRequested => {
let scene = self.render_root.redraw(); let scene = self.render_root.redraw();
@ -149,10 +174,12 @@ impl ApplicationHandler for MainState<'_> {
} }
_ => (), _ => (),
} }
while let Some(signal) = self.render_root.pop_signal() { while let Some(signal) = self.render_root.pop_signal() {
match signal { match signal {
render_root::RenderRootSignal::Action(action, widget_id) => { render_root::RenderRootSignal::Action(action, widget_id) => {
self.render_root.edit_root_widget(|root| { self.render_root.edit_root_widget(|root| {
debug!("Action {:?} on widget {:?}", action, widget_id);
let mut driver_ctx = DriverCtx { let mut driver_ctx = DriverCtx {
main_root_widget: root, main_root_widget: root,
}; };
@ -203,6 +230,24 @@ impl ApplicationHandler for MainState<'_> {
} }
} }
} }
self.accesskit_adapter
.update_if_active(|| self.render_root.root_accessibility(false));
}
fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: accesskit_winit::Event) {
match event.window_event {
// Note that this event can be called at any time, even multiple times if
// the user restarts their screen reader.
accesskit_winit::WindowEvent::InitialTreeRequested => {
self.accesskit_adapter
.update_if_active(|| self.render_root.root_accessibility(true));
}
accesskit_winit::WindowEvent::ActionRequested(action_request) => {
self.render_root.root_on_access_event(action_request);
}
accesskit_winit::WindowEvent::AccessibilityDeactivated => {}
}
} }
} }
@ -264,17 +309,23 @@ pub(crate) fn try_init_tracing() -> Result<(), SetGlobalDefaultError> {
{ {
use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::filter::LevelFilter;
use tracing_subscriber::prelude::*; use tracing_subscriber::prelude::*;
let filter_layer = if cfg!(debug_assertions) { use tracing_subscriber::EnvFilter;
let default_level = if cfg!(debug_assertions) {
LevelFilter::DEBUG LevelFilter::DEBUG
} else { } else {
LevelFilter::INFO LevelFilter::INFO
}; };
let env_filter = EnvFilter::builder()
.with_default_directive(default_level.into())
.with_env_var("RUST_LOG")
.from_env_lossy();
let fmt_layer = tracing_subscriber::fmt::layer() let fmt_layer = tracing_subscriber::fmt::layer()
// Display target (eg "my_crate::some_mod::submod") with logs // Display target (eg "my_crate::some_mod::submod") with logs
.with_target(true); .with_target(true);
let registry = tracing_subscriber::registry() let registry = tracing_subscriber::registry()
.with(filter_layer) .with(env_filter)
.with(fmt_layer); .with(fmt_layer);
tracing::dispatcher::set_global_default(registry.into()) tracing::dispatcher::set_global_default(registry.into())
} }

View File

@ -118,8 +118,10 @@ pub mod text2;
pub use action::Action; pub use action::Action;
pub use box_constraints::BoxConstraints; pub use box_constraints::BoxConstraints;
pub use contexts::{EventCtx, LayoutCtx, LifeCycleCtx, PaintCtx, WidgetCtx}; pub use contexts::{AccessCtx, EventCtx, LayoutCtx, LifeCycleCtx, PaintCtx, WidgetCtx};
pub use event::{InternalLifeCycle, LifeCycle, PointerEvent, StatusChange, TextEvent, WindowTheme}; pub use event::{
AccessEvent, InternalLifeCycle, LifeCycle, PointerEvent, StatusChange, TextEvent, WindowTheme,
};
pub use kurbo::{Affine, Insets, Point, Rect, Size, Vec2}; pub use kurbo::{Affine, Insets, Point, Rect, Size, Vec2};
pub use parley::layout::Alignment as TextAlignment; pub use parley::layout::Alignment as TextAlignment;
pub use util::{AsAny, Handled}; pub use util::{AsAny, Handled};

View File

@ -3,11 +3,12 @@
use std::collections::VecDeque; use std::collections::VecDeque;
use accesskit::{ActionRequest, NodeBuilder, Tree, TreeUpdate};
// Automatically defaults to std::time::Instant on non Wasm platforms // Automatically defaults to std::time::Instant on non Wasm platforms
use instant::Instant; use instant::Instant;
use kurbo::Affine; use kurbo::Affine;
use parley::FontContext; use parley::FontContext;
use tracing::{info_span, warn}; use tracing::{debug, info_span, warn};
use vello::peniko::{Color, Fill}; use vello::peniko::{Color, Fill};
use vello::Scene; use vello::Scene;
use winit::dpi::{LogicalPosition, LogicalSize, PhysicalSize}; use winit::dpi::{LogicalPosition, LogicalSize, PhysicalSize};
@ -20,7 +21,8 @@ use crate::event::{PointerEvent, TextEvent, WindowEvent};
use crate::kurbo::Point; use crate::kurbo::Point;
use crate::widget::{WidgetMut, WidgetState}; use crate::widget::{WidgetMut, WidgetState};
use crate::{ use crate::{
Action, BoxConstraints, Handled, InternalLifeCycle, LifeCycle, Widget, WidgetId, WidgetPod, AccessCtx, AccessEvent, Action, BoxConstraints, Handled, InternalLifeCycle, LifeCycle, Widget,
WidgetId, WidgetPod,
}; };
// TODO - Remove pub(crate) // TODO - Remove pub(crate)
@ -202,7 +204,10 @@ impl RenderRoot {
widget: &mut self.root.inner, widget: &mut self.root.inner,
}; };
let res = f(root_widget); let res = {
let _span = info_span!("edit_root_widget").entered();
f(root_widget)
};
self.post_event_processing(&mut fake_widget_state); self.post_event_processing(&mut fake_widget_state);
res res
@ -229,8 +234,11 @@ impl RenderRoot {
let handled = { let handled = {
ctx.global_state ctx.global_state
.debug_logger .debug_logger
.push_important_span(&format!("¨POINTER_EVENT {}", event.short_name())); .push_important_span(&format!("POINTER_EVENT {}", event.short_name()));
let _span = info_span!("event").entered(); let _span = info_span!("pointer_event").entered();
if !event.is_high_density() {
debug!("Running ON_POINTER_EVENT pass with {}", event.short_name());
}
self.root.on_pointer_event(&mut ctx, &event); self.root.on_pointer_event(&mut ctx, &event);
ctx.global_state.debug_logger.pop_span(); ctx.global_state.debug_logger.pop_span();
Handled::from(ctx.is_handled) Handled::from(ctx.is_handled)
@ -269,7 +277,10 @@ impl RenderRoot {
ctx.global_state ctx.global_state
.debug_logger .debug_logger
.push_important_span(&format!("TEXT_EVENT {}", event.short_name())); .push_important_span(&format!("TEXT_EVENT {}", event.short_name()));
let _span = info_span!("event").entered(); let _span = info_span!("text_event").entered();
if !event.is_high_density() {
debug!("Running ON_TEXT_EVENT pass with {}", event.short_name());
}
self.root.on_text_event(&mut ctx, &event); self.root.on_text_event(&mut ctx, &event);
ctx.global_state.debug_logger.pop_span(); ctx.global_state.debug_logger.pop_span();
Handled::from(ctx.is_handled) Handled::from(ctx.is_handled)
@ -292,6 +303,41 @@ impl RenderRoot {
handled handled
} }
pub fn root_on_access_event(&mut self, event: ActionRequest) {
let mut widget_state =
WidgetState::new(self.root.id(), Some(self.get_kurbo_size()), "<root>");
let mut ctx = EventCtx {
global_state: &mut self.state,
widget_state: &mut widget_state,
is_handled: false,
request_pan_to_child: None,
};
let Ok(id) = event.target.0.try_into() else {
warn!("Received ActionRequest with id 0. This shouldn't be possible.");
return;
};
let event = AccessEvent {
target: WidgetId(id),
action: event.action,
data: event.data,
};
{
ctx.global_state
.debug_logger
.push_important_span(&format!("ACCESSS_EVENT {}", event.short_name()));
let _span = info_span!("access_event").entered();
debug!("Running ON_ACCESS_EVENT pass with {}", event.short_name());
self.root.on_access_event(&mut ctx, &event);
ctx.global_state.debug_logger.pop_span();
}
self.post_event_processing(&mut widget_state);
self.root.as_dyn().debug_validate(false);
}
fn root_lifecycle(&mut self, event: LifeCycle) { fn root_lifecycle(&mut self, event: LifeCycle) {
let mut widget_state = let mut widget_state =
WidgetState::new(self.root.id(), Some(self.get_kurbo_size()), "<root>"); WidgetState::new(self.root.id(), Some(self.get_kurbo_size()), "<root>");
@ -369,7 +415,10 @@ impl RenderRoot {
}; };
let mut scene = Scene::new(); let mut scene = Scene::new();
self.root.paint(&mut ctx, &mut scene); {
let _span = info_span!("paint").entered();
self.root.paint(&mut ctx, &mut scene);
}
// FIXME - This is a workaround to Vello panicking when given an // FIXME - This is a workaround to Vello panicking when given an
// empty scene // empty scene
@ -386,6 +435,44 @@ impl RenderRoot {
scene scene
} }
// TODO - Integrate in unit tests?
pub fn root_accessibility(&mut self, rebuild_all: bool) -> TreeUpdate {
let mut tree_update = TreeUpdate {
nodes: vec![],
tree: None,
focus: self.state.focused_widget.unwrap_or(self.root.id()).into(),
};
let mut widget_state =
WidgetState::new(self.root.id(), Some(self.get_kurbo_size()), "<root>");
let mut ctx = AccessCtx {
global_state: &mut self.state,
widget_state: &mut widget_state,
tree_update: &mut tree_update,
current_node: NodeBuilder::default(),
rebuild_all,
};
// TODO - tree_update.tree
{
let _span = info_span!("accessibility").entered();
if rebuild_all {
debug!("Running ACCESSIBILITY pass with rebuild_all");
}
self.root.accessibility(&mut ctx);
}
if true {
tree_update.tree = Some(Tree {
root: self.root.id().into(),
app_name: None,
toolkit_name: Some("Masonry".to_string()),
toolkit_version: Some(env!("CARGO_PKG_VERSION").to_string()),
});
}
tree_update
}
fn get_kurbo_size(&self) -> kurbo::Size { fn get_kurbo_size(&self) -> kurbo::Size {
let size = self.size.to_logical(self.scale_factor); let size = self.size.to_logical(self.scale_factor);
kurbo::Size::new(size.width, size.height) kurbo::Size::new(size.width, size.height)

View File

@ -15,6 +15,8 @@ use std::cell::RefCell;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::rc::Rc; use std::rc::Rc;
use accesskit::Role;
use accesskit_winit::Event;
use smallvec::SmallVec; use smallvec::SmallVec;
use vello::Scene; use vello::Scene;
@ -24,10 +26,13 @@ use crate::*;
pub type PointerEventFn<S> = dyn FnMut(&mut S, &mut EventCtx, &PointerEvent); pub type PointerEventFn<S> = dyn FnMut(&mut S, &mut EventCtx, &PointerEvent);
pub type TextEventFn<S> = dyn FnMut(&mut S, &mut EventCtx, &TextEvent); pub type TextEventFn<S> = dyn FnMut(&mut S, &mut EventCtx, &TextEvent);
pub type AccessEventFn<S> = dyn FnMut(&mut S, &mut EventCtx, &AccessEvent);
pub type StatusChangeFn<S> = dyn FnMut(&mut S, &mut LifeCycleCtx, &StatusChange); pub type StatusChangeFn<S> = dyn FnMut(&mut S, &mut LifeCycleCtx, &StatusChange);
pub type LifeCycleFn<S> = dyn FnMut(&mut S, &mut LifeCycleCtx, &LifeCycle); pub type LifeCycleFn<S> = dyn FnMut(&mut S, &mut LifeCycleCtx, &LifeCycle);
pub type LayoutFn<S> = dyn FnMut(&mut S, &mut LayoutCtx, &BoxConstraints) -> Size; pub type LayoutFn<S> = dyn FnMut(&mut S, &mut LayoutCtx, &BoxConstraints) -> Size;
pub type PaintFn<S> = dyn FnMut(&mut S, &mut PaintCtx, &mut Scene); pub type PaintFn<S> = dyn FnMut(&mut S, &mut PaintCtx, &mut Scene);
pub type RoleFn<S> = dyn Fn(&S) -> Role;
pub type AccessFn<S> = dyn FnMut(&mut S, &mut AccessCtx);
pub type ChildrenFn<S> = dyn Fn(&S) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]>; pub type ChildrenFn<S> = dyn Fn(&S) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]>;
#[cfg(FALSE)] #[cfg(FALSE)]
@ -40,10 +45,13 @@ pub struct ModularWidget<S> {
state: S, state: S,
on_pointer_event: Option<Box<PointerEventFn<S>>>, on_pointer_event: Option<Box<PointerEventFn<S>>>,
on_text_event: Option<Box<TextEventFn<S>>>, on_text_event: Option<Box<TextEventFn<S>>>,
on_access_event: Option<Box<AccessEventFn<S>>>,
on_status_change: Option<Box<StatusChangeFn<S>>>, on_status_change: Option<Box<StatusChangeFn<S>>>,
lifecycle: Option<Box<LifeCycleFn<S>>>, lifecycle: Option<Box<LifeCycleFn<S>>>,
layout: Option<Box<LayoutFn<S>>>, layout: Option<Box<LayoutFn<S>>>,
paint: Option<Box<PaintFn<S>>>, paint: Option<Box<PaintFn<S>>>,
role: Option<Box<RoleFn<S>>>,
access: Option<Box<AccessFn<S>>>,
children: Option<Box<ChildrenFn<S>>>, children: Option<Box<ChildrenFn<S>>>,
} }
@ -84,10 +92,12 @@ pub struct Recording(Rc<RefCell<VecDeque<Record>>>);
pub enum Record { pub enum Record {
PE(PointerEvent), PE(PointerEvent),
TE(TextEvent), TE(TextEvent),
AE(AccessEvent),
SC(StatusChange), SC(StatusChange),
L(LifeCycle), L(LifeCycle),
Layout(Size), Layout(Size),
Paint, Paint,
Access,
} }
/// like `WidgetExt` but just for this one thing /// like `WidgetExt` but just for this one thing
@ -112,10 +122,13 @@ impl<S> ModularWidget<S> {
state, state,
on_pointer_event: None, on_pointer_event: None,
on_text_event: None, on_text_event: None,
on_access_event: None,
on_status_change: None, on_status_change: None,
lifecycle: None, lifecycle: None,
layout: None, layout: None,
paint: None, paint: None,
role: None,
access: None,
children: None, children: None,
} }
} }
@ -136,6 +149,14 @@ impl<S> ModularWidget<S> {
self self
} }
pub fn access_event_fn(
mut self,
f: impl FnMut(&mut S, &mut EventCtx, &AccessEvent) + 'static,
) -> Self {
self.on_access_event = Some(Box::new(f));
self
}
pub fn status_change_fn( pub fn status_change_fn(
mut self, mut self,
f: impl FnMut(&mut S, &mut LifeCycleCtx, &StatusChange) + 'static, f: impl FnMut(&mut S, &mut LifeCycleCtx, &StatusChange) + 'static,
@ -165,6 +186,16 @@ impl<S> ModularWidget<S> {
self self
} }
pub fn role_fn(mut self, f: impl Fn(&S) -> Role + 'static) -> Self {
self.role = Some(Box::new(f));
self
}
pub fn access_fn(mut self, f: impl FnMut(&mut S, &mut AccessCtx) + 'static) -> Self {
self.access = Some(Box::new(f));
self
}
pub fn children_fn( pub fn children_fn(
mut self, mut self,
children: impl Fn(&S) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> + 'static, children: impl Fn(&S) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> + 'static,
@ -187,6 +218,12 @@ impl<S: 'static> Widget for ModularWidget<S> {
} }
} }
fn on_access_event(&mut self, ctx: &mut EventCtx, event: &AccessEvent) {
if let Some(f) = self.on_access_event.as_mut() {
f(&mut self.state, ctx, event);
}
}
fn on_status_change(&mut self, ctx: &mut LifeCycleCtx, event: &StatusChange) { fn on_status_change(&mut self, ctx: &mut LifeCycleCtx, event: &StatusChange) {
if let Some(f) = self.on_status_change.as_mut() { if let Some(f) = self.on_status_change.as_mut() {
f(&mut self.state, ctx, event); f(&mut self.state, ctx, event);
@ -211,6 +248,18 @@ impl<S: 'static> Widget for ModularWidget<S> {
.unwrap_or_else(|| Size::new(100., 100.)) .unwrap_or_else(|| Size::new(100., 100.))
} }
fn accessibility_role(&self) -> Role {
if let Some(f) = self.role.as_ref() {
f(&self.state)
} else {
Role::Unknown
}
}
fn accessibility(&mut self, ctx: &mut AccessCtx) {
todo!()
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut Scene) { fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut Scene) {
if let Some(f) = self.paint.as_mut() { if let Some(f) = self.paint.as_mut() {
f(&mut self.state, ctx, scene); f(&mut self.state, ctx, scene);
@ -256,6 +305,10 @@ impl Widget for ReplaceChild {
todo!() todo!()
} }
fn on_access_event(&mut self, ctx: &mut EventCtx, event: &AccessEvent) {
todo!()
}
fn on_status_change(&mut self, ctx: &mut LifeCycleCtx, _event: &StatusChange) { fn on_status_change(&mut self, ctx: &mut LifeCycleCtx, _event: &StatusChange) {
ctx.request_layout(); ctx.request_layout();
} }
@ -272,6 +325,14 @@ impl Widget for ReplaceChild {
self.child.paint(ctx, scene); self.child.paint(ctx, scene);
} }
fn accessibility_role(&self) -> Role {
Role::GenericContainer
}
fn accessibility(&mut self, ctx: &mut AccessCtx) {
self.child.accessibility(ctx);
}
fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> { fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> {
self.child.widget().children() self.child.widget().children()
} }
@ -319,6 +380,11 @@ impl<W: Widget> Widget for Recorder<W> {
self.child.on_text_event(ctx, event); self.child.on_text_event(ctx, event);
} }
fn on_access_event(&mut self, ctx: &mut EventCtx, event: &AccessEvent) {
self.recording.push(Record::AE(event.clone()));
self.child.on_access_event(ctx, event);
}
fn on_status_change(&mut self, ctx: &mut LifeCycleCtx, event: &StatusChange) { fn on_status_change(&mut self, ctx: &mut LifeCycleCtx, event: &StatusChange) {
self.recording.push(Record::SC(event.clone())); self.recording.push(Record::SC(event.clone()));
self.child.on_status_change(ctx, event); self.child.on_status_change(ctx, event);
@ -336,8 +402,17 @@ impl<W: Widget> Widget for Recorder<W> {
} }
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut Scene) { fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut Scene) {
self.child.paint(ctx, scene);
self.recording.push(Record::Paint); self.recording.push(Record::Paint);
self.child.paint(ctx, scene);
}
fn accessibility_role(&self) -> Role {
self.child.accessibility_role()
}
fn accessibility(&mut self, ctx: &mut AccessCtx) {
self.recording.push(Record::Access);
self.child.accessibility(ctx);
} }
fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> { fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> {

View File

@ -8,15 +8,17 @@
// size constraints to its child means that "aligning" a widget may actually change // size constraints to its child means that "aligning" a widget may actually change
// its computed size. See issue #3. // its computed size. See issue #3.
use accesskit::Role;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use tracing::{trace, trace_span, Span}; use tracing::{trace, trace_span, Span};
use vello::Scene; use vello::Scene;
use crate::contexts::AccessCtx;
use crate::paint_scene_helpers::UnitPoint; use crate::paint_scene_helpers::UnitPoint;
use crate::widget::{WidgetPod, WidgetRef}; use crate::widget::{WidgetPod, WidgetRef};
use crate::{ use crate::{
BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, PointerEvent, Rect, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx,
Size, StatusChange, TextEvent, Widget, PointerEvent, Rect, Size, StatusChange, TextEvent, Widget,
}; };
// TODO - Have child widget type as generic argument // TODO - Have child widget type as generic argument
@ -89,6 +91,10 @@ impl Widget for Align {
self.child.on_text_event(ctx, event); self.child.on_text_event(ctx, event);
} }
fn on_access_event(&mut self, ctx: &mut EventCtx, event: &AccessEvent) {
self.child.on_access_event(ctx, event);
}
fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle) { fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle) {
self.child.lifecycle(ctx, event); self.child.lifecycle(ctx, event);
} }
@ -146,6 +152,14 @@ impl Widget for Align {
self.child.paint(ctx, scene); self.child.paint(ctx, scene);
} }
fn accessibility_role(&self) -> Role {
Role::GenericContainer
}
fn accessibility(&mut self, ctx: &mut AccessCtx) {
self.child.accessibility(ctx);
}
fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> { fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> {
smallvec![self.child.as_dyn()] smallvec![self.child.as_dyn()]
} }

View File

@ -3,6 +3,7 @@
//! A button widget. //! A button widget.
use accesskit::{DefaultActionVerb, Role};
use smallvec::SmallVec; use smallvec::SmallVec;
use tracing::{trace, trace_span, Span}; use tracing::{trace, trace_span, Span};
use vello::Scene; use vello::Scene;
@ -12,8 +13,8 @@ use crate::paint_scene_helpers::{fill_lin_gradient, stroke, UnitPoint};
use crate::text2::TextStorage; use crate::text2::TextStorage;
use crate::widget::{Label, WidgetMut, WidgetPod, WidgetRef}; use crate::widget::{Label, WidgetMut, WidgetPod, WidgetRef};
use crate::{ use crate::{
theme, BoxConstraints, EventCtx, Insets, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, theme, AccessCtx, AccessEvent, BoxConstraints, EventCtx, Insets, LayoutCtx, LifeCycle,
PointerEvent, Size, StatusChange, TextEvent, Widget, LifeCycleCtx, PaintCtx, PointerEvent, Size, StatusChange, TextEvent, Widget,
}; };
// the minimum padding added to a button. // the minimum padding added to a button.
@ -103,6 +104,19 @@ impl<T: TextStorage> Widget for Button<T> {
self.label.on_text_event(ctx, event); self.label.on_text_event(ctx, event);
} }
fn on_access_event(&mut self, ctx: &mut EventCtx, event: &AccessEvent) {
if event.target == ctx.widget_id() {
match event.action {
accesskit::Action::Default => {
ctx.submit_action(Action::ButtonPressed);
ctx.request_paint();
}
_ => {}
}
}
ctx.skip_child(&mut self.label);
}
fn on_status_change(&mut self, ctx: &mut LifeCycleCtx, _event: &StatusChange) { fn on_status_change(&mut self, ctx: &mut LifeCycleCtx, _event: &StatusChange) {
ctx.request_paint(); ctx.request_paint();
} }
@ -173,6 +187,20 @@ impl<T: TextStorage> Widget for Button<T> {
self.label.paint(ctx, scene); self.label.paint(ctx, scene);
} }
fn accessibility_role(&self) -> Role {
Role::Button
}
fn accessibility(&mut self, ctx: &mut AccessCtx) {
let _name = self.label.widget().text().as_str().to_string();
// We may want to add a name if it doesn't interfere with the child label
// ctx.current_node().set_name(name);
ctx.current_node()
.set_default_action_verb(DefaultActionVerb::Click);
self.label.accessibility(ctx);
}
fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> { fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> {
SmallVec::new() SmallVec::new()
} }

View File

@ -3,6 +3,7 @@
//! A checkbox widget. //! A checkbox widget.
use accesskit::{DefaultActionVerb, Role, Toggled};
use kurbo::{Affine, Stroke}; use kurbo::{Affine, Stroke};
use smallvec::SmallVec; use smallvec::SmallVec;
use tracing::{trace, trace_span, Span}; use tracing::{trace, trace_span, Span};
@ -14,8 +15,8 @@ use crate::paint_scene_helpers::{fill_lin_gradient, stroke, UnitPoint};
use crate::text2::TextStorage; use crate::text2::TextStorage;
use crate::widget::{Label, WidgetMut, WidgetRef}; use crate::widget::{Label, WidgetMut, WidgetRef};
use crate::{ use crate::{
theme, ArcStr, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, theme, AccessCtx, AccessEvent, ArcStr, BoxConstraints, EventCtx, LayoutCtx, LifeCycle,
PointerEvent, StatusChange, TextEvent, Widget, WidgetPod, LifeCycleCtx, PaintCtx, PointerEvent, StatusChange, TextEvent, Widget, WidgetPod,
}; };
/// A checkbox that can be toggled. /// A checkbox that can be toggled.
@ -85,6 +86,19 @@ impl<T: TextStorage> Widget for Checkbox<T> {
fn on_text_event(&mut self, _ctx: &mut EventCtx, _event: &TextEvent) {} fn on_text_event(&mut self, _ctx: &mut EventCtx, _event: &TextEvent) {}
fn on_access_event(&mut self, ctx: &mut EventCtx, event: &AccessEvent) {
if event.target == ctx.widget_id() {
match event.action {
accesskit::Action::Default => {
self.checked = !self.checked;
ctx.submit_action(Action::CheckboxChecked(self.checked));
ctx.request_paint();
}
_ => {}
}
}
}
fn on_status_change(&mut self, ctx: &mut LifeCycleCtx, _event: &StatusChange) { fn on_status_change(&mut self, ctx: &mut LifeCycleCtx, _event: &StatusChange) {
ctx.request_paint(); ctx.request_paint();
} }
@ -166,6 +180,27 @@ impl<T: TextStorage> Widget for Checkbox<T> {
self.label.paint(ctx, scene); self.label.paint(ctx, scene);
} }
fn accessibility_role(&self) -> Role {
Role::CheckBox
}
fn accessibility(&mut self, ctx: &mut AccessCtx) {
let _name = self.label.widget().text().as_str().to_string();
// We may want to add a name if it doesn't interfere with the child label
// ctx.current_node().set_name(name);
if self.checked {
ctx.current_node().set_toggled(Toggled::True);
ctx.current_node()
.set_default_action_verb(DefaultActionVerb::Uncheck);
} else {
ctx.current_node().set_toggled(Toggled::False);
ctx.current_node()
.set_default_action_verb(DefaultActionVerb::Check);
}
self.label.accessibility(ctx);
}
fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> { fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> {
SmallVec::new() SmallVec::new()
} }

View File

@ -3,6 +3,7 @@
//! A widget that arranges its children in a one-dimensional array. //! A widget that arranges its children in a one-dimensional array.
use accesskit::Role;
use kurbo::{Affine, Stroke}; use kurbo::{Affine, Stroke};
use smallvec::SmallVec; use smallvec::SmallVec;
use tracing::{trace, trace_span, Span}; use tracing::{trace, trace_span, Span};
@ -13,8 +14,8 @@ use crate::kurbo::Vec2;
use crate::theme::get_debug_color; use crate::theme::get_debug_color;
use crate::widget::{WidgetMut, WidgetRef}; use crate::widget::{WidgetMut, WidgetRef};
use crate::{ use crate::{
BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Point, PointerEvent, AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx,
Rect, Size, StatusChange, TextEvent, Widget, WidgetId, WidgetPod, Point, PointerEvent, Rect, Size, StatusChange, TextEvent, Widget, WidgetId, WidgetPod,
}; };
/// A container with either horizontal or vertical layout. /// A container with either horizontal or vertical layout.
@ -498,6 +499,12 @@ impl Widget for Flex {
} }
} }
fn on_access_event(&mut self, ctx: &mut EventCtx, event: &AccessEvent) {
for child in self.children.iter_mut().filter_map(|x| x.widget_mut()) {
child.on_access_event(ctx, event);
}
}
fn on_status_change(&mut self, _ctx: &mut LifeCycleCtx, _event: &StatusChange) {} fn on_status_change(&mut self, _ctx: &mut LifeCycleCtx, _event: &StatusChange) {}
fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle) { fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle) {
@ -719,6 +726,16 @@ impl Widget for Flex {
} }
} }
fn accessibility_role(&self) -> Role {
Role::GenericContainer
}
fn accessibility(&mut self, ctx: &mut AccessCtx) {
for child in self.children.iter_mut().filter_map(|x| x.widget_mut()) {
child.accessibility(ctx);
}
}
fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> { fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> {
self.children self.children
.iter() .iter()

View File

@ -4,6 +4,7 @@
//! An Image widget. //! An Image widget.
//! Please consider using SVG and the SVG widget as it scales much better. //! Please consider using SVG and the SVG widget as it scales much better.
use accesskit::Role;
use kurbo::Affine; use kurbo::Affine;
use smallvec::SmallVec; use smallvec::SmallVec;
use tracing::{trace, trace_span, Span}; use tracing::{trace, trace_span, Span};
@ -12,8 +13,8 @@ use vello::Scene;
use crate::widget::{FillStrat, WidgetMut, WidgetRef}; use crate::widget::{FillStrat, WidgetMut, WidgetRef};
use crate::{ use crate::{
BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, PointerEvent, Size, AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx,
StatusChange, TextEvent, Widget, PointerEvent, Size, StatusChange, TextEvent, Widget,
}; };
// TODO - Resolve name collision between masonry::Image and peniko::Image // TODO - Resolve name collision between masonry::Image and peniko::Image
@ -68,6 +69,8 @@ impl Widget for Image {
fn on_text_event(&mut self, _ctx: &mut EventCtx, _event: &TextEvent) {} fn on_text_event(&mut self, _ctx: &mut EventCtx, _event: &TextEvent) {}
fn on_access_event(&mut self, _ctx: &mut EventCtx, _event: &AccessEvent) {}
fn on_status_change(&mut self, _ctx: &mut LifeCycleCtx, _event: &StatusChange) {} fn on_status_change(&mut self, _ctx: &mut LifeCycleCtx, _event: &StatusChange) {}
fn lifecycle(&mut self, _ctx: &mut LifeCycleCtx, _event: &LifeCycle) {} fn lifecycle(&mut self, _ctx: &mut LifeCycleCtx, _event: &LifeCycle) {}
@ -101,6 +104,14 @@ impl Widget for Image {
scene.pop_layer(); scene.pop_layer();
} }
fn accessibility_role(&self) -> Role {
Role::Image
}
fn accessibility(&mut self, _ctx: &mut AccessCtx) {
// TODO - Handle alt text and such.
}
fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> { fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> {
SmallVec::new() SmallVec::new()
} }

View File

@ -1,27 +1,24 @@
// Copyright 2019 the Xilem Authors and the Druid Authors // Copyright 2019 the Xilem Authors and the Druid Authors
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
//! A label widget.
use accesskit::Role;
use kurbo::{Affine, Point, Size}; use kurbo::{Affine, Point, Size};
use parley::{ use parley::layout::Alignment;
layout::Alignment, use parley::style::{FontFamily, FontStack};
style::{FontFamily, FontStack},
};
use smallvec::SmallVec; use smallvec::SmallVec;
use tracing::trace; use tracing::trace;
use vello::{ use vello::peniko::BlendMode;
peniko::{BlendMode, Color}, use vello::Scene;
Scene,
};
use crate::text2::{TextBrush, TextLayout, TextStorage};
use crate::widget::{WidgetMut, WidgetRef};
use crate::{ use crate::{
text2::{TextBrush, TextLayout, TextStorage}, AccessCtx, AccessEvent, ArcStr, BoxConstraints, Color, EventCtx, LayoutCtx, LifeCycle,
widget::WidgetRef, LifeCycleCtx, PaintCtx, PointerEvent, StatusChange, TextEvent, Widget,
ArcStr, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, PointerEvent,
StatusChange, TextEvent, Widget,
}; };
use super::WidgetMut;
// added padding between the edges of the widget and the text. // added padding between the edges of the widget and the text.
pub(super) const LABEL_X_PADDING: f64 = 2.0; pub(super) const LABEL_X_PADDING: f64 = 2.0;
@ -164,6 +161,8 @@ impl<T: TextStorage> Widget for Label<T> {
// that the bounding boxes can go e.g. across line boundaries? // that the bounding boxes can go e.g. across line boundaries?
} }
fn on_access_event(&mut self, _ctx: &mut EventCtx, _event: &AccessEvent) {}
#[allow(missing_docs)] #[allow(missing_docs)]
fn on_status_change(&mut self, _ctx: &mut LifeCycleCtx, event: &StatusChange) { fn on_status_change(&mut self, _ctx: &mut LifeCycleCtx, event: &StatusChange) {
match event { match event {
@ -244,6 +243,15 @@ impl<T: TextStorage> Widget for Label<T> {
} }
} }
fn accessibility_role(&self) -> Role {
Role::StaticText
}
fn accessibility(&mut self, ctx: &mut AccessCtx) {
ctx.current_node()
.set_name(self.text().as_str().to_string());
}
fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> { fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> {
SmallVec::new() SmallVec::new()
} }

View File

@ -5,6 +5,7 @@
use std::ops::Range; use std::ops::Range;
use accesskit::Role;
use kurbo::Affine; use kurbo::Affine;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use tracing::{trace_span, Span}; use tracing::{trace_span, Span};
@ -14,8 +15,8 @@ use vello::Scene;
use crate::kurbo::{Point, Rect, Size, Vec2}; use crate::kurbo::{Point, Rect, Size, Vec2};
use crate::widget::{Axis, ScrollBar, WidgetMut, WidgetRef}; use crate::widget::{Axis, ScrollBar, WidgetMut, WidgetRef};
use crate::{ use crate::{
BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, PointerEvent, AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx,
StatusChange, TextEvent, Widget, WidgetPod, PointerEvent, StatusChange, TextEvent, Widget, WidgetPod,
}; };
// TODO - refactor - see issue #15 // TODO - refactor - see issue #15
@ -289,6 +290,14 @@ impl<W: Widget> Widget for Portal<W> {
self.scrollbar_vertical.on_text_event(ctx, event); self.scrollbar_vertical.on_text_event(ctx, event);
} }
fn on_access_event(&mut self, ctx: &mut EventCtx, event: &AccessEvent) {
// TODO - Handle scroll-related events?
self.child.on_access_event(ctx, event);
self.scrollbar_horizontal.on_access_event(ctx, event);
self.scrollbar_vertical.on_access_event(ctx, event);
}
fn on_status_change(&mut self, _ctx: &mut LifeCycleCtx, _event: &StatusChange) {} fn on_status_change(&mut self, _ctx: &mut LifeCycleCtx, _event: &StatusChange) {}
fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle) { fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle) {
@ -379,6 +388,31 @@ impl<W: Widget> Widget for Portal<W> {
} }
} }
fn accessibility_role(&self) -> Role {
Role::GenericContainer
}
fn accessibility(&mut self, ctx: &mut AccessCtx) {
// TODO - Double check this code
// Not sure about these values
if false {
ctx.current_node().set_scroll_x(self.viewport_pos.x);
ctx.current_node().set_scroll_y(self.viewport_pos.y);
ctx.current_node().set_scroll_x_min(0.0);
ctx.current_node()
.set_scroll_x_max(self.scrollbar_horizontal.widget().portal_size);
ctx.current_node().set_scroll_y_min(0.0);
ctx.current_node()
.set_scroll_y_max(self.scrollbar_vertical.widget().portal_size);
}
ctx.current_node().set_clips_children();
self.child.accessibility(ctx);
self.scrollbar_horizontal.accessibility(ctx);
self.scrollbar_vertical.accessibility(ctx);
}
fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> { fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> {
smallvec![self.child.as_dyn()] smallvec![self.child.as_dyn()]
} }

View File

@ -1,6 +1,7 @@
// Copyright 2018 the Xilem Authors and the Druid Authors // Copyright 2018 the Xilem Authors and the Druid Authors
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
use accesskit::Role;
use kurbo::{Affine, Point, Size}; use kurbo::{Affine, Point, Size};
use parley::{ use parley::{
layout::Alignment, layout::Alignment,
@ -13,8 +14,8 @@ use vello::{peniko::BlendMode, Scene};
use crate::{ use crate::{
text2::{Selectable, TextBrush, TextWithSelection}, text2::{Selectable, TextBrush, TextWithSelection},
widget::label::LABEL_X_PADDING, widget::label::LABEL_X_PADDING,
BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, PointerEvent, AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx,
StatusChange, TextEvent, Widget, PointerEvent, StatusChange, TextEvent, Widget,
}; };
use super::{LineBreaking, WidgetMut, WidgetRef}; use super::{LineBreaking, WidgetMut, WidgetRef};
@ -175,6 +176,10 @@ impl<T: Selectable> Widget for Prose<T> {
} }
} }
fn on_access_event(&mut self, _ctx: &mut EventCtx, _event: &AccessEvent) {
// TODO - Handle accesskit::Action::SetTextSelection
}
#[allow(missing_docs)] #[allow(missing_docs)]
fn on_status_change(&mut self, ctx: &mut LifeCycleCtx, event: &StatusChange) { fn on_status_change(&mut self, ctx: &mut LifeCycleCtx, event: &StatusChange) {
match event { match event {
@ -261,6 +266,15 @@ impl<T: Selectable> Widget for Prose<T> {
} }
} }
fn accessibility_role(&self) -> Role {
Role::StaticText
}
fn accessibility(&mut self, ctx: &mut AccessCtx) {
ctx.current_node()
.set_name(self.text().as_str().to_string());
}
fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> { fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> {
SmallVec::new() SmallVec::new()
} }

View File

@ -3,6 +3,7 @@
#![allow(missing_docs)] #![allow(missing_docs)]
use accesskit::Role;
use smallvec::SmallVec; use smallvec::SmallVec;
use tracing::{trace_span, Span}; use tracing::{trace_span, Span};
use vello::Scene; use vello::Scene;
@ -12,8 +13,8 @@ use crate::kurbo::Rect;
use crate::paint_scene_helpers::{fill_color, stroke}; use crate::paint_scene_helpers::{fill_color, stroke};
use crate::widget::{WidgetMut, WidgetRef}; use crate::widget::{WidgetMut, WidgetRef};
use crate::{ use crate::{
theme, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Point, theme, AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx,
PointerEvent, Size, StatusChange, TextEvent, Widget, PaintCtx, Point, PointerEvent, Size, StatusChange, TextEvent, Widget,
}; };
// RULES // RULES
@ -172,6 +173,10 @@ impl Widget for ScrollBar {
fn on_text_event(&mut self, _ctx: &mut EventCtx, _event: &TextEvent) {} fn on_text_event(&mut self, _ctx: &mut EventCtx, _event: &TextEvent) {}
fn on_access_event(&mut self, _ctx: &mut EventCtx, _event: &AccessEvent) {
// TODO - Handle scroll-related events?
}
fn on_status_change(&mut self, _ctx: &mut LifeCycleCtx, _event: &StatusChange) {} fn on_status_change(&mut self, _ctx: &mut LifeCycleCtx, _event: &StatusChange) {}
fn lifecycle(&mut self, _ctx: &mut LifeCycleCtx, _event: &LifeCycle) {} fn lifecycle(&mut self, _ctx: &mut LifeCycleCtx, _event: &LifeCycle) {}
@ -210,6 +215,15 @@ impl Widget for ScrollBar {
); );
} }
fn accessibility_role(&self) -> Role {
Role::ScrollBar
}
fn accessibility(&mut self, _ctx: &mut AccessCtx) {
// TODO
// Use set_scroll_x/y_min/max?
}
fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> { fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> {
SmallVec::new() SmallVec::new()
} }

View File

@ -3,6 +3,7 @@
//! A widget with predefined size. //! A widget with predefined size.
use accesskit::Role;
use kurbo::Affine; use kurbo::Affine;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use tracing::{trace, trace_span, warn, Span}; use tracing::{trace, trace_span, warn, Span};
@ -13,8 +14,8 @@ use crate::kurbo::RoundedRectRadii;
use crate::paint_scene_helpers::{fill_color, stroke}; use crate::paint_scene_helpers::{fill_color, stroke};
use crate::widget::{WidgetId, WidgetMut, WidgetPod, WidgetRef}; use crate::widget::{WidgetId, WidgetMut, WidgetPod, WidgetRef};
use crate::{ use crate::{
BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Point, PointerEvent, AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx,
Size, StatusChange, TextEvent, Widget, Point, PointerEvent, Size, StatusChange, TextEvent, Widget,
}; };
// FIXME - Improve all doc in this module ASAP. // FIXME - Improve all doc in this module ASAP.
@ -291,6 +292,8 @@ impl Widget for SizedBox {
} }
} }
fn on_access_event(&mut self, _ctx: &mut EventCtx, _event: &AccessEvent) {}
fn on_status_change(&mut self, _ctx: &mut LifeCycleCtx, _event: &StatusChange) {} fn on_status_change(&mut self, _ctx: &mut LifeCycleCtx, _event: &StatusChange) {}
fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle) { fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle) {
@ -366,6 +369,16 @@ impl Widget for SizedBox {
} }
} }
fn accessibility_role(&self) -> Role {
Role::GenericContainer
}
fn accessibility(&mut self, ctx: &mut AccessCtx) {
if let Some(child) = self.child.as_mut() {
child.accessibility(ctx);
}
}
fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> { fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> {
if let Some(child) = &self.child { if let Some(child) = &self.child {
smallvec![child.as_dyn()] smallvec![child.as_dyn()]

View File

@ -5,6 +5,7 @@
use std::f64::consts::PI; use std::f64::consts::PI;
use accesskit::Role;
use kurbo::{Affine, Cap, Stroke}; use kurbo::{Affine, Cap, Stroke};
use smallvec::SmallVec; use smallvec::SmallVec;
use tracing::trace; use tracing::trace;
@ -13,8 +14,8 @@ use vello::Scene;
use crate::kurbo::Line; use crate::kurbo::Line;
use crate::widget::{WidgetMut, WidgetRef}; use crate::widget::{WidgetMut, WidgetRef};
use crate::{ use crate::{
theme, BoxConstraints, Color, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Point, theme, AccessCtx, AccessEvent, BoxConstraints, Color, EventCtx, LayoutCtx, LifeCycle,
PointerEvent, Size, StatusChange, TextEvent, Vec2, Widget, LifeCycleCtx, PaintCtx, Point, PointerEvent, Size, StatusChange, TextEvent, Vec2, Widget,
}; };
// TODO - Set color // TODO - Set color
@ -72,6 +73,8 @@ impl Widget for Spinner {
fn on_text_event(&mut self, _ctx: &mut EventCtx, _event: &TextEvent) {} fn on_text_event(&mut self, _ctx: &mut EventCtx, _event: &TextEvent) {}
fn on_access_event(&mut self, _ctx: &mut EventCtx, _event: &AccessEvent) {}
fn on_status_change(&mut self, _ctx: &mut LifeCycleCtx, _event: &StatusChange) {} fn on_status_change(&mut self, _ctx: &mut LifeCycleCtx, _event: &StatusChange) {}
fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle) { fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle) {
@ -136,6 +139,14 @@ impl Widget for Spinner {
} }
} }
fn accessibility_role(&self) -> Role {
// Don't like to use that role, but I'm not seing
// anything that matches in accesskit::Role
Role::Unknown
}
fn accessibility(&mut self, _ctx: &mut AccessCtx) {}
fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> { fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> {
SmallVec::new() SmallVec::new()
} }

View File

@ -3,6 +3,7 @@
//! A widget which splits an area in two, with a settable ratio, and optional draggable resizing. //! A widget which splits an area in two, with a settable ratio, and optional draggable resizing.
use accesskit::Role;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use tracing::{trace, trace_span, warn, Span}; use tracing::{trace, trace_span, warn, Span};
use vello::Scene; use vello::Scene;
@ -15,8 +16,8 @@ use crate::paint_scene_helpers::{fill_color, stroke};
use crate::widget::flex::Axis; use crate::widget::flex::Axis;
use crate::widget::{WidgetMut, WidgetPod, WidgetRef}; use crate::widget::{WidgetMut, WidgetPod, WidgetRef};
use crate::{ use crate::{
theme, BoxConstraints, Color, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Point, theme, AccessCtx, AccessEvent, BoxConstraints, Color, EventCtx, LayoutCtx, LifeCycle,
PointerEvent, Rect, Size, StatusChange, TextEvent, Widget, LifeCycleCtx, PaintCtx, Point, PointerEvent, Rect, Size, StatusChange, TextEvent, Widget,
}; };
// TODO - Have child widget type as generic argument // TODO - Have child widget type as generic argument
@ -443,6 +444,11 @@ impl Widget for Split {
self.child2.on_text_event(ctx, event); self.child2.on_text_event(ctx, event);
} }
fn on_access_event(&mut self, ctx: &mut EventCtx, event: &AccessEvent) {
self.child1.on_access_event(ctx, event);
self.child2.on_access_event(ctx, event);
}
fn on_status_change(&mut self, _ctx: &mut LifeCycleCtx, _event: &StatusChange) {} fn on_status_change(&mut self, _ctx: &mut LifeCycleCtx, _event: &StatusChange) {}
fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle) { fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle) {
@ -558,6 +564,15 @@ impl Widget for Split {
self.child2.paint(ctx, scene); self.child2.paint(ctx, scene);
} }
fn accessibility_role(&self) -> Role {
Role::Splitter
}
fn accessibility(&mut self, ctx: &mut AccessCtx) {
self.child1.accessibility(ctx);
self.child2.accessibility(ctx);
}
fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> { fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> {
smallvec![self.child1.as_dyn(), self.child2.as_dyn()] smallvec![self.child1.as_dyn(), self.child2.as_dyn()]
} }

View File

@ -1,6 +1,7 @@
// Copyright 2018 the Xilem Authors and the Druid Authors // Copyright 2018 the Xilem Authors and the Druid Authors
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
use accesskit::Role;
use kurbo::{Affine, Point, Size, Stroke}; use kurbo::{Affine, Point, Size, Stroke};
use parley::{ use parley::{
layout::Alignment, layout::Alignment,
@ -15,8 +16,8 @@ use vello::{
use crate::{ use crate::{
text2::{EditableText, TextBrush, TextEditor, TextWithSelection}, text2::{EditableText, TextBrush, TextEditor, TextWithSelection},
BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, PointerEvent, AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx,
StatusChange, TextEvent, Widget, PointerEvent, StatusChange, TextEvent, Widget,
}; };
use super::{LineBreaking, WidgetMut, WidgetRef}; use super::{LineBreaking, WidgetMut, WidgetRef};
@ -179,6 +180,12 @@ impl<T: EditableText> Widget for Textbox<T> {
} }
} }
fn on_access_event(&mut self, _ctx: &mut EventCtx, _event: &AccessEvent) {
// TODO - Handle accesskit::Action::SetTextSelection
// TODO - Handle accesskit::Action::ReplaceSelectedText
// TODO - Handle accesskit::Action::SetValue
}
#[allow(missing_docs)] #[allow(missing_docs)]
fn on_status_change(&mut self, ctx: &mut LifeCycleCtx, event: &StatusChange) { fn on_status_change(&mut self, ctx: &mut LifeCycleCtx, event: &StatusChange) {
match event { match event {
@ -274,6 +281,14 @@ impl<T: EditableText> Widget for Textbox<T> {
} }
} }
fn accessibility_role(&self) -> Role {
Role::TextInput
}
fn accessibility(&mut self, _ctx: &mut AccessCtx) {
// TODO
}
fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> { fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> {
SmallVec::new() SmallVec::new()
} }

View File

@ -6,15 +6,16 @@ use std::num::NonZeroU64;
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::atomic::{AtomicU64, Ordering};
use accesskit::Role;
use smallvec::SmallVec; use smallvec::SmallVec;
use tracing::{trace_span, Span}; use tracing::{trace_span, Span};
use vello::Scene; use vello::Scene;
use crate::event::StatusChange; use crate::event::{AccessEvent, PointerEvent, StatusChange, TextEvent};
use crate::event::{PointerEvent, TextEvent};
use crate::widget::WidgetRef; use crate::widget::WidgetRef;
use crate::{ use crate::{
AsAny, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Point, Size, AccessCtx, AsAny, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx,
Point, Size,
}; };
/// A unique identifier for a single [`Widget`]. /// A unique identifier for a single [`Widget`].
@ -39,7 +40,7 @@ use crate::{
/// If you set a `WidgetId` directly, you are responsible for ensuring that it /// If you set a `WidgetId` directly, you are responsible for ensuring that it
/// is unique. Two widgets must not be created with the same id. /// is unique. Two widgets must not be created with the same id.
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub struct WidgetId(NonZeroU64); pub struct WidgetId(pub(crate) NonZeroU64);
// TODO - Add tutorial: implementing a widget - See issue #5 // TODO - Add tutorial: implementing a widget - See issue #5
/// The trait implemented by all widgets. /// The trait implemented by all widgets.
@ -73,6 +74,9 @@ pub trait Widget: AsAny {
fn on_pointer_event(&mut self, ctx: &mut EventCtx, event: &PointerEvent); fn on_pointer_event(&mut self, ctx: &mut EventCtx, event: &PointerEvent);
fn on_text_event(&mut self, ctx: &mut EventCtx, event: &TextEvent); fn on_text_event(&mut self, ctx: &mut EventCtx, event: &TextEvent);
/// Handle an event from the platform's accessibility API.
fn on_access_event(&mut self, ctx: &mut EventCtx, event: &AccessEvent);
#[allow(missing_docs)] #[allow(missing_docs)]
fn on_status_change(&mut self, ctx: &mut LifeCycleCtx, event: &StatusChange); fn on_status_change(&mut self, ctx: &mut LifeCycleCtx, event: &StatusChange);
@ -110,6 +114,10 @@ pub trait Widget: AsAny {
/// the render context, which is especially useful for scrolling. /// the render context, which is especially useful for scrolling.
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut Scene); fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut Scene);
fn accessibility_role(&self) -> Role;
fn accessibility(&mut self, ctx: &mut AccessCtx);
/// Return references to this widget's children. /// Return references to this widget's children.
/// ///
/// Leaf widgets return an empty array. Container widgets return references to /// Leaf widgets return an empty array. Container widgets return references to
@ -228,11 +236,17 @@ impl WidgetId {
WidgetId(unsafe { std::num::NonZeroU64::new_unchecked(id) }) WidgetId(unsafe { std::num::NonZeroU64::new_unchecked(id) })
} }
pub(crate) fn to_raw(self) -> u64 { pub fn to_raw(self) -> u64 {
self.0.into() self.0.into()
} }
} }
impl From<WidgetId> for accesskit::NodeId {
fn from(id: WidgetId) -> accesskit::NodeId {
accesskit::NodeId(id.0.into())
}
}
// TODO - remove // TODO - remove
impl Widget for Box<dyn Widget> { impl Widget for Box<dyn Widget> {
fn on_pointer_event(&mut self, ctx: &mut EventCtx, event: &PointerEvent) { fn on_pointer_event(&mut self, ctx: &mut EventCtx, event: &PointerEvent) {
@ -243,6 +257,10 @@ impl Widget for Box<dyn Widget> {
self.deref_mut().on_text_event(ctx, event); self.deref_mut().on_text_event(ctx, event);
} }
fn on_access_event(&mut self, ctx: &mut EventCtx, event: &AccessEvent) {
self.deref_mut().on_access_event(ctx, event);
}
fn on_status_change(&mut self, ctx: &mut LifeCycleCtx, event: &StatusChange) { fn on_status_change(&mut self, ctx: &mut LifeCycleCtx, event: &StatusChange) {
self.deref_mut().on_status_change(ctx, event); self.deref_mut().on_status_change(ctx, event);
} }
@ -259,6 +277,14 @@ impl Widget for Box<dyn Widget> {
self.deref_mut().paint(ctx, scene); self.deref_mut().paint(ctx, scene);
} }
fn accessibility_role(&self) -> Role {
self.deref().accessibility_role()
}
fn accessibility(&mut self, ctx: &mut AccessCtx) {
self.deref_mut().accessibility(ctx);
}
fn type_name(&self) -> &'static str { fn type_name(&self) -> &'static str {
self.deref().type_name() self.deref().type_name()
} }

View File

@ -1,19 +1,20 @@
// Copyright 2018 the Xilem Authors and the Druid Authors // Copyright 2018 the Xilem Authors and the Druid Authors
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
use accesskit::{NodeBuilder, NodeId};
use tracing::{info_span, trace, warn}; use tracing::{info_span, trace, warn};
use vello::Scene; use vello::Scene;
use winit::dpi::LogicalPosition; use winit::dpi::LogicalPosition;
use crate::event::{PointerEvent, TextEvent}; use crate::event::{AccessEvent, PointerEvent, TextEvent};
use crate::kurbo::{Affine, Insets, Point, Rect, Shape, Size}; use crate::kurbo::{Affine, Insets, Point, Rect, Shape, Size};
use crate::paint_scene_helpers::stroke; use crate::paint_scene_helpers::stroke;
use crate::render_root::RenderRootState; use crate::render_root::RenderRootState;
use crate::theme::get_debug_color; use crate::theme::get_debug_color;
use crate::widget::{WidgetRef, WidgetState}; use crate::widget::{WidgetRef, WidgetState};
use crate::{ use crate::{
BoxConstraints, EventCtx, InternalLifeCycle, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, AccessCtx, BoxConstraints, EventCtx, InternalLifeCycle, LayoutCtx, LifeCycle, LifeCycleCtx,
StatusChange, Widget, WidgetId, PaintCtx, StatusChange, Widget, WidgetId,
}; };
// TODO - rewrite links in doc // TODO - rewrite links in doc
@ -497,6 +498,59 @@ impl<W: Widget> WidgetPod<W> {
self.inner.lifecycle(&mut inner_ctx, &event); self.inner.lifecycle(&mut inner_ctx, &event);
} }
pub fn on_access_event(&mut self, parent_ctx: &mut EventCtx, event: &AccessEvent) {
let _span = self.inner.make_trace_span().entered();
// TODO #11
parent_ctx
.global_state
.debug_logger
.push_span(self.inner.short_type_name());
// TODO - explain this
self.mark_as_visited();
self.check_initialized("on_text_event");
if parent_ctx.is_handled {
parent_ctx.global_state.debug_logger.pop_span();
// If the event was already handled, we quit early.
return;
}
if self.state.children.may_contain(&event.target) {
self.call_widget_method_with_checks("on_access_event", |widget_pod| {
// widget_pod is a reborrow of `self`
let mut inner_ctx = EventCtx {
global_state: parent_ctx.global_state,
widget_state: &mut widget_pod.state,
is_handled: false,
request_pan_to_child: None,
};
widget_pod.inner.on_access_event(&mut inner_ctx, event);
inner_ctx.widget_state.has_active |= inner_ctx.widget_state.is_active;
parent_ctx.is_handled |= inner_ctx.is_handled;
// TODO - request_pan_to_child
});
}
// Always merge even if not needed, because merging is idempotent and gives us simpler code.
// Doing this conditionally only makes sense when there's a measurable performance boost.
parent_ctx.widget_state.merge_up(&mut self.state);
parent_ctx
.global_state
.debug_logger
.update_widget_state(self.as_dyn());
parent_ctx
.global_state
.debug_logger
.push_log(false, "updated state");
parent_ctx.global_state.debug_logger.pop_span();
}
// --- LIFECYCLE --- // --- LIFECYCLE ---
// TODO #5 - Some implicit invariants: // TODO #5 - Some implicit invariants:
@ -629,6 +683,8 @@ impl<W: Widget> WidgetPod<W> {
self.state.needs_layout = true; self.state.needs_layout = true;
self.state.needs_paint = true; self.state.needs_paint = true;
self.state.needs_window_origin = true; self.state.needs_window_origin = true;
self.state.needs_accessibility_update = true;
self.state.request_accessibility_update = true;
true true
} }
@ -791,6 +847,8 @@ impl<W: Widget> WidgetPod<W> {
self.state.is_expecting_place_child_call = true; self.state.is_expecting_place_child_call = true;
// TODO - Not everything that has been re-laid out needs to be repainted. // TODO - Not everything that has been re-laid out needs to be repainted.
self.state.needs_paint = true; self.state.needs_paint = true;
self.state.request_accessibility_update = false;
self.state.needs_accessibility_update = false;
bc.debug_check(self.inner.short_type_name()); bc.debug_check(self.inner.short_type_name());
@ -945,6 +1003,78 @@ impl<W: Widget> WidgetPod<W> {
let scene = &mut self.fragment; let scene = &mut self.fragment;
stroke(scene, &rect, color, BORDER_WIDTH); stroke(scene, &rect, color, BORDER_WIDTH);
} }
pub fn accessibility(&mut self, parent_ctx: &mut AccessCtx) {
let _span = self.inner.make_trace_span().entered();
// TODO
// if self.state.is_stashed {}
// TODO - explain this
self.mark_as_visited();
self.check_initialized("accessibility");
// If this widget or a child has requested an accessibility update,
// or if AccessKit has requested a full rebuild,
// we call the accessibility method on this widget.
if parent_ctx.rebuild_all || self.state.request_accessibility_update {
trace!(
"Building accessibility node for widget '{}' #{}",
self.inner.short_type_name(),
self.state.id.to_raw()
);
self.call_widget_method_with_checks("accessibility", |widget_pod| {
let current_node = widget_pod.build_access_node();
let mut inner_ctx = AccessCtx {
global_state: parent_ctx.global_state,
widget_state: &mut widget_pod.state,
tree_update: parent_ctx.tree_update,
current_node,
rebuild_all: parent_ctx.rebuild_all,
};
widget_pod.inner.accessibility(&mut inner_ctx);
let id = inner_ctx.widget_state.id.into();
inner_ctx
.tree_update
.nodes
.push((id, inner_ctx.current_node.build()));
});
}
self.state.request_accessibility_update = false;
self.state.needs_accessibility_update = false;
}
fn build_access_node(&mut self) -> NodeBuilder {
let mut node = NodeBuilder::new(self.inner.accessibility_role());
node.set_bounds(to_accesskit_rect(self.state.window_layout_rect()));
node.set_children(
self.inner
.children()
.iter()
.map(|pod| pod.id().into())
.collect::<Vec<NodeId>>(),
);
if self.state.is_hot {
node.set_hovered();
}
if self.state.is_disabled() {
node.set_disabled();
}
if self.state.is_stashed {
node.set_hidden();
}
node
}
}
fn to_accesskit_rect(r: Rect) -> accesskit::Rect {
accesskit::Rect::new(r.x0, r.y0, r.x1, r.y1)
} }
// TODO - negative rects? // TODO - negative rects?

View File

@ -83,6 +83,7 @@ pub struct WidgetState {
pub(crate) needs_layout: bool, pub(crate) needs_layout: bool,
pub(crate) needs_paint: bool, pub(crate) needs_paint: bool,
pub(crate) needs_accessibility_update: bool,
/// Because of some scrolling or something, `parent_window_origin` needs to be updated. /// Because of some scrolling or something, `parent_window_origin` needs to be updated.
pub(crate) needs_window_origin: bool, pub(crate) needs_window_origin: bool,
@ -90,6 +91,9 @@ pub struct WidgetState {
/// Any descendant has requested an animation frame. /// Any descendant has requested an animation frame.
pub(crate) request_anim: bool, pub(crate) request_anim: bool,
/// Any descendant has requested an accessibility update.
pub(crate) request_accessibility_update: bool,
pub(crate) update_focus_chain: bool, pub(crate) update_focus_chain: bool,
pub(crate) focus_chain: Vec<WidgetId>, pub(crate) focus_chain: Vec<WidgetId>,
@ -161,11 +165,13 @@ impl WidgetState {
is_hot: false, is_hot: false,
needs_layout: false, needs_layout: false,
needs_paint: false, needs_paint: false,
needs_accessibility_update: false,
needs_window_origin: false, needs_window_origin: false,
is_active: false, is_active: false,
has_active: false, has_active: false,
has_focus: false, has_focus: false,
request_anim: false, request_anim: false,
request_accessibility_update: false,
focus_chain: Vec::new(), focus_chain: Vec::new(),
children: Bloom::new(), children: Bloom::new(),
children_changed: false, children_changed: false,
@ -214,6 +220,7 @@ impl WidgetState {
self.needs_paint |= child_state.needs_paint; self.needs_paint |= child_state.needs_paint;
self.needs_window_origin |= child_state.needs_window_origin; self.needs_window_origin |= child_state.needs_window_origin;
self.request_anim |= child_state.request_anim; self.request_anim |= child_state.request_anim;
self.request_accessibility_update |= child_state.request_accessibility_update;
self.children_disabled_changed |= child_state.children_disabled_changed; self.children_disabled_changed |= child_state.children_disabled_changed;
self.children_disabled_changed |= self.children_disabled_changed |=
child_state.is_explicitly_disabled_new != child_state.is_explicitly_disabled; child_state.is_explicitly_disabled_new != child_state.is_explicitly_disabled;

View File

@ -23,3 +23,5 @@ winit.workspace = true
tracing.workspace = true tracing.workspace = true
vello.workspace = true vello.workspace = true
smallvec.workspace = true smallvec.workspace = true
accesskit.workspace = true
accesskit_winit.workspace = true

View File

@ -3,10 +3,11 @@
use std::{any::Any, ops::Deref}; use std::{any::Any, ops::Deref};
use accesskit::Role;
use masonry::widget::{WidgetMut, WidgetRef}; use masonry::widget::{WidgetMut, WidgetRef};
use masonry::{ use masonry::{
BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Point, PointerEvent, AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx,
Size, StatusChange, TextEvent, Widget, WidgetPod, Point, PointerEvent, Size, StatusChange, TextEvent, Widget, WidgetPod,
}; };
use smallvec::SmallVec; use smallvec::SmallVec;
use vello::Scene; use vello::Scene;
@ -201,6 +202,9 @@ impl Widget for DynWidget {
fn on_text_event(&mut self, ctx: &mut EventCtx, event: &TextEvent) { fn on_text_event(&mut self, ctx: &mut EventCtx, event: &TextEvent) {
self.inner.on_text_event(ctx, event); self.inner.on_text_event(ctx, event);
} }
fn on_access_event(&mut self, ctx: &mut EventCtx, event: &AccessEvent) {
self.inner.on_access_event(ctx, event);
}
fn on_status_change(&mut self, _: &mut LifeCycleCtx, _: &StatusChange) { fn on_status_change(&mut self, _: &mut LifeCycleCtx, _: &StatusChange) {
// Intentionally do nothing // Intentionally do nothing
@ -220,6 +224,14 @@ impl Widget for DynWidget {
self.inner.paint(ctx, scene); self.inner.paint(ctx, scene);
} }
fn accessibility_role(&self) -> Role {
self.inner.widget().accessibility_role()
}
fn accessibility(&mut self, ctx: &mut AccessCtx) {
self.inner.accessibility(ctx);
}
fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> { fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> {
let mut vec = SmallVec::new(); let mut vec = SmallVec::new();
vec.push(self.inner.as_dyn()); vec.push(self.inner.as_dyn());

View File

@ -4,17 +4,22 @@
#![allow(clippy::comparison_chain)] #![allow(clippy::comparison_chain)]
use std::{any::Any, collections::HashMap}; use std::{any::Any, collections::HashMap};
use accesskit::Role;
use masonry::{ use masonry::{
app_driver::AppDriver, app_driver::AppDriver,
event_loop_runner, event_loop_runner,
widget::{WidgetMut, WidgetRef}, widget::{WidgetMut, WidgetRef},
BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Point, PointerEvent, AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx,
Size, StatusChange, TextEvent, Widget, WidgetId, WidgetPod, Point, PointerEvent, Size, StatusChange, TextEvent, Widget, WidgetId, WidgetPod,
}; };
pub use masonry::{widget::Axis, Color, TextAlignment}; pub use masonry::{widget::Axis, Color, TextAlignment};
use smallvec::SmallVec; use smallvec::SmallVec;
use vello::Scene; use vello::Scene;
use winit::{dpi::LogicalSize, error::EventLoopError, event_loop::EventLoop, window::Window}; use winit::{
dpi::LogicalSize,
error::EventLoopError,
window::{Window, WindowAttributes},
};
mod any_view; mod any_view;
mod id; mod id;
@ -54,6 +59,9 @@ impl<E: 'static + Widget> Widget for RootWidget<E> {
fn on_text_event(&mut self, ctx: &mut EventCtx, event: &TextEvent) { fn on_text_event(&mut self, ctx: &mut EventCtx, event: &TextEvent) {
self.pod.on_text_event(ctx, event); self.pod.on_text_event(ctx, event);
} }
fn on_access_event(&mut self, ctx: &mut EventCtx, event: &AccessEvent) {
self.pod.on_access_event(ctx, event);
}
fn on_status_change(&mut self, _: &mut LifeCycleCtx, _: &StatusChange) { fn on_status_change(&mut self, _: &mut LifeCycleCtx, _: &StatusChange) {
// Intentionally do nothing? // Intentionally do nothing?
@ -73,6 +81,14 @@ impl<E: 'static + Widget> Widget for RootWidget<E> {
self.pod.paint(ctx, scene); self.pod.paint(ctx, scene);
} }
fn accessibility_role(&self) -> Role {
Role::Window
}
fn accessibility(&mut self, ctx: &mut AccessCtx) {
self.pod.accessibility(ctx);
}
fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> { fn children(&self) -> SmallVec<[WidgetRef<'_, dyn Widget>; 16]> {
let mut vec = SmallVec::new(); let mut vec = SmallVec::new();
vec.push(self.pod.as_dyn()); vec.push(self.pod.as_dyn());
@ -171,32 +187,22 @@ where
Logic: 'static, Logic: 'static,
View: 'static, View: 'static,
{ {
let event_loop = EventLoop::new().unwrap();
let window_size = LogicalSize::new(600., 800.); let window_size = LogicalSize::new(600., 800.);
#[allow(deprecated)] let window_attributes = Window::default_attributes()
let window = event_loop .with_title(window_title)
.create_window( .with_resizable(true)
Window::default_attributes() .with_min_inner_size(window_size);
.with_title(window_title) self.run_windowed_in(window_attributes)
.with_resizable(true)
.with_min_inner_size(window_size),
)
.unwrap();
self.run_windowed_in(window, event_loop)
} }
// TODO: Make windows into a custom view // TODO: Make windows into a custom view
pub fn run_windowed_in( pub fn run_windowed_in(self, window_attributes: WindowAttributes) -> Result<(), EventLoopError>
self,
window: Window,
event_loop: EventLoop<()>,
) -> Result<(), EventLoopError>
where where
State: 'static, State: 'static,
Logic: 'static, Logic: 'static,
View: 'static, View: 'static,
{ {
event_loop_runner::run(self.root_widget, window, event_loop, self.driver) event_loop_runner::run(window_attributes, self.root_widget, self.driver)
} }
} }
pub trait MasonryView<State, Action = ()>: Send + 'static { pub trait MasonryView<State, Action = ()>: Send + 'static {