From ad1aa9d7479274a27fe819ce21324cfb05777419 Mon Sep 17 00:00:00 2001 From: Arnold Loubriat Date: Fri, 28 Aug 2026 12:08:57 +0200 Subject: [PATCH] fix: Support live updates for non-label nodes on macOS and iOS --- adapters/ios/src/event.rs | 12 ++++++------ adapters/ios/src/node.rs | 31 ++++++++++++++++++++++++++----- adapters/macos/src/event.rs | 33 +++++++++++++++++++-------------- adapters/macos/src/node.rs | 8 ++++++++ 4 files changed, 59 insertions(+), 25 deletions(-) diff --git a/adapters/ios/src/event.rs b/adapters/ios/src/event.rs index 3423d48e5..171ef470d 100644 --- a/adapters/ios/src/event.rs +++ b/adapters/ios/src/event.rs @@ -20,7 +20,7 @@ use std::rc::Rc; use crate::{ context::Context, filters::filter, - node::PlatformNode, + node::{NodeWrapper, PlatformNode}, util::{announcement_priority_high, announcement_priority_key, announcement_priority_low}, }; @@ -210,10 +210,10 @@ impl TreeChangeHandler for EventGenerator { return; } self.insert_layout_changed_event_if_needed(); - if let Some(value) = node.value() { + if let Some(label) = NodeWrapper(node).label() { if node.live() != Live::Off { self.events - .push(QueuedEvent::live_region_announcement(value, node.live())); + .push(QueuedEvent::live_region_announcement(label, node.live())); } } } @@ -238,14 +238,14 @@ impl TreeChangeHandler for EventGenerator { } let was_filtered_out = old_filter_result != FilterResult::Include; - if let Some(value) = new_node.value() { + if let Some(label) = NodeWrapper(new_node).label() { if new_node.live() != Live::Off - && (Some(&value) != old_node.value().as_ref() + && (Some(&label) != NodeWrapper(old_node).label().as_ref() || new_node.live() != old_node.live() || was_filtered_out) { self.events.push(QueuedEvent::live_region_announcement( - value, + label, new_node.live(), )); } diff --git a/adapters/ios/src/node.rs b/adapters/ios/src/node.rs index 902f39079..2764606de 100644 --- a/adapters/ios/src/node.rs +++ b/adapters/ios/src/node.rs @@ -61,8 +61,12 @@ enum FrameSource { pub(crate) struct NodeWrapper<'a>(pub(crate) &'a NodeRef<'a>); impl NodeWrapper<'_> { - fn label(&self) -> Option { - self.0.label() + pub(crate) fn label(&self) -> Option { + if self.0.label_comes_from_value() { + self.0.value() + } else { + self.0.label() + } } fn hint(&self) -> Option { @@ -73,6 +77,9 @@ impl NodeWrapper<'_> { if let Some(toggled) = self.0.toggled() { return Some(Value::Bool(toggled != Toggled::False)); } + if self.0.label_comes_from_value() { + return None; + } if let Some(value) = self.0.value() { return Some(Value::String(value)); } @@ -568,12 +575,19 @@ mod tests { // ---- label ---- #[test] - fn label_present() { + fn label_when_it_comes_from_label() { let mut node = Node::new(Role::Button); node.set_label("OK"); assert_eq!(wrapper_label(&node), Some("OK".into())); } + #[test] + fn label_when_it_comes_from_value() { + let mut node = Node::new(Role::Label); + node.set_value("Hello"); + assert_eq!(wrapper_label(&node), Some("Hello".into())); + } + #[test] fn label_absent() { let node = Node::new(Role::Button); @@ -620,11 +634,18 @@ mod tests { #[test] fn value_text_string() { - let mut node = Node::new(Role::Label); + let mut node = Node::new(Role::TextInput); node.set_value("hello"); assert_eq!(wrapper_value(&node), Some(Value::String("hello".into()))); } + #[test] + fn value_is_none_when_value_is_for_label() { + let mut node = Node::new(Role::Label); + node.set_value("hello"); + assert!(wrapper_value(&node).is_none()); + } + #[test] fn value_numeric() { let mut node = Node::new(Role::Slider); @@ -643,7 +664,7 @@ mod tests { #[test] fn value_string_over_numeric() { - let mut node = Node::new(Role::Label); + let mut node = Node::new(Role::ProgressIndicator); node.set_value("text"); node.set_numeric_value(1.0); assert_eq!(wrapper_value(&node), Some(Value::String("text".into()))); diff --git a/adapters/macos/src/event.rs b/adapters/macos/src/event.rs index fa0bddcc9..445419cb3 100644 --- a/adapters/macos/src/event.rs +++ b/adapters/macos/src/event.rs @@ -32,10 +32,10 @@ pub(crate) enum QueuedEvent { } impl QueuedEvent { - fn live_region_announcement(node: &NodeRef) -> Self { + fn live_region_announcement(text: String, live: Live) -> Self { Self::Announcement { - text: node.value().unwrap(), - priority: if node.live() == Live::Assertive { + text, + priority: if live == Live::Assertive { NSAccessibilityPriorityLevel::NSAccessibilityPriorityHigh } else { NSAccessibilityPriorityLevel::NSAccessibilityPriorityMedium @@ -233,9 +233,11 @@ impl TreeChangeHandler for EventGenerator { if let Some(true) = node.is_selected() { self.enqueue_selected_rows_change_if_needed(node); } - if node.value().is_some() && node.live() != Live::Off { - self.events - .push(QueuedEvent::live_region_announcement(node)); + if let Some(label) = NodeWrapper(node).label() { + if node.live() != Live::Off { + self.events + .push(QueuedEvent::live_region_announcement(label, node.live())); + } } } @@ -295,14 +297,17 @@ impl TreeChangeHandler for EventGenerator { notification: unsafe { NSAccessibilitySelectedTextChangedNotification }, }); } - if new_node.value().is_some() - && new_node.live() != Live::Off - && (new_node.value() != old_node.value() - || new_node.live() != old_node.live() - || old_filter_result != FilterResult::Include) - { - self.events - .push(QueuedEvent::live_region_announcement(new_node)); + if let Some(new_label) = new_wrapper.label() { + if new_node.live() != Live::Off + && (Some(&new_label) != old_wrapper.label().as_ref() + || new_node.live() != old_node.live() + || old_filter_result != FilterResult::Include) + { + self.events.push(QueuedEvent::live_region_announcement( + new_label, + new_node.live(), + )); + } } if new_node.is_selected() != old_node.is_selected() || (old_filter_result != FilterResult::Include && new_node.is_selected() == Some(true)) diff --git a/adapters/macos/src/node.rs b/adapters/macos/src/node.rs index bedcedbe1..7be2e59b6 100644 --- a/adapters/macos/src/node.rs +++ b/adapters/macos/src/node.rs @@ -325,6 +325,14 @@ impl NodeWrapper<'_> { self.0.label() } + pub(crate) fn label(&self) -> Option { + if self.0.label_comes_from_value() { + self.0.value() + } else { + self.0.label() + } + } + pub(crate) fn description(&self) -> Option { self.0.description() }