Repository: zng-ui/zng (language: rust)

Issue:
Review default accessibility actions
We only set click "verb", there are others.

[Relevant line](https://github.com/zng-ui/zng/blob/ec4b7d29101465dfa1b3f310ad647ff149616045/zng-view/src/util.rs#L1268)


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0554aafc4..41fd991cb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,10 @@
 * Fix `WINDOWS.frame_image` var not updating on load or error.
 * Fix cursor not resetting on widget deinit.
 * Add missing `zng::app::test_log`.
+* **Breaking:** View API accessibility updates.
+    - Added `Event::AccessDeinit`, access can now be disabled by the system.
+    - Removed `WindowRequest::access_root`, no longer needed.
+    - Note that this is only a breaking change for direct dependents of `zng-view-api`.
 
 # 0.3.3
 
diff --git a/crates/zng-app/src/access.rs b/crates/zng-app/src/access.rs
index a1c3973c2..a032c9bc5 100644
--- a/crates/zng-app/src/access.rs
+++ b/crates/zng-app/src/access.rs
@@ -17,6 +17,11 @@ pub(super) fn on_access_init(window_id: WindowId) -> EventUpdate {
     ACCESS_INITED_EVENT.new_update(args)
 }
 
+pub(super) fn on_access_deinit(window_id: WindowId) -> EventUpdate {
+    let args = AccessDeinitedArgs::now(window_id);
+    ACCESS_DEINITED_EVENT.new_update(args)
+}
+
 pub(super) fn on_access_command(window_id: WindowId, widget_id: WidgetId, command: AccessCmd) -> Option<EventUpdate> {
     match command {
         AccessCmd::Click(primary) => {
@@ -89,6 +94,19 @@ event_args! {
         }
     }
 
+        /// Arguments for the [`ACCESS_DEINITED_EVENT`].
+        pub struct AccessDeinitedArgs {
+            /// Target window.
+            pub window_id: WindowId,
+
+            ..
+
+            /// Event is broadcast.
+            fn delivery_list(&self, list: &mut UpdateDeliveryList) {
+                list.search_all()
+            }
+        }
+
     /// Arguments for the [`ACCESS_CLICK_EVENT`].
     pub struct AccessClickArgs {
         /// Target window.
@@ -300,9 +318,12 @@ impl AccessClickArgs {
 }
 
 event! {
-    /// Accessibility info was requested for the first time in a window.
+    /// Accessibility info is now required for the window.
     pub static ACCESS_INITED_EVENT: AccessInitedArgs;
 
+    /// Accessibility info is no longer required for the window.
+    pub static ACCESS_DEINITED_EVENT: AccessDeinitedArgs;
+
     /// Run the primary or context click action.
     pub static ACCESS_CLICK_EVENT: AccessClickArgs;
 
diff --git a/crates/zng-app/src/running.rs b/crates/zng-app/src/running.rs
index 0e8b7ba09..f65a222ef 100644
--- a/crates/zng-app/src/running.rs
+++ b/crates/zng-app/src/running.rs
@@ -377,6 +377,9 @@ impl<E: AppExtension> RunningApp<E> {
                     self.notify_event(update, observer);
                 }
             }
+            Event::AccessDeinit { window: w_id } => {
+                self.notify_event(crate::access::on_access_deinit(window_id(w_id)), observer);
+            }
 
             // native dialog responses
             Event::MsgDialogResponse(id, response) => {
diff --git a/crates/zng-ext-window/src/control.rs b/crates/zng-ext-window/src/control.rs
index 863b88e43..4862e736e 100644
--- a/crates/zng-ext-window/src/control.rs
+++ b/crates/zng-ext-window/src/control.rs
@@ -3,7 +3,7 @@
 use std::{mem, sync::Arc};
 
 use zng_app::{
-    access::ACCESS_INITED_EVENT,
+    access::{ACCESS_DEINITED_EVENT, ACCESS_INITED_EVENT},
     app_hn_once,
     event::{AnyEventArgs, CommandHandle},
     render::{FrameBuilder, FrameUpdate},
@@ -515,8 +515,7 @@ impl HeadedCtrl {
             self.vars.0.actual_color_scheme.set(scheme);
         }
 
-        if let Some(e) = self.vars.0.access_enabled.get_new() {
-            debug_assert!(e.is_enabled());
+        if self.vars.0.access_enabled.is_new() {
             UPDATES.update_info_window(WINDOW.id());
         } else if self.vars.0.access_enabled.get() == AccessEnabled::VIEW && WINDOW_FOCUS.focused().is_new() {
             self.update_access_focused();
@@ -750,9 +749,18 @@ impl HeadedCtrl {
             }
         } else if let Some(args) = ACCESS_INITED_EVENT.on(update) {
             if args.window_id == WINDOW.id() {
-                tracing::info!("accessibility info enabled for {:?}", args.window_id);
+                tracing::info!("accessibility info enabled in view for {:?}", args.window_id);
                 self.vars.0.access_enabled.set(AccessEnabled::VIEW);
             }
+        } else if let Some(args) = ACCESS_DEINITED_EVENT.on(update) {
+            if args.window_id == WINDOW.id() {
+                tracing::info!("accessibility info disabled in view for {:?}", args.window_id);
+                self.vars.0.access_enabled.modify(|a| {
+                    if a.is_enabled() {
+                        *a.to_mut() = AccessEnabled::APP;
+                    }
+                });
+            }
         } else if let Some(args) = VIEW_PROCESS_INITED_EVENT.on(update) {
             if let Some(view) = &self.window {
                 if view.renderer().generation() != Ok(args.generation) {
@@ -1067,7 +1075,6 @@ impl HeadedCtrl {
             transparent: self.transparent,
             capture_mode: matches!(self.vars.frame_capture_mode().get(), FrameCaptureMode::All),
             render_mode: self.render_mode.unwrap_or_else(|| WINDOWS.default_render_mode().get()),
-            access_root: self.content.root_ctx.id().into(),
 
             focus: self.start_focused,
             focus_indicator: self.vars.focus_indicator().get(),
@@ -1197,8 +1204,6 @@ impl HeadedCtrl {
             focus: WINDOWS.is_focused(WINDOW.id()).unwrap_or(false),
             focus_indicator: self.vars.focus_indicator().get(),
 
-            access_root: self.content.root_ctx.id().into(),
-
             ime_area: self.ime_info.as_ref().and_then(|a| {
                 let info = WINDOW.info();
                 let area = info.get(a.target.widget_id())?.ime_area().to_dip(info.scale_factor());
@@ -1414,8 +1419,7 @@ impl HeadlessWithRendererCtrl {
     }
 
     pub fn update(&mut self, update_widgets: &WidgetUpdates) {
-        if let Some(e) = self.vars.0.access_enabled.get_new() {
-            debug_assert!(e.is_enabled());
+        if self.vars.0.access_enabled.is_new() {
             UPDATES.update_info_window(WINDOW.id());
         }
 
@@ -1662,8 +1666,7 @@ impl HeadlessCtrl {
     }
 
     pub fn update(&mut self, update_widgets: &WidgetUpdates) {
-        if let Some(e) = self.vars.0.access_enabled.get_new() {
-            debug_assert!(e.is_enabled());
+        if self.vars.0.access_enabled.is_new() {
             UPDATES.update_info_window(WINDOW.id());
         }
 
diff --git a/crates/zng-ext-window/src/vars.rs b/crates/zng-ext-window/src/vars.rs
index 32a8005eb..41703f7ca 100644
--- a/crates/zng-ext-window/src/vars.rs
+++ b/crates/zng-ext-window/src/vars.rs
@@ -653,9 +653,11 @@ impl WindowVars {
 
     /// If an accessibility service has requested info from this window.
     ///
-    /// This variable can only update with more bit flags, you can enable it in the app-process using [`enable_access`], the
+    /// You can enable this in the app-process using [`enable_access`], the
     /// view-process can also enable it on the first request for accessibility info by an external tool.
     ///
+    /// This variable does not update to fully disabled after first enable, but the VIEW bit can disable and re-enable.
+    ///
     /// [`enable_access`]: crate::WINDOW_Ext::enable_access
     pub fn access_enabled(&self) -> ReadOnlyArcVar<AccessEnabled> {
         self.0.access_enabled.read_only()
diff --git a/crates/zng-view-api/src/types.rs b/crates/zng-view-api/src/types.rs
index e9e983686..5357b849d 100644
--- a/crates/zng-view-api/src/types.rs
+++ b/crates/zng-view-api/src/types.rs
@@ -518,12 +518,11 @@ pub enum Event {
     /// User responded to a native file dialog.
     FileDialogResponse(DialogId, FileDialogResponse),
 
-    /// Accessibility info tree was requested for the first time on a window.
+    /// Accessibility info tree is now required for the window.
     AccessInit {
-        /// Window that received the first accessibility request.
+        /// Window that must now build access info.
         window: WindowId,
     },
-
     /// Accessibility command.
     AccessCommand {
         /// Window that had pixels copied.
@@ -533,6 +532,14 @@ pub enum Event {
         /// Command.
         command: AccessCmd,
     },
+    /// Accessibility info tree is no longer needed for the window.
+    ///
+    /// Note that accessibility may be enabled again after this. It is not an error
+    /// to send access updates after this, but they will be ignored.
+    AccessDeinit {
+        /// Window that can release access info.
+        window: WindowId,
+    },
 
     /// System low memory warning, some platforms may kill the app if it does not release memory.
     LowMemory,
diff --git a/crates/zng-view-api/src/window.rs b/crates/zng-view-api/src/window.rs
index ec64cbb0f..171f37223 100644
--- a/crates/zng-view-api/src/window.rs
+++ b/crates/zng-view-api/src/window.rs
@@ -6,7 +6,6 @@ use serde::{Deserialize, Serialize};
 use zng_txt::Txt;
 
 use crate::{
-    access::AccessNodeId,
     api_extension::{ApiExtensionId, ApiExtensionPayload},
     config::ColorScheme,
     display_list::{DisplayList, FrameValueUpdate},
@@ -408,15 +407,6 @@ pub struct WindowRequest {
     /// The `zng-view` crate implements this by calling `RendererExtension::configure` with the payload.
     pub extensions: Vec<(ApiExtensionId, ApiExtensionPayload)>,
 
-    /// Initial accessibility info tree root widget.
-    ///
-    /// This is used to declare a placeholder node on the first request for accessibility info, the [`Event::AccessInit`] event
-    /// is send to the window if any accessibility service requests the tree, the window must send the full info at that moment,
-    /// after the init the window should assume that accessibility info is required for the lifetime of the window and updates must be send.
-    ///
-    /// [`Event::AccessInit`]: crate::Event::AccessInit
-    pub access_root: AccessNodeId,
-
     /// IME cursor area, if IME is enabled.
     pub ime_area: Option<DipRect>,
 }
diff --git a/crates/zng-view/Cargo.toml b/crates/zng-view/Cargo.toml
index 3286425d8..666e4057b 100644
--- a/crates/zng-view/Cargo.toml
+++ b/crates/zng-view/Cargo.toml
@@ -85,8 +85,8 @@ rustc-hash = "1.1"
 rayon = "1.10"
 serde = "1.0"
 rfd = { version = "0.12", default_features = false, features = ["xdg-portal"] }
-accesskit = "0.12"
-accesskit_winit = "0.18"
+accesskit = "0.13"
+accesskit_winit = "0.19"
 
 [target.'cfg(windows)'.dependencies.windows-sys]
 version = "0.48.0" # matches winit, glutin, other crates
diff --git a/crates/zng-view/src/util.rs b/crates/zng-view/src/util.rs
index fb2ee81d0..70e0a7772 100644
--- a/crates/zng-view/src/util.rs
+++ b/crates/zng-view/src/util.rs
@@ -1206,18 +1206,6 @@ pub(crate) fn accesskit_to_event(
     })
 }
 
-pub(crate) fn access_tree_init(root_id: AccessNodeId) -> accesskit::TreeUpdate {
-    let root_id = access_id_to_kit(root_id);
-    let mut classes = accesskit::NodeClassSet::new();
-    let root = accesskit::NodeBuilder::new(accesskit::Role::Application).build(&mut classes);
-
-    accesskit::TreeUpdate {
-        nodes: vec![(root_id, root)],
-        tree: Some(accesskit::Tree::new(root_id)),
-        focus: root_id,
-    }
-}
-
 pub(crate) fn access_tree_update_to_kit(update: zng_view_api::access::AccessTreeUpdate) -> accesskit::TreeUpdate {
     let mut classes = accesskit::NodeClassSet::new();
     let mut nodes = Vec::with_capacity(update.updates.iter().map(|t| t.len()).sum());
@@ -1257,6 +1245,9 @@ fn access_node_to_kit(
         builder.set_bounds(bounds);
     }
 
+    // action that will be performed on click.
+    let mut default_verb = None;
+
     // add actions
     for cmd in &node.commands {
         use zng_view_api::access::AccessCmdName::*;
@@ -1265,11 +1256,14 @@ fn access_node_to_kit(
             Click => {
                 builder.add_action(accesskit::Action::Default);
                 builder.add_action(accesskit::Action::ShowContextMenu);
-                builder.set_default_action_verb(accesskit::DefaultActionVerb::Click);
+                default_verb = Some(accesskit::DefaultActionVerb::Click);
             }
             Focus => {
                 builder.add_action(accesskit::Action::Focus);
                 builder.add_action(accesskit::Action::Blur);
+                if default_verb.is_none() {
+                    default_verb = Some(accesskit::DefaultActionVerb::Focus);
+                }
             }
             SetExpanded => {
                 builder.add_action(accesskit::Action::Expand);
@@ -1308,6 +1302,10 @@ fn access_node_to_kit(
         }
     }
 
+    if let Some(v) = default_verb {
+        builder.set_default_action_verb(v);
+    }
+
     // add state
     for state in &node.state {
         use zng_view_api::access::{self, AccessState::*};
diff --git a/crates/zng-view/src/window.rs b/crates/zng-view/src/window.rs
index 9ec0a3360..603a6c7ec 100644
--- a/crates/zng-view/src/window.rs
+++ b/crates/zng-view/src/window.rs
@@ -346,17 +346,17 @@ impl Window {
 
         drop(wr_scope);
 
-        let access_root = cfg.access_root;
-        let mut first = Some((event_sender.clone(), id));
-        let access = accesskit_winit::Adapter::with_action_handler(
+        let access = accesskit_winit::Adapter::with_direct_handlers(
             &winit_window,
-            move || {
-                if let Some((first, window_id)) = first.take() {
-                    let _ = first.send(AppEvent::Notify(Event::AccessInit { window: window_id }));
-                }
-                crate::util::access_tree_init(access_root)
+            AccessActivateHandler {
+                id,
+                event_sender: event_sender.clone(),
             },
-            Box::new(AccessSender { id, event_sender }),
+            AccessActionSender {
+                id,
+                event_sender: event_sender.clone(),
+            },
+            AccessDeactivateHandler { id, event_sender },
         );
 
         let mut win = Self {
@@ -1654,18 +1654,18 @@ impl Window {
     }
 
     /// Pump the accessibility adapter.
-    pub fn pump_access(&self, event: &winit::event::WindowEvent) {
-        if let Some(a) = &self.access {
+    pub fn pump_access(&mut self, event: &winit::event::WindowEvent) {
+        if let Some(a) = &mut self.access {
             a.process_event(&self.window, event);
         }
     }
 
     /// Update the accessibility info.
     pub fn access_update(&mut self, update: zng_view_api::access::AccessTreeUpdate, event_sender: &AppEventSender) {
-        if let Some(a) = &self.access {
+        if let Some(a) = &mut self.access {
             // SAFETY: we drop `access` in case of panic.
-            let a = std::panic::AssertUnwindSafe(a);
-            let panic = crate::util::catch_suppress(|| {
+            let mut a = std::panic::AssertUnwindSafe(a);
+            let panic = crate::util::catch_suppress(move || {
                 a.update_if_active(|| crate::util::access_tree_update_to_kit(update));
             });
             if let Err(p) = panic {
@@ -1734,11 +1734,33 @@ pub(crate) struct FrameReadyResult {
     pub first_frame: bool,
 }
 
-struct AccessSender {
+struct AccessActivateHandler {
+    id: WindowId,
+    event_sender: AppEventSender,
+}
+impl accesskit::ActivationHandler for AccessActivateHandler {
+    fn request_initial_tree(&mut self) -> Option<accesskit::TreeUpdate> {
+        let _ = self.event_sender.send(AppEvent::Notify(Event::AccessInit { window: self.id }));
+        None
+    }
+}
+
+struct AccessDeactivateHandler {
+    id: WindowId,
+    event_sender: AppEventSender,
+}
+
+impl accesskit::DeactivationHandler for AccessDeactivateHandler {
+    fn deactivate_accessibility(&mut self) {
+        let _ = self.event_sender.send(AppEvent::Notify(Event::AccessDeinit { window: self.id }));
+    }
+}
+
+struct AccessActionSender {
     id: WindowId,
     event_sender: AppEventSender,
 }
-impl accesskit::ActionHandler for AccessSender {
+impl accesskit::ActionHandler for AccessActionSender {
     fn do_action(&mut self, request: accesskit::ActionRequest) {
         if let Some(ev) = crate::util::accesskit_to_event(self.id, request) {
             let _ = self.event_sender.send(AppEvent::Notify(ev));


Test patch: