From e6e2bc64bc067aaf4e88b00817cfa72786660fdd Mon Sep 17 00:00:00 2001 From: William Selna Date: Thu, 15 Oct 2026 23:52:59 +0200 Subject: [PATCH 1/2] feat(macos): expose the adapter's activation state `Adapter::update_if_active` does the right thing in every activation phase, but it cannot tell the host which phase that was. A host whose `ActivationHandler::request_initial_tree` returns `None` has to mirror the adapter's state machine in its own handler to know when the one full tree that `update_if_active` then requires is due, and a host that prepares its tree updates eagerly has no way to tell that nobody will consume them yet. `Adapter::activation_state`, also surfaced on `SubclassingAdapter`, returns `Inactive`, `Placeholder` or `Active`. It is a read-only probe; the state machine itself is unchanged. Co-Authored-By: Claude Fable 5.1 --- adapters/macos/src/adapter.rs | 33 +++++++++++++++++++++++++++++++++ adapters/macos/src/lib.rs | 2 +- adapters/macos/src/subclass.rs | 10 +++++++++- 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/adapters/macos/src/adapter.rs b/adapters/macos/src/adapter.rs index 86b4b0073..90035c0c9 100644 --- a/adapters/macos/src/adapter.rs +++ b/adapters/macos/src/adapter.rs @@ -23,6 +23,23 @@ use std::{ffi::c_void, ptr::null_mut, rc::Rc}; const PLACEHOLDER_ROOT_ID: NodeId = NodeId(0); +/// Which phase of activation the adapter is in; see +/// [`Adapter::activation_state`]. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ActivationState { + /// No assistive technology has activated the adapter yet. The factory + /// passed to [`Adapter::update_if_active`] is never called in this phase, + /// so a host that prepares its updates eagerly can skip that work. + Inactive, + /// An assistive technology activated the adapter while + /// [`ActivationHandler::request_initial_tree`] returned `None`. The + /// assistive technology currently sees a placeholder tree, and the next + /// update passed to [`Adapter::update_if_active`] must be a full tree. + Placeholder, + /// The tree is live; updates are applied incrementally. + Active, +} + enum State { Inactive { view: WeakId, @@ -144,6 +161,22 @@ impl Adapter { } } + /// The phase of activation the adapter is in. + /// + /// This is a read-only probe; it never changes the state. A host whose + /// [`ActivationHandler::request_initial_tree`] returns `None` otherwise + /// has to mirror the adapter's state machine itself to know when the + /// one full tree that [`Adapter::update_if_active`] then requires is due, + /// and a host that prepares its updates eagerly has no other way to tell + /// that nobody will consume them yet. + pub fn activation_state(&self) -> ActivationState { + match &self.state { + State::Inactive { .. } => ActivationState::Inactive, + State::Placeholder { .. } => ActivationState::Placeholder, + State::Active(_) => ActivationState::Active, + } + } + /// Update the tree state based on whether the window is focused. /// /// If a [`QueuedEvents`] instance is returned, the caller must call diff --git a/adapters/macos/src/lib.rs b/adapters/macos/src/lib.rs index 5b795424e..9810ab34a 100644 --- a/adapters/macos/src/lib.rs +++ b/adapters/macos/src/lib.rs @@ -11,7 +11,7 @@ mod node; mod util; mod adapter; -pub use adapter::Adapter; +pub use adapter::{ActivationState, Adapter}; mod event; pub use event::QueuedEvents; diff --git a/adapters/macos/src/subclass.rs b/adapters/macos/src/subclass.rs index a873f7c85..af3d6b6d9 100644 --- a/adapters/macos/src/subclass.rs +++ b/adapters/macos/src/subclass.rs @@ -22,7 +22,7 @@ use objc2_app_kit::{NSView, NSWindow}; use objc2_foundation::{NSArray, NSObject, NSPoint}; use std::{cell::RefCell, ffi::c_void, sync::Mutex}; -use crate::{Adapter, event::QueuedEvents}; +use crate::{ActivationState, Adapter, event::QueuedEvents}; static SUBCLASSES: Mutex> = Mutex::new(Vec::new()); @@ -254,6 +254,14 @@ impl SubclassingAdapter { let mut state = self.associated.ivars().state.borrow_mut(); state.adapter.update_view_focus_state(is_focused) } + + /// The phase of activation the wrapped adapter is in; see + /// [`Adapter::activation_state`]. The internal borrow taken here is + /// released before this method returns. + pub fn activation_state(&self) -> ActivationState { + let state = self.associated.ivars().state.borrow(); + state.adapter.activation_state() + } } impl Drop for SubclassingAdapter { From c7d7acc026afa5ad1bcc55ec7f88b320840fa110 Mon Sep 17 00:00:00 2001 From: William Selna Date: Fri, 16 Oct 2026 00:13:46 +0200 Subject: [PATCH 2/2] chore(macos): Trim the activation state docs Co-Authored-By: Claude Fable 5.1 --- adapters/macos/src/adapter.rs | 27 +++++++++------------------ adapters/macos/src/subclass.rs | 4 +--- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/adapters/macos/src/adapter.rs b/adapters/macos/src/adapter.rs index 90035c0c9..ec769a7e1 100644 --- a/adapters/macos/src/adapter.rs +++ b/adapters/macos/src/adapter.rs @@ -23,20 +23,16 @@ use std::{ffi::c_void, ptr::null_mut, rc::Rc}; const PLACEHOLDER_ROOT_ID: NodeId = NodeId(0); -/// Which phase of activation the adapter is in; see -/// [`Adapter::activation_state`]. +/// See [`Adapter::activation_state`]. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ActivationState { - /// No assistive technology has activated the adapter yet. The factory - /// passed to [`Adapter::update_if_active`] is never called in this phase, - /// so a host that prepares its updates eagerly can skip that work. + /// No assistive technology has activated the adapter yet. Inactive, - /// An assistive technology activated the adapter while - /// [`ActivationHandler::request_initial_tree`] returned `None`. The - /// assistive technology currently sees a placeholder tree, and the next - /// update passed to [`Adapter::update_if_active`] must be a full tree. + /// Activated while [`ActivationHandler::request_initial_tree`] returned + /// `None`; the next update passed to [`Adapter::update_if_active`] must + /// contain a full tree. Placeholder, - /// The tree is live; updates are applied incrementally. + /// The tree is live. Active, } @@ -161,14 +157,9 @@ impl Adapter { } } - /// The phase of activation the adapter is in. - /// - /// This is a read-only probe; it never changes the state. A host whose - /// [`ActivationHandler::request_initial_tree`] returns `None` otherwise - /// has to mirror the adapter's state machine itself to know when the - /// one full tree that [`Adapter::update_if_active`] then requires is due, - /// and a host that prepares its updates eagerly has no other way to tell - /// that nobody will consume them yet. + /// The adapter's current activation state. This lets a host whose + /// [`ActivationHandler::request_initial_tree`] returns `None` know when + /// the full tree is due without tracking activation itself. pub fn activation_state(&self) -> ActivationState { match &self.state { State::Inactive { .. } => ActivationState::Inactive, diff --git a/adapters/macos/src/subclass.rs b/adapters/macos/src/subclass.rs index af3d6b6d9..a77ca18ca 100644 --- a/adapters/macos/src/subclass.rs +++ b/adapters/macos/src/subclass.rs @@ -255,9 +255,7 @@ impl SubclassingAdapter { state.adapter.update_view_focus_state(is_focused) } - /// The phase of activation the wrapped adapter is in; see - /// [`Adapter::activation_state`]. The internal borrow taken here is - /// released before this method returns. + /// See [`Adapter::activation_state`]. pub fn activation_state(&self) -> ActivationState { let state = self.associated.ivars().state.borrow(); state.adapter.activation_state()