mirror of https://github.com/linebender/xilem
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:
parent
392dbf5338
commit
6880515b95
File diff suppressed because it is too large
Load Diff
|
@ -34,6 +34,8 @@ smallvec = "1.13.2"
|
|||
fnv = "1.0.7"
|
||||
instant = "0.1.6"
|
||||
bitflags = "2.0.0"
|
||||
accesskit = "0.14.0"
|
||||
accesskit_winit = "0.20.0"
|
||||
|
||||
[package]
|
||||
name = "xilem_classic"
|
||||
|
|
|
@ -36,7 +36,9 @@ pollster = "0.3.0"
|
|||
unicode-segmentation = "1.11.0"
|
||||
# TODO: Is this still the most up-to-date crate for this?
|
||||
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]
|
||||
float-cmp = { version = "0.8.0", features = ["std"], default-features = false }
|
||||
|
|
|
@ -5,20 +5,22 @@
|
|||
|
||||
// On Windows platform, don't show a console when opening the app.
|
||||
#![windows_subsystem = "windows"]
|
||||
#![allow(clippy::single_match)]
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use accesskit::{DefaultActionVerb, Role};
|
||||
use masonry::app_driver::{AppDriver, DriverCtx};
|
||||
use masonry::widget::{Align, CrossAxisAlignment, Flex, Label, SizedBox, WidgetRef};
|
||||
use masonry::{
|
||||
Action, BoxConstraints, Color, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Point,
|
||||
PointerEvent, Size, StatusChange, TextEvent, Widget, WidgetId, WidgetPod,
|
||||
AccessCtx, AccessEvent, Action, BoxConstraints, Color, EventCtx, LayoutCtx, LifeCycle,
|
||||
LifeCycleCtx, PaintCtx, Point, PointerEvent, Size, StatusChange, TextEvent, Widget, WidgetId,
|
||||
WidgetPod,
|
||||
};
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use tracing::{trace, trace_span, Span};
|
||||
use vello::Scene;
|
||||
use winit::dpi::LogicalSize;
|
||||
use winit::event_loop::EventLoop;
|
||||
use winit::window::Window;
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -170,6 +172,19 @@ impl Widget for CalcButton {
|
|||
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) {
|
||||
match event {
|
||||
StatusChange::HotChanged(true) => {
|
||||
|
@ -200,6 +215,23 @@ impl Widget for CalcButton {
|
|||
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]> {
|
||||
smallvec![self.inner.as_dyn()]
|
||||
}
|
||||
|
@ -280,7 +312,7 @@ fn flex_row(
|
|||
}
|
||||
|
||||
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()
|
||||
.with_flex_spacer(0.2)
|
||||
.with_child(display)
|
||||
|
@ -338,18 +370,12 @@ fn build_calc() -> impl Widget {
|
|||
}
|
||||
|
||||
pub fn main() {
|
||||
let event_loop = EventLoop::new().unwrap();
|
||||
let window_size = LogicalSize::new(223., 300.);
|
||||
|
||||
#[allow(deprecated)]
|
||||
let window = event_loop
|
||||
.create_window(
|
||||
Window::default_attributes()
|
||||
let window_attributes = Window::default_attributes()
|
||||
.with_title("Simple Calculator")
|
||||
.with_resizable(true)
|
||||
.with_min_inner_size(window_size),
|
||||
)
|
||||
.unwrap();
|
||||
.with_min_inner_size(window_size);
|
||||
|
||||
let calc_state = CalcState {
|
||||
value: "0".to_string(),
|
||||
|
@ -358,5 +384,5 @@ pub fn main() {
|
|||
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();
|
||||
}
|
||||
|
|
|
@ -7,13 +7,15 @@
|
|||
// On Windows platform, don't show a console when opening the app.
|
||||
#![windows_subsystem = "windows"]
|
||||
|
||||
use accesskit::Role;
|
||||
use kurbo::Stroke;
|
||||
use masonry::app_driver::{AppDriver, DriverCtx};
|
||||
use masonry::kurbo::BezPath;
|
||||
use masonry::widget::{FillStrat, WidgetRef};
|
||||
use masonry::{
|
||||
Action, Affine, BoxConstraints, Color, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx,
|
||||
Point, PointerEvent, Rect, Size, StatusChange, TextEvent, Widget, WidgetId,
|
||||
AccessCtx, AccessEvent, Action, Affine, BoxConstraints, Color, EventCtx, LayoutCtx, LifeCycle,
|
||||
LifeCycleCtx, PaintCtx, Point, PointerEvent, Rect, Size, StatusChange, TextEvent, Widget,
|
||||
WidgetId,
|
||||
};
|
||||
use parley::layout::Alignment;
|
||||
use parley::style::{FontFamily, FontStack, StyleProperty};
|
||||
|
@ -21,7 +23,6 @@ use smallvec::SmallVec;
|
|||
use tracing::{trace_span, Span};
|
||||
use vello::peniko::{Brush, Fill, Format, Image};
|
||||
use vello::Scene;
|
||||
use winit::event_loop::EventLoop;
|
||||
use winit::window::Window;
|
||||
|
||||
struct Driver;
|
||||
|
@ -40,6 +41,8 @@ impl Widget for CustomWidget {
|
|||
|
||||
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 on_status_change(&mut self, _ctx: &mut LifeCycleCtx, _event: &StatusChange) {}
|
||||
|
@ -126,6 +129,17 @@ impl Widget for CustomWidget {
|
|||
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]> {
|
||||
SmallVec::new()
|
||||
}
|
||||
|
@ -137,13 +151,9 @@ impl Widget for CustomWidget {
|
|||
|
||||
pub fn main() {
|
||||
let my_string = "Masonry + Vello".to_string();
|
||||
let event_loop = EventLoop::new().unwrap();
|
||||
#[allow(deprecated)]
|
||||
let window = event_loop
|
||||
.create_window(Window::default_attributes().with_title("Fancy colots"))
|
||||
.unwrap();
|
||||
let window_attributes = Window::default_attributes().with_title("Fancy colors");
|
||||
|
||||
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> {
|
||||
|
|
|
@ -12,7 +12,6 @@ use masonry::widget::prelude::*;
|
|||
use masonry::widget::{Button, Flex, Label};
|
||||
use masonry::Action;
|
||||
use winit::dpi::LogicalSize;
|
||||
use winit::event_loop::EventLoop;
|
||||
use winit::window::Window;
|
||||
|
||||
const VERTICAL_WIDGET_SPACING: f64 = 20.0;
|
||||
|
@ -33,19 +32,13 @@ impl AppDriver for Driver {
|
|||
}
|
||||
|
||||
pub fn main() {
|
||||
let event_loop = EventLoop::new().unwrap();
|
||||
let window_size = LogicalSize::new(400.0, 400.0);
|
||||
#[allow(deprecated)]
|
||||
let window = event_loop
|
||||
.create_window(
|
||||
Window::default_attributes()
|
||||
let window_attributes = Window::default_attributes()
|
||||
.with_title("Hello World!")
|
||||
.with_resizable(true)
|
||||
.with_min_inner_size(window_size),
|
||||
)
|
||||
.unwrap();
|
||||
.with_min_inner_size(window_size);
|
||||
|
||||
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 {
|
||||
|
|
|
@ -13,7 +13,6 @@ use masonry::widget::{FillStrat, Image};
|
|||
use masonry::{Action, WidgetId};
|
||||
use vello::peniko::{Format, Image as ImageBuf};
|
||||
use winit::dpi::LogicalSize;
|
||||
use winit::event_loop::EventLoop;
|
||||
use winit::window::Window;
|
||||
|
||||
struct Driver;
|
||||
|
@ -29,17 +28,11 @@ pub fn main() {
|
|||
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 event_loop = EventLoop::new().unwrap();
|
||||
let window_size = LogicalSize::new(650.0, 450.0);
|
||||
#[allow(deprecated)]
|
||||
let window = event_loop
|
||||
.create_window(
|
||||
Window::default_attributes()
|
||||
let window_attributes = Window::default_attributes()
|
||||
.with_title("Simple image example")
|
||||
.with_min_inner_size(window_size)
|
||||
.with_max_inner_size(window_size),
|
||||
)
|
||||
.unwrap();
|
||||
.with_max_inner_size(window_size);
|
||||
|
||||
masonry::event_loop_runner::run(image, window, event_loop, Driver).unwrap();
|
||||
masonry::event_loop_runner::run(window_attributes, image, Driver).unwrap();
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
use std::any::Any;
|
||||
use std::time::Duration;
|
||||
|
||||
use accesskit::{NodeBuilder, TreeUpdate};
|
||||
use parley::FontContext;
|
||||
use tracing::{trace, warn};
|
||||
use winit::dpi::LogicalPosition;
|
||||
|
@ -86,6 +87,14 @@ pub struct PaintCtx<'a> {
|
|||
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> {
|
||||
// TODO
|
||||
#[allow(dead_code)]
|
||||
|
@ -100,6 +109,7 @@ impl_context_method!(
|
|||
LifeCycleCtx<'_>,
|
||||
PaintCtx<'_>,
|
||||
LayoutCtx<'_>,
|
||||
AccessCtx<'_>,
|
||||
{
|
||||
/// get the `WidgetId` of the current widget.
|
||||
pub fn widget_id(&self) -> WidgetId {
|
||||
|
@ -365,6 +375,12 @@ impl_context_method!(WidgetCtx<'_>, EventCtx<'_>, LifeCycleCtx<'_>, {
|
|||
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.
|
||||
pub fn request_anim_frame(&mut self) {
|
||||
trace!("request_anim_frame");
|
||||
|
@ -662,3 +678,19 @@ impl PaintCtx<'_> {
|
|||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ use crate::WidgetId;
|
|||
|
||||
use std::{collections::HashSet, path::PathBuf};
|
||||
|
||||
use accesskit::{Action, ActionData};
|
||||
use winit::dpi::{LogicalPosition, PhysicalPosition, PhysicalSize};
|
||||
use winit::event::{Ime, KeyEvent, Modifiers, MouseButton};
|
||||
use winit::keyboard::ModifiersState;
|
||||
|
@ -56,6 +57,14 @@ pub enum TextEvent {
|
|||
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)]
|
||||
pub struct PointerState {
|
||||
// TODO
|
||||
|
@ -230,6 +239,20 @@ impl PointerEvent {
|
|||
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 {
|
||||
|
@ -241,6 +264,48 @@ impl TextEvent {
|
|||
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 {
|
||||
|
|
|
@ -4,8 +4,9 @@
|
|||
use std::num::NonZeroUsize;
|
||||
use std::sync::Arc;
|
||||
|
||||
use accesskit_winit::Adapter;
|
||||
use tracing::subscriber::SetGlobalDefaultError;
|
||||
use tracing::warn;
|
||||
use tracing::{debug, warn};
|
||||
use vello::kurbo::Affine;
|
||||
use vello::util::{RenderContext, RenderSurface};
|
||||
use vello::{peniko::Color, AaSupport, RenderParams, Renderer, RendererOptions, Scene};
|
||||
|
@ -15,7 +16,7 @@ use winit::dpi::LogicalPosition;
|
|||
use winit::error::EventLoopError;
|
||||
use winit::event::WindowEvent as WinitWindowEvent;
|
||||
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::event::{PointerState, WindowEvent};
|
||||
|
@ -30,12 +31,33 @@ struct MainState<'a> {
|
|||
renderer: Option<Renderer>,
|
||||
pointer_state: PointerState,
|
||||
app_driver: Box<dyn AppDriver>,
|
||||
accesskit_adapter: Adapter,
|
||||
}
|
||||
|
||||
pub fn run(
|
||||
window_attributes: WindowAttributes,
|
||||
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,
|
||||
event_loop: EventLoop<()>,
|
||||
event_loop: EventLoop<accesskit_winit::Event>,
|
||||
accesskit_adapter: Adapter,
|
||||
root_widget: impl Widget,
|
||||
app_driver: impl AppDriver + 'static,
|
||||
) -> Result<(), EventLoopError> {
|
||||
let window = Arc::new(window);
|
||||
|
@ -57,6 +79,7 @@ pub fn run(
|
|||
renderer: None,
|
||||
pointer_state: PointerState::empty(),
|
||||
app_driver: Box::new(app_driver),
|
||||
accesskit_adapter,
|
||||
};
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
impl ApplicationHandler for MainState<'_> {
|
||||
impl ApplicationHandler<accesskit_winit::Event> for MainState<'_> {
|
||||
fn resumed(&mut self, _event_loop: &ActiveEventLoop) {
|
||||
// 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) {
|
||||
self.accesskit_adapter.process_event(&self.window, &event);
|
||||
|
||||
match event {
|
||||
WinitWindowEvent::RedrawRequested => {
|
||||
let scene = self.render_root.redraw();
|
||||
|
@ -149,10 +174,12 @@ impl ApplicationHandler for MainState<'_> {
|
|||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
while let Some(signal) = self.render_root.pop_signal() {
|
||||
match signal {
|
||||
render_root::RenderRootSignal::Action(action, widget_id) => {
|
||||
self.render_root.edit_root_widget(|root| {
|
||||
debug!("Action {:?} on widget {:?}", action, widget_id);
|
||||
let mut driver_ctx = DriverCtx {
|
||||
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::prelude::*;
|
||||
let filter_layer = if cfg!(debug_assertions) {
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
let default_level = if cfg!(debug_assertions) {
|
||||
LevelFilter::DEBUG
|
||||
} else {
|
||||
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()
|
||||
// Display target (eg "my_crate::some_mod::submod") with logs
|
||||
.with_target(true);
|
||||
|
||||
let registry = tracing_subscriber::registry()
|
||||
.with(filter_layer)
|
||||
.with(env_filter)
|
||||
.with(fmt_layer);
|
||||
tracing::dispatcher::set_global_default(registry.into())
|
||||
}
|
||||
|
|
|
@ -118,8 +118,10 @@ pub mod text2;
|
|||
|
||||
pub use action::Action;
|
||||
pub use box_constraints::BoxConstraints;
|
||||
pub use contexts::{EventCtx, LayoutCtx, LifeCycleCtx, PaintCtx, WidgetCtx};
|
||||
pub use event::{InternalLifeCycle, LifeCycle, PointerEvent, StatusChange, TextEvent, WindowTheme};
|
||||
pub use contexts::{AccessCtx, EventCtx, LayoutCtx, LifeCycleCtx, PaintCtx, WidgetCtx};
|
||||
pub use event::{
|
||||
AccessEvent, InternalLifeCycle, LifeCycle, PointerEvent, StatusChange, TextEvent, WindowTheme,
|
||||
};
|
||||
pub use kurbo::{Affine, Insets, Point, Rect, Size, Vec2};
|
||||
pub use parley::layout::Alignment as TextAlignment;
|
||||
pub use util::{AsAny, Handled};
|
||||
|
|
|
@ -3,11 +3,12 @@
|
|||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use accesskit::{ActionRequest, NodeBuilder, Tree, TreeUpdate};
|
||||
// Automatically defaults to std::time::Instant on non Wasm platforms
|
||||
use instant::Instant;
|
||||
use kurbo::Affine;
|
||||
use parley::FontContext;
|
||||
use tracing::{info_span, warn};
|
||||
use tracing::{debug, info_span, warn};
|
||||
use vello::peniko::{Color, Fill};
|
||||
use vello::Scene;
|
||||
use winit::dpi::{LogicalPosition, LogicalSize, PhysicalSize};
|
||||
|
@ -20,7 +21,8 @@ use crate::event::{PointerEvent, TextEvent, WindowEvent};
|
|||
use crate::kurbo::Point;
|
||||
use crate::widget::{WidgetMut, WidgetState};
|
||||
use crate::{
|
||||
Action, BoxConstraints, Handled, InternalLifeCycle, LifeCycle, Widget, WidgetId, WidgetPod,
|
||||
AccessCtx, AccessEvent, Action, BoxConstraints, Handled, InternalLifeCycle, LifeCycle, Widget,
|
||||
WidgetId, WidgetPod,
|
||||
};
|
||||
|
||||
// TODO - Remove pub(crate)
|
||||
|
@ -202,7 +204,10 @@ impl RenderRoot {
|
|||
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);
|
||||
|
||||
res
|
||||
|
@ -229,8 +234,11 @@ impl RenderRoot {
|
|||
let handled = {
|
||||
ctx.global_state
|
||||
.debug_logger
|
||||
.push_important_span(&format!("¨POINTER_EVENT {}", event.short_name()));
|
||||
let _span = info_span!("event").entered();
|
||||
.push_important_span(&format!("POINTER_EVENT {}", event.short_name()));
|
||||
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);
|
||||
ctx.global_state.debug_logger.pop_span();
|
||||
Handled::from(ctx.is_handled)
|
||||
|
@ -269,7 +277,10 @@ impl RenderRoot {
|
|||
ctx.global_state
|
||||
.debug_logger
|
||||
.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);
|
||||
ctx.global_state.debug_logger.pop_span();
|
||||
Handled::from(ctx.is_handled)
|
||||
|
@ -292,6 +303,41 @@ impl RenderRoot {
|
|||
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) {
|
||||
let mut widget_state =
|
||||
WidgetState::new(self.root.id(), Some(self.get_kurbo_size()), "<root>");
|
||||
|
@ -369,7 +415,10 @@ impl RenderRoot {
|
|||
};
|
||||
|
||||
let mut scene = Scene::new();
|
||||
{
|
||||
let _span = info_span!("paint").entered();
|
||||
self.root.paint(&mut ctx, &mut scene);
|
||||
}
|
||||
|
||||
// FIXME - This is a workaround to Vello panicking when given an
|
||||
// empty scene
|
||||
|
@ -386,6 +435,44 @@ impl RenderRoot {
|
|||
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 {
|
||||
let size = self.size.to_logical(self.scale_factor);
|
||||
kurbo::Size::new(size.width, size.height)
|
||||
|
|
|
@ -15,6 +15,8 @@ use std::cell::RefCell;
|
|||
use std::collections::VecDeque;
|
||||
use std::rc::Rc;
|
||||
|
||||
use accesskit::Role;
|
||||
use accesskit_winit::Event;
|
||||
use smallvec::SmallVec;
|
||||
use vello::Scene;
|
||||
|
||||
|
@ -24,10 +26,13 @@ use crate::*;
|
|||
|
||||
pub type PointerEventFn<S> = dyn FnMut(&mut S, &mut EventCtx, &PointerEvent);
|
||||
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 LifeCycleFn<S> = dyn FnMut(&mut S, &mut LifeCycleCtx, &LifeCycle);
|
||||
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 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]>;
|
||||
|
||||
#[cfg(FALSE)]
|
||||
|
@ -40,10 +45,13 @@ pub struct ModularWidget<S> {
|
|||
state: S,
|
||||
on_pointer_event: Option<Box<PointerEventFn<S>>>,
|
||||
on_text_event: Option<Box<TextEventFn<S>>>,
|
||||
on_access_event: Option<Box<AccessEventFn<S>>>,
|
||||
on_status_change: Option<Box<StatusChangeFn<S>>>,
|
||||
lifecycle: Option<Box<LifeCycleFn<S>>>,
|
||||
layout: Option<Box<LayoutFn<S>>>,
|
||||
paint: Option<Box<PaintFn<S>>>,
|
||||
role: Option<Box<RoleFn<S>>>,
|
||||
access: Option<Box<AccessFn<S>>>,
|
||||
children: Option<Box<ChildrenFn<S>>>,
|
||||
}
|
||||
|
||||
|
@ -84,10 +92,12 @@ pub struct Recording(Rc<RefCell<VecDeque<Record>>>);
|
|||
pub enum Record {
|
||||
PE(PointerEvent),
|
||||
TE(TextEvent),
|
||||
AE(AccessEvent),
|
||||
SC(StatusChange),
|
||||
L(LifeCycle),
|
||||
Layout(Size),
|
||||
Paint,
|
||||
Access,
|
||||
}
|
||||
|
||||
/// like `WidgetExt` but just for this one thing
|
||||
|
@ -112,10 +122,13 @@ impl<S> ModularWidget<S> {
|
|||
state,
|
||||
on_pointer_event: None,
|
||||
on_text_event: None,
|
||||
on_access_event: None,
|
||||
on_status_change: None,
|
||||
lifecycle: None,
|
||||
layout: None,
|
||||
paint: None,
|
||||
role: None,
|
||||
access: None,
|
||||
children: None,
|
||||
}
|
||||
}
|
||||
|
@ -136,6 +149,14 @@ impl<S> ModularWidget<S> {
|
|||
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(
|
||||
mut self,
|
||||
f: impl FnMut(&mut S, &mut LifeCycleCtx, &StatusChange) + 'static,
|
||||
|
@ -165,6 +186,16 @@ impl<S> ModularWidget<S> {
|
|||
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(
|
||||
mut self,
|
||||
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) {
|
||||
if let Some(f) = self.on_status_change.as_mut() {
|
||||
f(&mut self.state, ctx, event);
|
||||
|
@ -211,6 +248,18 @@ impl<S: 'static> Widget for ModularWidget<S> {
|
|||
.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) {
|
||||
if let Some(f) = self.paint.as_mut() {
|
||||
f(&mut self.state, ctx, scene);
|
||||
|
@ -256,6 +305,10 @@ impl Widget for ReplaceChild {
|
|||
todo!()
|
||||
}
|
||||
|
||||
fn on_access_event(&mut self, ctx: &mut EventCtx, event: &AccessEvent) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn on_status_change(&mut self, ctx: &mut LifeCycleCtx, _event: &StatusChange) {
|
||||
ctx.request_layout();
|
||||
}
|
||||
|
@ -272,6 +325,14 @@ impl Widget for ReplaceChild {
|
|||
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]> {
|
||||
self.child.widget().children()
|
||||
}
|
||||
|
@ -319,6 +380,11 @@ impl<W: Widget> Widget for Recorder<W> {
|
|||
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) {
|
||||
self.recording.push(Record::SC(event.clone()));
|
||||
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) {
|
||||
self.child.paint(ctx, scene);
|
||||
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]> {
|
||||
|
|
|
@ -8,15 +8,17 @@
|
|||
// size constraints to its child means that "aligning" a widget may actually change
|
||||
// its computed size. See issue #3.
|
||||
|
||||
use accesskit::Role;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use tracing::{trace, trace_span, Span};
|
||||
use vello::Scene;
|
||||
|
||||
use crate::contexts::AccessCtx;
|
||||
use crate::paint_scene_helpers::UnitPoint;
|
||||
use crate::widget::{WidgetPod, WidgetRef};
|
||||
use crate::{
|
||||
BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, PointerEvent, Rect,
|
||||
Size, StatusChange, TextEvent, Widget,
|
||||
AccessEvent, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx,
|
||||
PointerEvent, Rect, Size, StatusChange, TextEvent, Widget,
|
||||
};
|
||||
|
||||
// TODO - Have child widget type as generic argument
|
||||
|
@ -89,6 +91,10 @@ impl Widget for Align {
|
|||
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) {
|
||||
self.child.lifecycle(ctx, event);
|
||||
}
|
||||
|
@ -146,6 +152,14 @@ impl Widget for Align {
|
|||
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]> {
|
||||
smallvec![self.child.as_dyn()]
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
//! A button widget.
|
||||
|
||||
use accesskit::{DefaultActionVerb, Role};
|
||||
use smallvec::SmallVec;
|
||||
use tracing::{trace, trace_span, Span};
|
||||
use vello::Scene;
|
||||
|
@ -12,8 +13,8 @@ use crate::paint_scene_helpers::{fill_lin_gradient, stroke, UnitPoint};
|
|||
use crate::text2::TextStorage;
|
||||
use crate::widget::{Label, WidgetMut, WidgetPod, WidgetRef};
|
||||
use crate::{
|
||||
theme, BoxConstraints, EventCtx, Insets, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx,
|
||||
PointerEvent, Size, StatusChange, TextEvent, Widget,
|
||||
theme, AccessCtx, AccessEvent, BoxConstraints, EventCtx, Insets, LayoutCtx, LifeCycle,
|
||||
LifeCycleCtx, PaintCtx, PointerEvent, Size, StatusChange, TextEvent, Widget,
|
||||
};
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
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) {
|
||||
ctx.request_paint();
|
||||
}
|
||||
|
@ -173,6 +187,20 @@ impl<T: TextStorage> Widget for Button<T> {
|
|||
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]> {
|
||||
SmallVec::new()
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
//! A checkbox widget.
|
||||
|
||||
use accesskit::{DefaultActionVerb, Role, Toggled};
|
||||
use kurbo::{Affine, Stroke};
|
||||
use smallvec::SmallVec;
|
||||
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::widget::{Label, WidgetMut, WidgetRef};
|
||||
use crate::{
|
||||
theme, ArcStr, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx,
|
||||
PointerEvent, StatusChange, TextEvent, Widget, WidgetPod,
|
||||
theme, AccessCtx, AccessEvent, ArcStr, BoxConstraints, EventCtx, LayoutCtx, LifeCycle,
|
||||
LifeCycleCtx, PaintCtx, PointerEvent, StatusChange, TextEvent, Widget, WidgetPod,
|
||||
};
|
||||
|
||||
/// 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_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) {
|
||||
ctx.request_paint();
|
||||
}
|
||||
|
@ -166,6 +180,27 @@ impl<T: TextStorage> Widget for Checkbox<T> {
|
|||
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]> {
|
||||
SmallVec::new()
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
//! A widget that arranges its children in a one-dimensional array.
|
||||
|
||||
use accesskit::Role;
|
||||
use kurbo::{Affine, Stroke};
|
||||
use smallvec::SmallVec;
|
||||
use tracing::{trace, trace_span, Span};
|
||||
|
@ -13,8 +14,8 @@ use crate::kurbo::Vec2;
|
|||
use crate::theme::get_debug_color;
|
||||
use crate::widget::{WidgetMut, WidgetRef};
|
||||
use crate::{
|
||||
BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Point, PointerEvent,
|
||||
Rect, Size, StatusChange, TextEvent, Widget, WidgetId, WidgetPod,
|
||||
AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx,
|
||||
Point, PointerEvent, Rect, Size, StatusChange, TextEvent, Widget, WidgetId, WidgetPod,
|
||||
};
|
||||
|
||||
/// 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 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]> {
|
||||
self.children
|
||||
.iter()
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
//! An Image widget.
|
||||
//! Please consider using SVG and the SVG widget as it scales much better.
|
||||
|
||||
use accesskit::Role;
|
||||
use kurbo::Affine;
|
||||
use smallvec::SmallVec;
|
||||
use tracing::{trace, trace_span, Span};
|
||||
|
@ -12,8 +13,8 @@ use vello::Scene;
|
|||
|
||||
use crate::widget::{FillStrat, WidgetMut, WidgetRef};
|
||||
use crate::{
|
||||
BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, PointerEvent, Size,
|
||||
StatusChange, TextEvent, Widget,
|
||||
AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx,
|
||||
PointerEvent, Size, StatusChange, TextEvent, Widget,
|
||||
};
|
||||
|
||||
// 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_access_event(&mut self, _ctx: &mut EventCtx, _event: &AccessEvent) {}
|
||||
|
||||
fn on_status_change(&mut self, _ctx: &mut LifeCycleCtx, _event: &StatusChange) {}
|
||||
|
||||
fn lifecycle(&mut self, _ctx: &mut LifeCycleCtx, _event: &LifeCycle) {}
|
||||
|
@ -101,6 +104,14 @@ impl Widget for Image {
|
|||
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]> {
|
||||
SmallVec::new()
|
||||
}
|
||||
|
|
|
@ -1,27 +1,24 @@
|
|||
// Copyright 2019 the Xilem Authors and the Druid Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! A label widget.
|
||||
|
||||
use accesskit::Role;
|
||||
use kurbo::{Affine, Point, Size};
|
||||
use parley::{
|
||||
layout::Alignment,
|
||||
style::{FontFamily, FontStack},
|
||||
};
|
||||
use parley::layout::Alignment;
|
||||
use parley::style::{FontFamily, FontStack};
|
||||
use smallvec::SmallVec;
|
||||
use tracing::trace;
|
||||
use vello::{
|
||||
peniko::{BlendMode, Color},
|
||||
Scene,
|
||||
};
|
||||
use vello::peniko::BlendMode;
|
||||
use vello::Scene;
|
||||
|
||||
use crate::text2::{TextBrush, TextLayout, TextStorage};
|
||||
use crate::widget::{WidgetMut, WidgetRef};
|
||||
use crate::{
|
||||
text2::{TextBrush, TextLayout, TextStorage},
|
||||
widget::WidgetRef,
|
||||
ArcStr, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, PointerEvent,
|
||||
StatusChange, TextEvent, Widget,
|
||||
AccessCtx, AccessEvent, ArcStr, BoxConstraints, Color, EventCtx, LayoutCtx, LifeCycle,
|
||||
LifeCycleCtx, PaintCtx, PointerEvent, StatusChange, TextEvent, Widget,
|
||||
};
|
||||
|
||||
use super::WidgetMut;
|
||||
|
||||
// added padding between the edges of the widget and the text.
|
||||
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?
|
||||
}
|
||||
|
||||
fn on_access_event(&mut self, _ctx: &mut EventCtx, _event: &AccessEvent) {}
|
||||
|
||||
#[allow(missing_docs)]
|
||||
fn on_status_change(&mut self, _ctx: &mut LifeCycleCtx, event: &StatusChange) {
|
||||
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]> {
|
||||
SmallVec::new()
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
use std::ops::Range;
|
||||
|
||||
use accesskit::Role;
|
||||
use kurbo::Affine;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use tracing::{trace_span, Span};
|
||||
|
@ -14,8 +15,8 @@ use vello::Scene;
|
|||
use crate::kurbo::{Point, Rect, Size, Vec2};
|
||||
use crate::widget::{Axis, ScrollBar, WidgetMut, WidgetRef};
|
||||
use crate::{
|
||||
BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, PointerEvent,
|
||||
StatusChange, TextEvent, Widget, WidgetPod,
|
||||
AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx,
|
||||
PointerEvent, StatusChange, TextEvent, Widget, WidgetPod,
|
||||
};
|
||||
|
||||
// TODO - refactor - see issue #15
|
||||
|
@ -289,6 +290,14 @@ impl<W: Widget> Widget for Portal<W> {
|
|||
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 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]> {
|
||||
smallvec![self.child.as_dyn()]
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright 2018 the Xilem Authors and the Druid Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use accesskit::Role;
|
||||
use kurbo::{Affine, Point, Size};
|
||||
use parley::{
|
||||
layout::Alignment,
|
||||
|
@ -13,8 +14,8 @@ use vello::{peniko::BlendMode, Scene};
|
|||
use crate::{
|
||||
text2::{Selectable, TextBrush, TextWithSelection},
|
||||
widget::label::LABEL_X_PADDING,
|
||||
BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, PointerEvent,
|
||||
StatusChange, TextEvent, Widget,
|
||||
AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx,
|
||||
PointerEvent, StatusChange, TextEvent, Widget,
|
||||
};
|
||||
|
||||
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)]
|
||||
fn on_status_change(&mut self, ctx: &mut LifeCycleCtx, event: &StatusChange) {
|
||||
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]> {
|
||||
SmallVec::new()
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use accesskit::Role;
|
||||
use smallvec::SmallVec;
|
||||
use tracing::{trace_span, Span};
|
||||
use vello::Scene;
|
||||
|
@ -12,8 +13,8 @@ use crate::kurbo::Rect;
|
|||
use crate::paint_scene_helpers::{fill_color, stroke};
|
||||
use crate::widget::{WidgetMut, WidgetRef};
|
||||
use crate::{
|
||||
theme, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Point,
|
||||
PointerEvent, Size, StatusChange, TextEvent, Widget,
|
||||
theme, AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx,
|
||||
PaintCtx, Point, PointerEvent, Size, StatusChange, TextEvent, Widget,
|
||||
};
|
||||
|
||||
// RULES
|
||||
|
@ -172,6 +173,10 @@ impl Widget for ScrollBar {
|
|||
|
||||
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 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]> {
|
||||
SmallVec::new()
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
//! A widget with predefined size.
|
||||
|
||||
use accesskit::Role;
|
||||
use kurbo::Affine;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
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::widget::{WidgetId, WidgetMut, WidgetPod, WidgetRef};
|
||||
use crate::{
|
||||
BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Point, PointerEvent,
|
||||
Size, StatusChange, TextEvent, Widget,
|
||||
AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx,
|
||||
Point, PointerEvent, Size, StatusChange, TextEvent, Widget,
|
||||
};
|
||||
|
||||
// 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 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]> {
|
||||
if let Some(child) = &self.child {
|
||||
smallvec![child.as_dyn()]
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
use std::f64::consts::PI;
|
||||
|
||||
use accesskit::Role;
|
||||
use kurbo::{Affine, Cap, Stroke};
|
||||
use smallvec::SmallVec;
|
||||
use tracing::trace;
|
||||
|
@ -13,8 +14,8 @@ use vello::Scene;
|
|||
use crate::kurbo::Line;
|
||||
use crate::widget::{WidgetMut, WidgetRef};
|
||||
use crate::{
|
||||
theme, BoxConstraints, Color, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Point,
|
||||
PointerEvent, Size, StatusChange, TextEvent, Vec2, Widget,
|
||||
theme, AccessCtx, AccessEvent, BoxConstraints, Color, EventCtx, LayoutCtx, LifeCycle,
|
||||
LifeCycleCtx, PaintCtx, Point, PointerEvent, Size, StatusChange, TextEvent, Vec2, Widget,
|
||||
};
|
||||
|
||||
// TODO - Set color
|
||||
|
@ -72,6 +73,8 @@ impl Widget for Spinner {
|
|||
|
||||
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 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]> {
|
||||
SmallVec::new()
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
//! A widget which splits an area in two, with a settable ratio, and optional draggable resizing.
|
||||
|
||||
use accesskit::Role;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use tracing::{trace, trace_span, warn, Span};
|
||||
use vello::Scene;
|
||||
|
@ -15,8 +16,8 @@ use crate::paint_scene_helpers::{fill_color, stroke};
|
|||
use crate::widget::flex::Axis;
|
||||
use crate::widget::{WidgetMut, WidgetPod, WidgetRef};
|
||||
use crate::{
|
||||
theme, BoxConstraints, Color, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Point,
|
||||
PointerEvent, Rect, Size, StatusChange, TextEvent, Widget,
|
||||
theme, AccessCtx, AccessEvent, BoxConstraints, Color, EventCtx, LayoutCtx, LifeCycle,
|
||||
LifeCycleCtx, PaintCtx, Point, PointerEvent, Rect, Size, StatusChange, TextEvent, Widget,
|
||||
};
|
||||
|
||||
// TODO - Have child widget type as generic argument
|
||||
|
@ -443,6 +444,11 @@ impl Widget for Split {
|
|||
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 lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle) {
|
||||
|
@ -558,6 +564,15 @@ impl Widget for Split {
|
|||
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]> {
|
||||
smallvec![self.child1.as_dyn(), self.child2.as_dyn()]
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright 2018 the Xilem Authors and the Druid Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use accesskit::Role;
|
||||
use kurbo::{Affine, Point, Size, Stroke};
|
||||
use parley::{
|
||||
layout::Alignment,
|
||||
|
@ -15,8 +16,8 @@ use vello::{
|
|||
|
||||
use crate::{
|
||||
text2::{EditableText, TextBrush, TextEditor, TextWithSelection},
|
||||
BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, PointerEvent,
|
||||
StatusChange, TextEvent, Widget,
|
||||
AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx,
|
||||
PointerEvent, StatusChange, TextEvent, Widget,
|
||||
};
|
||||
|
||||
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)]
|
||||
fn on_status_change(&mut self, ctx: &mut LifeCycleCtx, event: &StatusChange) {
|
||||
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]> {
|
||||
SmallVec::new()
|
||||
}
|
||||
|
|
|
@ -6,15 +6,16 @@ use std::num::NonZeroU64;
|
|||
use std::ops::{Deref, DerefMut};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
use accesskit::Role;
|
||||
use smallvec::SmallVec;
|
||||
use tracing::{trace_span, Span};
|
||||
use vello::Scene;
|
||||
|
||||
use crate::event::StatusChange;
|
||||
use crate::event::{PointerEvent, TextEvent};
|
||||
use crate::event::{AccessEvent, PointerEvent, StatusChange, TextEvent};
|
||||
use crate::widget::WidgetRef;
|
||||
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`].
|
||||
|
@ -39,7 +40,7 @@ use crate::{
|
|||
/// 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.
|
||||
#[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
|
||||
/// 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_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)]
|
||||
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.
|
||||
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.
|
||||
///
|
||||
/// 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) })
|
||||
}
|
||||
|
||||
pub(crate) fn to_raw(self) -> u64 {
|
||||
pub fn to_raw(self) -> u64 {
|
||||
self.0.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WidgetId> for accesskit::NodeId {
|
||||
fn from(id: WidgetId) -> accesskit::NodeId {
|
||||
accesskit::NodeId(id.0.into())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO - remove
|
||||
impl Widget for Box<dyn Widget> {
|
||||
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);
|
||||
}
|
||||
|
||||
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) {
|
||||
self.deref_mut().on_status_change(ctx, event);
|
||||
}
|
||||
|
@ -259,6 +277,14 @@ impl Widget for Box<dyn Widget> {
|
|||
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 {
|
||||
self.deref().type_name()
|
||||
}
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
// Copyright 2018 the Xilem Authors and the Druid Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use accesskit::{NodeBuilder, NodeId};
|
||||
use tracing::{info_span, trace, warn};
|
||||
use vello::Scene;
|
||||
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::paint_scene_helpers::stroke;
|
||||
use crate::render_root::RenderRootState;
|
||||
use crate::theme::get_debug_color;
|
||||
use crate::widget::{WidgetRef, WidgetState};
|
||||
use crate::{
|
||||
BoxConstraints, EventCtx, InternalLifeCycle, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx,
|
||||
StatusChange, Widget, WidgetId,
|
||||
AccessCtx, BoxConstraints, EventCtx, InternalLifeCycle, LayoutCtx, LifeCycle, LifeCycleCtx,
|
||||
PaintCtx, StatusChange, Widget, WidgetId,
|
||||
};
|
||||
|
||||
// TODO - rewrite links in doc
|
||||
|
@ -497,6 +498,59 @@ impl<W: Widget> WidgetPod<W> {
|
|||
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 ---
|
||||
|
||||
// TODO #5 - Some implicit invariants:
|
||||
|
@ -629,6 +683,8 @@ impl<W: Widget> WidgetPod<W> {
|
|||
self.state.needs_layout = true;
|
||||
self.state.needs_paint = true;
|
||||
self.state.needs_window_origin = true;
|
||||
self.state.needs_accessibility_update = true;
|
||||
self.state.request_accessibility_update = true;
|
||||
|
||||
true
|
||||
}
|
||||
|
@ -791,6 +847,8 @@ impl<W: Widget> WidgetPod<W> {
|
|||
self.state.is_expecting_place_child_call = true;
|
||||
// TODO - Not everything that has been re-laid out needs to be repainted.
|
||||
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());
|
||||
|
||||
|
@ -945,6 +1003,78 @@ impl<W: Widget> WidgetPod<W> {
|
|||
let scene = &mut self.fragment;
|
||||
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?
|
||||
|
|
|
@ -83,6 +83,7 @@ pub struct WidgetState {
|
|||
|
||||
pub(crate) needs_layout: 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.
|
||||
pub(crate) needs_window_origin: bool,
|
||||
|
@ -90,6 +91,9 @@ pub struct WidgetState {
|
|||
/// Any descendant has requested an animation frame.
|
||||
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) focus_chain: Vec<WidgetId>,
|
||||
|
@ -161,11 +165,13 @@ impl WidgetState {
|
|||
is_hot: false,
|
||||
needs_layout: false,
|
||||
needs_paint: false,
|
||||
needs_accessibility_update: false,
|
||||
needs_window_origin: false,
|
||||
is_active: false,
|
||||
has_active: false,
|
||||
has_focus: false,
|
||||
request_anim: false,
|
||||
request_accessibility_update: false,
|
||||
focus_chain: Vec::new(),
|
||||
children: Bloom::new(),
|
||||
children_changed: false,
|
||||
|
@ -214,6 +220,7 @@ impl WidgetState {
|
|||
self.needs_paint |= child_state.needs_paint;
|
||||
self.needs_window_origin |= child_state.needs_window_origin;
|
||||
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.is_explicitly_disabled_new != child_state.is_explicitly_disabled;
|
||||
|
|
|
@ -23,3 +23,5 @@ winit.workspace = true
|
|||
tracing.workspace = true
|
||||
vello.workspace = true
|
||||
smallvec.workspace = true
|
||||
accesskit.workspace = true
|
||||
accesskit_winit.workspace = true
|
||||
|
|
|
@ -3,10 +3,11 @@
|
|||
|
||||
use std::{any::Any, ops::Deref};
|
||||
|
||||
use accesskit::Role;
|
||||
use masonry::widget::{WidgetMut, WidgetRef};
|
||||
use masonry::{
|
||||
BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Point, PointerEvent,
|
||||
Size, StatusChange, TextEvent, Widget, WidgetPod,
|
||||
AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx,
|
||||
Point, PointerEvent, Size, StatusChange, TextEvent, Widget, WidgetPod,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use vello::Scene;
|
||||
|
@ -201,6 +202,9 @@ impl Widget for DynWidget {
|
|||
fn on_text_event(&mut self, ctx: &mut EventCtx, event: &TextEvent) {
|
||||
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) {
|
||||
// Intentionally do nothing
|
||||
|
@ -220,6 +224,14 @@ impl Widget for DynWidget {
|
|||
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]> {
|
||||
let mut vec = SmallVec::new();
|
||||
vec.push(self.inner.as_dyn());
|
||||
|
|
|
@ -4,17 +4,22 @@
|
|||
#![allow(clippy::comparison_chain)]
|
||||
use std::{any::Any, collections::HashMap};
|
||||
|
||||
use accesskit::Role;
|
||||
use masonry::{
|
||||
app_driver::AppDriver,
|
||||
event_loop_runner,
|
||||
widget::{WidgetMut, WidgetRef},
|
||||
BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Point, PointerEvent,
|
||||
Size, StatusChange, TextEvent, Widget, WidgetId, WidgetPod,
|
||||
AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx,
|
||||
Point, PointerEvent, Size, StatusChange, TextEvent, Widget, WidgetId, WidgetPod,
|
||||
};
|
||||
pub use masonry::{widget::Axis, Color, TextAlignment};
|
||||
use smallvec::SmallVec;
|
||||
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 id;
|
||||
|
@ -54,6 +59,9 @@ impl<E: 'static + Widget> Widget for RootWidget<E> {
|
|||
fn on_text_event(&mut self, ctx: &mut EventCtx, event: &TextEvent) {
|
||||
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) {
|
||||
// Intentionally do nothing?
|
||||
|
@ -73,6 +81,14 @@ impl<E: 'static + Widget> Widget for RootWidget<E> {
|
|||
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]> {
|
||||
let mut vec = SmallVec::new();
|
||||
vec.push(self.pod.as_dyn());
|
||||
|
@ -171,32 +187,22 @@ where
|
|||
Logic: 'static,
|
||||
View: 'static,
|
||||
{
|
||||
let event_loop = EventLoop::new().unwrap();
|
||||
let window_size = LogicalSize::new(600., 800.);
|
||||
#[allow(deprecated)]
|
||||
let window = event_loop
|
||||
.create_window(
|
||||
Window::default_attributes()
|
||||
let window_attributes = Window::default_attributes()
|
||||
.with_title(window_title)
|
||||
.with_resizable(true)
|
||||
.with_min_inner_size(window_size),
|
||||
)
|
||||
.unwrap();
|
||||
self.run_windowed_in(window, event_loop)
|
||||
.with_min_inner_size(window_size);
|
||||
self.run_windowed_in(window_attributes)
|
||||
}
|
||||
|
||||
// TODO: Make windows into a custom view
|
||||
pub fn run_windowed_in(
|
||||
self,
|
||||
window: Window,
|
||||
event_loop: EventLoop<()>,
|
||||
) -> Result<(), EventLoopError>
|
||||
pub fn run_windowed_in(self, window_attributes: WindowAttributes) -> Result<(), EventLoopError>
|
||||
where
|
||||
State: 'static,
|
||||
Logic: '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 {
|
||||
|
|
Loading…
Reference in New Issue