Announce text attributes in VoiceOver.

It is essential for users of VoiceOver on the Mac platform to have
access to stylistic information for all the text on a web page. This
change exposes font size, foreground color, background color, bold,
italic, underline, and strikethrough text attributes to AT users. "When
 text attributes change:" needs to be set to "Speak Attributes" in
VoiceOver Utility to observe this change.

A later change will expose the remaining text attributes.

bold, italic, underline and strikethrough in VoiceOver.

Bug: 958811
Change-Id: I4f291fdd27466a71d2e65dff8add64475bd820f8
AX-Relnotes: Announce font size, foreground color, background color,
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4313423
Reviewed-by: David Tseng <[email protected]>
Commit-Queue: Sara Tang <[email protected]>
Reviewed-by: Avi Drissman <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1119570}
diff --git a/ui/accessibility/ax_position.h b/ui/accessibility/ax_position.h
index 998e63d..af0a50ab7 100644
--- a/ui/accessibility/ax_position.h
+++ b/ui/accessibility/ax_position.h
@@ -4407,6 +4407,22 @@
     return GetAnchor()->GetRole();
   }
 
+  AXTextAttributes GetTextAttributes() const {
+    // Check either the current anchor or its parent for text attributes.
+    AXTextAttributes current_anchor_text_attributes =
+        !IsNullPosition() ? GetAnchor()->GetTextAttributes()
+                          : AXTextAttributes();
+    if (current_anchor_text_attributes.IsUnset()) {
+      AXPositionInstance parent_position =
+          AsTreePosition()->CreateParentPosition(
+              ax::mojom::MoveDirection::kBackward);
+      if (!parent_position->IsNullPosition()) {
+        return parent_position->GetAnchor()->GetTextAttributes();
+      }
+    }
+    return current_anchor_text_attributes;
+  }
+
  protected:
   AXPosition()
       : kind_(AXPositionKind::NULL_POSITION),
@@ -4683,21 +4699,6 @@
 
   ax::mojom::Role GetRole(AXNode* node) const { return node->GetRole(); }
 
-  AXTextAttributes GetTextAttributes() const {
-    // Check either the current anchor or its parent for text attributes.
-    AXTextAttributes current_anchor_text_attributes =
-        !IsNullPosition() ? GetAnchor()->GetTextAttributes()
-                          : AXTextAttributes();
-    if (current_anchor_text_attributes.IsUnset()) {
-      AXPositionInstance parent_position =
-          AsTreePosition()->CreateParentPosition(
-              ax::mojom::MoveDirection::kBackward);
-      if (!parent_position->IsNullPosition())
-        return parent_position->GetAnchor()->GetTextAttributes();
-    }
-    return current_anchor_text_attributes;
-  }
-
   const std::vector<int32_t>& GetWordStartOffsets() const {
     if (IsNullPosition()) {
       static const base::NoDestructor<std::vector<int32_t>> empty_word_starts;
diff --git a/ui/accessibility/ax_text_attributes.cc b/ui/accessibility/ax_text_attributes.cc
index ce95eeca..545a978 100644
--- a/ui/accessibility/ax_text_attributes.cc
+++ b/ui/accessibility/ax_text_attributes.cc
@@ -75,4 +75,11 @@
          marker_types.size() == 0 && highlight_types.size() == 0;
 }
 
+bool AXTextAttributes::HasTextStyle(
+    ax::mojom::TextStyle text_style_enum) const {
+  return text_style != kUnsetValue &&
+         (static_cast<uint32_t>(text_style) &
+          (1U << static_cast<uint32_t>(text_style_enum))) != 0;
+}
+
 }  // namespace ui
diff --git a/ui/accessibility/ax_text_attributes.h b/ui/accessibility/ax_text_attributes.h
index c298023..4c05e5a1e 100644
--- a/ui/accessibility/ax_text_attributes.h
+++ b/ui/accessibility/ax_text_attributes.h
@@ -9,6 +9,7 @@
 #include <vector>
 
 #include "ui/accessibility/ax_base_export.h"
