Skip to content

Commit 641b05b

Browse files
feat: Expose text attributes on macOS (#691)
Co-authored-by: Arnold Loubriat <datatriny@gmail.com>
1 parent da70097 commit 641b05b

4 files changed

Lines changed: 183 additions & 14 deletions

File tree

consumer/src/text.rs

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -677,14 +677,11 @@ impl<'a> Range<'a> {
677677
None
678678
}
679679

680-
pub fn text(&self) -> String {
681-
let mut result = String::new();
682-
self.write_text(&mut result).unwrap();
683-
result
684-
}
685-
686-
pub fn write_text<W: fmt::Write>(&self, mut writer: W) -> fmt::Result {
687-
if let Some(err) = self.walk(|node| {
680+
pub fn traverse_text<F, T>(&self, mut f: F) -> Option<T>
681+
where
682+
F: FnMut(&Node<'a>, &str) -> Option<T>,
683+
{
684+
self.walk(|node| {
688685
let character_lengths = node.data().character_lengths();
689686
let start_index = if node.id() == self.start.node.id() {
690687
self.start.character_index
@@ -715,14 +712,24 @@ impl<'a> Range<'a> {
715712
.sum::<usize>();
716713
&value[slice_start..slice_end]
717714
};
718-
writer.write_str(s).err()
719-
}) {
715+
f(node, s)
716+
})
717+
}
718+
719+
pub fn write_text<W: fmt::Write>(&self, mut writer: W) -> fmt::Result {
720+
if let Some(err) = self.traverse_text(|_, s| writer.write_str(s).err()) {
720721
Err(err)
721722
} else {
722723
Ok(())
723724
}
724725
}
725726

727+
pub fn text(&self) -> String {
728+
let mut result = String::new();
729+
self.write_text(&mut result).unwrap();
730+
result
731+
}
732+
726733
/// Returns the range's transformed bounding boxes relative to the tree's
727734
/// container (e.g. window).
728735
///

platforms/macos/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ objc2-app-kit = { version = "0.2.0", features = [
3030
"NSAccessibilityConstants",
3131
"NSAccessibilityElement",
3232
"NSAccessibilityProtocols",
33+
"NSColor",
3334
"NSResponder",
35+
"NSText",
3436
"NSView",
3537
"NSWindow",
3638
] }

platforms/macos/src/node.rs

Lines changed: 134 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010

1111
#![allow(non_upper_case_globals)]
1212

13-
use accesskit::{Action, ActionData, ActionRequest, Orientation, Role, TextSelection, Toggled};
13+
use accesskit::{
14+
Action, ActionData, ActionRequest, Orientation, Role, TextAlign, TextSelection, Toggled,
15+
};
1416
use accesskit_consumer::{FilterResult, Node, NodeId, Tree};
1517
use objc2::{
1618
declare_class, msg_send_id,
@@ -21,8 +23,9 @@ use objc2::{
2123
};
2224
use objc2_app_kit::*;
2325
use objc2_foundation::{
24-
ns_string, NSArray, NSCopying, NSInteger, NSNumber, NSObject, NSObjectProtocol, NSPoint,
25-
NSRange, NSRect, NSString, NSURL,
26+
ns_string, NSArray, NSAttributedString, NSCopying, NSInteger, NSMutableAttributedString,
27+
NSMutableDictionary, NSNumber, NSObject, NSObjectProtocol, NSPoint, NSRange, NSRect, NSString,
28+
NSURL,
2629
};
2730
use std::rc::{Rc, Weak};
2831

@@ -823,6 +826,112 @@ declare_class!(
823826
.flatten()
824827
}
825828

829+
#[method_id(accessibilityAttributedStringForRange:)]
830+
fn attributed_string_for_range(&self, range: NSRange) -> Option<Id<NSAttributedString>> {
831+
self.resolve(|node| {
832+
if node.supports_text_ranges() {
833+
if let Some(range) = from_ns_range(node, range) {
834+
let mut result = NSMutableAttributedString::new();
835+
unsafe { result.beginEditing() };
836+
range.traverse_text::<_, ()>(|node, text| {
837+
let ns_text = NSString::from_str(text);
838+
let mut attrs = NSMutableDictionary::new();
839+
if let Some(color) = node.background_color() {
840+
attrs.insert_id(
841+
unsafe { NSAccessibilityBackgroundColorTextAttribute },
842+
to_color_attribute(color)
843+
);
844+
}
845+
if let Some(color) = node.foreground_color() {
846+
attrs.insert_id(
847+
unsafe { NSAccessibilityForegroundColorTextAttribute },
848+
to_color_attribute(color)
849+
);
850+
}
851+
let mut font_attrs = NSMutableDictionary::<NSAccessibilityFontAttributeKey, AnyObject>::new();
852+
if let Some(family) = node.font_family() {
853+
font_attrs.insert_id(
854+
unsafe { NSAccessibilityFontFamilyKey },
855+
Id::into_super(Id::into_super(NSString::from_str(family)))
856+
);
857+
}
858+
if let Some(size) = node.font_size() {
859+
font_attrs.insert_id(
860+
unsafe { NSAccessibilityFontSizeKey },
861+
Id::into_super(Id::into_super(Id::into_super(NSNumber::new_f32(size))))
862+
);
863+
}
864+
if let Some(weight) = node.font_weight() {
865+
if weight >= 700.0 {
866+
font_attrs.insert_id(
867+
ns_string!("AXFontBold"),
868+
Id::into_super(Id::into_super(Id::into_super(NSNumber::new_bool(true))))
869+
);
870+
}
871+
}
872+
if node.is_italic() {
873+
font_attrs.insert_id(
874+
ns_string!("AXFontItalic"),
875+
Id::into_super(Id::into_super(Id::into_super(NSNumber::new_bool(true))))
876+
);
877+
}
878+
if !font_attrs.is_empty() {
879+
attrs.insert_id(
880+
unsafe { NSAccessibilityFontTextAttribute },
881+
Id::into_super(Id::into_super(Id::into_super(font_attrs)))
882+
);
883+
}
884+
if let Some(deco) = node.underline() {
885+
attrs.insert_id(
886+
unsafe { NSAccessibilityUnderlineTextAttribute },
887+
Id::into_super(Id::into_super(Id::into_super(NSNumber::new_bool(true))))
888+
);
889+
attrs.insert_id(
890+
unsafe { NSAccessibilityUnderlineColorTextAttribute },
891+
to_color_attribute(deco.color)
892+
);
893+
}
894+
if let Some(deco) = node.strikethrough() {
895+
attrs.insert_id(
896+
unsafe { NSAccessibilityStrikethroughTextAttribute },
897+
Id::into_super(Id::into_super(Id::into_super(NSNumber::new_bool(true))))
898+
);
899+
attrs.insert_id(
900+
unsafe { NSAccessibilityStrikethroughColorTextAttribute },
901+
to_color_attribute(deco.color)
902+
);
903+
}
904+
if let Some(language) = node.language() {
905+
attrs.insert_id(
906+
unsafe { NSAccessibilityLanguageTextAttribute },
907+
Id::into_super(Id::into_super(NSString::from_str(language)))
908+
);
909+
}
910+
if let Some(align) = node.text_align() {
911+
let ns_align = match align {
912+
TextAlign::Left => NSTextAlignment::Left,
913+
TextAlign::Center => NSTextAlignment::Center,
914+
TextAlign::Right => NSTextAlignment::Right,
915+
TextAlign::Justify => NSTextAlignment::Justified,
916+
};
917+
attrs.insert_id(
918+
unsafe { NSAccessibilityTextAlignmentAttribute },
919+
Id::into_super(Id::into_super(Id::into_super(NSNumber::new_isize(ns_align.0))))
920+
);
921+
}
922+
let part = unsafe { NSAttributedString::new_with_attributes(&ns_text, &attrs) };
923+
unsafe { result.appendAttributedString(&part) };
924+
None
925+
});
926+
unsafe { result.endEditing() };
927+
return Some(Id::into_super(result));
928+
}
929+
}
930+
None
931+
})
932+
.flatten()
933+
}
934+
826935
#[method(accessibilityFrameForRange:)]
827936
fn frame_for_range(&self, range: NSRange) -> NSRect {
828937
self.resolve_with_context(|node, _, context| {
@@ -874,6 +983,26 @@ declare_class!(
874983
.unwrap_or_else(|| NSRange::new(0, 0))
875984
}
876985

986+
#[method(accessibilityStyleRangeForIndex:)]
987+
fn style_range_for_index(&self, index: NSInteger) -> NSRange {
988+
self.resolve(|node| {
989+
if node.supports_text_ranges() && index >= 0 {
990+
if let Some(pos) = node.text_position_from_global_utf16_index(index as _) {
991+
let start = if pos.is_format_start() {
992+
pos
993+
} else {
994+
pos.backward_to_format_start()
995+
};
996+
let mut range = start.to_degenerate_range();
997+
range.set_end(pos.forward_to_format_end());
998+
return to_ns_range(&range);
999+
}
1000+
}
1001+
NSRange::new(0, 0)
1002+
})
1003+
.unwrap_or_else(|| NSRange::new(0, 0))
1004+
}
1005+
8771006
#[method(setAccessibilitySelectedTextRange:)]
8781007
fn set_selected_text_range(&self, range: NSRange) {
8791008
self.resolve_with_context(|node, tree, context| {
@@ -1097,9 +1226,11 @@ declare_class!(
10971226
|| selector == sel!(accessibilityRangeForLine:)
10981227
|| selector == sel!(accessibilityRangeForPosition:)
10991228
|| selector == sel!(accessibilityStringForRange:)
1229+
|| selector == sel!(accessibilityAttributedStringForRange:)
11001230
|| selector == sel!(accessibilityFrameForRange:)
11011231
|| selector == sel!(accessibilityLineForIndex:)
11021232
|| selector == sel!(accessibilityRangeForIndex:)
1233+
|| selector == sel!(accessibilityStyleRangeForIndex:)
11031234
|| selector == sel!(setAccessibilitySelectedTextRange:)
11041235
{
11051236
return node.supports_text_ranges();

platforms/macos/src/util.rs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
// the LICENSE-APACHE file) or the MIT license (found in
44
// the LICENSE-MIT file), at your option.
55

6-
use accesskit::{Point, Rect};
6+
use accesskit::{Color, Point, Rect};
77
use accesskit_consumer::{Node, TextPosition, TextRange};
8+
use objc2::encode::{Encoding, RefEncode};
9+
use objc2::{msg_send, rc::Id, runtime::AnyObject};
810
use objc2_app_kit::*;
911
use objc2_foundation::{NSPoint, NSRange, NSRect, NSSize};
1012

@@ -77,3 +79,30 @@ pub(crate) fn to_ns_rect(view: &NSView, rect: Rect) -> NSRect {
7779
let window = view.window().unwrap();
7880
window.convertRectToScreen(rect)
7981
}
82+
83+
fn color_channel_to_f64(channel: u8) -> f64 {
84+
(channel as f64) / 255.0
85+
}
86+
87+
// TODO: can be removed after updating objc2 to 0.6 which has proper `CGColor` support
88+
#[repr(C)]
89+
struct CGColor {
90+
_private: [u8; 0],
91+
}
92+
93+
unsafe impl RefEncode for CGColor {
94+
const ENCODING_REF: Encoding = Encoding::Pointer(&Encoding::Struct("CGColor", &[]));
95+
}
96+
97+
pub(crate) fn to_color_attribute(color: Color) -> Id<AnyObject> {
98+
let ns_color = unsafe {
99+
NSColor::colorWithSRGBRed_green_blue_alpha(
100+
color_channel_to_f64(color.red),
101+
color_channel_to_f64(color.green),
102+
color_channel_to_f64(color.blue),
103+
color_channel_to_f64(color.alpha),
104+
)
105+
};
106+
let cg_color: *const CGColor = unsafe { msg_send![&ns_color, CGColor] };
107+
unsafe { Id::retain(cg_color as *mut AnyObject).unwrap() }
108+
}

0 commit comments

Comments
 (0)