+#include "ui/accessibility/ax_enums.mojom-shared.h"
 
 namespace ui {
 
@@ -36,6 +37,8 @@
 
   bool IsUnset() const;
 
+  bool HasTextStyle(const ax::mojom::TextStyle text_style_enum) const;
+
   int32_t background_color = kUnsetValue;
   int32_t color = kUnsetValue;
   int32_t invalid_state = kUnsetValue;
diff --git a/ui/accessibility/platform/DEPS b/ui/accessibility/platform/DEPS
index 301ade0..eb35f397 100644
--- a/ui/accessibility/platform/DEPS
+++ b/ui/accessibility/platform/DEPS
@@ -2,6 +2,9 @@
   "atk_util_auralinux_x11\.cc": [
     "+ui/events/x",
   ],
+  "ax_platform_node_cocoa\.mm": [
+    "+skia/ext",
+  ],
   "ax_platform_node_win\.cc": [
     "+skia/ext",
   ]
diff --git a/ui/accessibility/platform/ax_platform_node_cocoa.mm b/ui/accessibility/platform/ax_platform_node_cocoa.mm
index 4d43b44..bfe3733 100644
--- a/ui/accessibility/platform/ax_platform_node_cocoa.mm
+++ b/ui/accessibility/platform/ax_platform_node_cocoa.mm
@@ -12,6 +12,7 @@
 #include "base/no_destructor.h"
 #include "base/strings/sys_string_conversions.h"
 #include "base/trace_event/trace_event.h"
+#include "skia/ext/skia_utils_mac.h"
 #include "ui/accessibility/ax_action_data.h"
 #include "ui/accessibility/ax_enums.mojom.h"
 #include "ui/accessibility/ax_range.h"
@@ -807,6 +808,89 @@
                                range:leafRange];
     }
 
+    ui::AXTextAttributes text_attrs =
+        leafTextRange.anchor()->GetTextAttributes();
+
+    NSMutableDictionary* fontAttributes = [NSMutableDictionary dictionary];
+
+    // TODO(crbug.com/958811): Implement NSAccessibilityFontFamilyKey.
+    // TODO(crbug.com/958811): Implement NSAccessibilityFontNameKey.
+    // TODO(crbug.com/958811): Implement NSAccessibilityVisibleNameKey.
+
+    if (text_attrs.font_size != ui::AXTextAttributes::kUnsetValue) {
+      [fontAttributes setValue:@(text_attrs.font_size)
+                        forKey:NSAccessibilityFontSizeKey];
+    }
+
+    if (text_attrs.HasTextStyle(ax::mojom::TextStyle::kBold)) {
+      [fontAttributes setValue:@YES forKey:@"AXFontBold"];
+    }
+
+    if (text_attrs.HasTextStyle(ax::mojom::TextStyle::kItalic)) {
+      [fontAttributes setValue:@YES forKey:@"AXFontItalic"];
+    }
+
+    [attributedString addAttribute:NSAccessibilityFontTextAttribute
+                             value:fontAttributes
+                             range:leafRange];
+
+    if (text_attrs.color != ui::AXTextAttributes::kUnsetValue) {
+      [attributedString addAttribute:NSAccessibilityForegroundColorTextAttribute
+                               value:(__bridge id)skia::SkColorToSRGBNSColor(
+                                         SkColor(text_attrs.color))
+                                         .CGColor
+                               range:leafRange];
+    } else {
+      [attributedString
+          removeAttribute:NSAccessibilityForegroundColorTextAttribute
+                    range:leafRange];
+    }
+
+    if (text_attrs.background_color != ui::AXTextAttributes::kUnsetValue) {
+      [attributedString addAttribute:NSAccessibilityBackgroundColorTextAttribute
+                               value:(__bridge id)skia::SkColorToSRGBNSColor(
+                                         SkColor(text_attrs.background_color))
+                                         .CGColor
+                               range:leafRange];
+    } else {
+      [attributedString
+          removeAttribute:NSAccessibilityBackgroundColorTextAttribute
+                    range:leafRange];
+    }
+
+    // TODO(crbug.com/958811): Implement
+    // NSAccessibilitySuperscriptTextAttribute.
+    // TODO(crbug.com/958811): Implement NSAccessibilityShadowTextAttribute.
+
+    if (text_attrs.underline_style != ui::AXTextAttributes::kUnsetValue) {
+      [attributedString addAttribute:NSAccessibilityUnderlineTextAttribute
+                               value:@YES
+                               range:leafRange];
+    } else {
+      [attributedString removeAttribute:NSAccessibilityUnderlineTextAttribute
+                                  range:leafRange];
+    }
+
+    // TODO(crbug.com/958811): Implement
+    // NSAccessibilityUnderlineColorTextAttribute.
+
+    if (text_attrs.strikethrough_style != ui::AXTextAttributes::kUnsetValue) {
+      [attributedString addAttribute:NSAccessibilityStrikethroughTextAttribute
+                               value:@YES
+                               range:leafRange];
+    } else {
+      [attributedString
+          removeAttribute:NSAccessibilityStrikethroughTextAttribute
+                    range:leafRange];
+    }
+
+    // TODO(crbug.com/958811): Implement
+    // NSAccessibilityStrikethroughColorTextAttribute.
+
+    // TODO(crbug.com/958811): Implement NSAccessibilityLinkTextAttribute.
+    // TODO(crbug.com/958811): Implement
+    // NSAccessibilityAutocorrectedTextAttribute.
+
     anchorStartOffset += leafTextLength;
   }
   [attributedString endEditing];
diff --git a/ui/accessibility/platform/inspect/ax_transform_mac.h b/ui/accessibility/platform/inspect/ax_transform_mac.h
index c7b961a..ff9b3d93 100644
--- a/ui/accessibility/platform/inspect/ax_transform_mac.h
+++ b/ui/accessibility/platform/inspect/ax_transform_mac.h
@@ -37,6 +37,16 @@
 // Returns the base::Value representation of the given AXTextMarkerRange.
 base::Value AXTextMarkerRangeToBaseValue(id, const AXTreeIndexerMac*);
 
+// Returns the base::Value::Dict representation of the given NSAttributedString.
+COMPONENT_EXPORT(AX_PLATFORM)
+base::Value NSAttributedStringToBaseValue(NSAttributedString*,
+                                          const AXTreeIndexerMac*);
+
+// Returns the base::Value representation of CGColorRef in the form CGColor(r,
+// g, b, a).
+COMPONENT_EXPORT(AX_PLATFORM)
+base::Value CGColorRefToBaseValue(CGColorRef color);
+
 // Returns the base::Value representation of nil.
 COMPONENT_EXPORT(AX_PLATFORM) base::Value AXNilToBaseValue();
 
diff --git a/ui/accessibility/platform/inspect/ax_transform_mac.mm b/ui/accessibility/platform/inspect/ax_transform_mac.mm
index 2b4c110a..7bcf2ea 100644
--- a/ui/accessibility/platform/inspect/ax_transform_mac.mm
+++ b/ui/accessibility/platform/inspect/ax_transform_mac.mm
@@ -62,6 +62,16 @@
     }
   }
 
+  // NSAttributedString
+  if ([value isKindOfClass:[NSAttributedString class]]) {
+    return NSAttributedStringToBaseValue((NSAttributedString*)value, indexer);
+  }
+
+  // CGColorRef
+  if (CFGetTypeID(value) == CGColorGetTypeID()) {
+    return base::Value(CGColorRefToBaseValue(static_cast<CGColorRef>(value)));
+  }
+
   // AXValue
   if (CFGetTypeID(value) == AXValueGetTypeID()) {
     AXValueType type = AXValueGetType(static_cast<AXValueRef>(value));
@@ -181,6 +191,40 @@
   return base::Value(std::move(value));
 }
 
+base::Value NSAttributedStringToBaseValue(NSAttributedString* attr_string,
+                                          const AXTreeIndexerMac* indexer) {
+  __block base::Value::Dict result;
+
+  [attr_string
+      enumerateAttributesInRange:NSMakeRange(0, [attr_string length])
+                         options:
+                             NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
+                      usingBlock:^(NSDictionary* attrs, NSRange nsRange,
+                                   BOOL* stop) {
+                        __block base::Value::Dict base_attrs;
+                        [attrs enumerateKeysAndObjectsUsingBlock:^(
+                                   NSString* key, id attr, BOOL* dict_stop) {
+                          base_attrs.Set(
+                              std::string(base::SysNSStringToUTF8(key)),
+                              AXNSObjectToBaseValue(attr, indexer));
+                        }];
+
+                        result.Set(std::string(base::SysNSStringToUTF8(
+                                       [[attr_string string]
+                                           substringWithRange:nsRange])),
+                                   std::move(base_attrs));
+                      }];
+  return base::Value(std::move(result));
+}
+
+base::Value CGColorRefToBaseValue(CGColorRef color) {
+  const CGFloat* color_components = CGColorGetComponents(color);
+  return base::Value(base::SysNSStringToUTF16(
+      [NSString stringWithFormat:@"CGColor(%1.2f, %1.2f, %1.2f, %1.2f)",
+                                 color_components[0], color_components[1],
+                                 color_components[2], color_components[3]]));
+}
+
 base::Value AXNilToBaseValue() {
   return base::Value(kNilValue);
 }