Skip to content

Commit

Permalink
AX: Serve initial AXRelativeFrame for elements without cached frames …
Browse files Browse the repository at this point in the history
…off of the main thread

https://bugs.webkit.org/show_bug.cgi?id=268035
rdar://120764228

Reviewed by Tyler Wilcock.

We only serve relative frames for objects on the secondary thread once they are cached during
the Accessibility paint phase. This means that when we move to objects that were offscreen or
haven't been painted, we have to use the main thread to serve the relative frame.

This patch instead serves a rough frame from the AX thread initally, until the object's frame
is cached during painting. This rough frame is constructed by using the frame rect of the element
for its size, and it's closest painted ancestor for its position. As soon as the element's
precise frame is cached duiring paint, this is updated.

* LayoutTests/accessibility/mac/initial-relative-frame-cached-expected.txt: Added.
* LayoutTests/accessibility/mac/initial-relative-frame-cached.html: Added.
* LayoutTests/platform/mac-wk1/TestExpectations:
* Source/WebCore/accessibility/AXObjectCache.cpp:
(WebCore::AXObjectCache::setForceInitialFrameCaching):
(WebCore::AXObjectCache::shouldServeInitialCachedFrame):
* Source/WebCore/accessibility/AXObjectCache.h:
(WebCore::AXObjectCache::forceInitialFrameCaching):
* Source/WebCore/accessibility/AccessibilityObject.h:
(WebCore::AccessibilityObject::frameRect const):
(WebCore::AccessibilityObject::isNonLayerSVGObject const):
* Source/WebCore/accessibility/AccessibilityRenderObject.cpp:
(WebCore::AccessibilityRenderObject::isNonLayerSVGObject const):
(WebCore::AccessibilityRenderObject::frameRect const):
* Source/WebCore/accessibility/AccessibilityRenderObject.h:
* Source/WebCore/accessibility/isolatedtree/AXIsolatedObject.cpp:
(WebCore::AXIsolatedObject::initializeProperties):
(WebCore::AXIsolatedObject::relativeFrame const):
* Source/WebCore/accessibility/isolatedtree/AXIsolatedObject.h:
* Source/WebCore/accessibility/isolatedtree/AXIsolatedTree.cpp:
(WebCore::AXIsolatedTree::updateFrame):
* Source/WebCore/accessibility/isolatedtree/AXIsolatedTree.h:
* Source/WebKit/WebProcess/InjectedBundle/API/c/WKBundlePage.cpp:
(WKAccessibilitySetForceInitialFrameCaching):
* Source/WebKit/WebProcess/InjectedBundle/API/c/WKBundlePagePrivate.h:
* Tools/WebKitTestRunner/InjectedBundle/AccessibilityController.cpp:
(WTR::AccessibilityController::setForceInitialFrameCaching):
* Tools/WebKitTestRunner/InjectedBundle/AccessibilityController.h:
* Tools/WebKitTestRunner/InjectedBundle/Bindings/AccessibilityController.idl:

Canonical link: https://commits.webkit.org/274472@main
  • Loading branch information
hoffmanjoshua authored and twilco committed Feb 12, 2024
1 parent 9c97e55 commit e6c6fde
Show file tree
Hide file tree
Showing 17 changed files with 158 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
This test verifies that when initial rough frame caching is enabled, frames are eventually correct.

heading1 has relative frame NSRect: {{8, 278}, {754, 37}}.
image has relative frame NSRect: {{8, 336}, {100, 101}}.
list has relative frame NSRect: {{8, 456}, {754, 37}}.
listitem has relative frame NSRect: {{48, 456}, {714, 19}}.

PASS successfullyParsed is true

TEST COMPLETE
Hello, world.


List item 1
List item 2
60 changes: 60 additions & 0 deletions LayoutTests/accessibility/mac/initial-relative-frame-cached.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<!DOCTYPE html>
<html>
<head>
<script src="../../resources/accessibility-helper.js"></script>
<script src="../../resources/js-test.js"></script>
</head>
<body style="height: 1000px; width=1000px">

<div style="height: 500px; overflow: auto;" id="scrollarea">
<div style="margin-top: 600px;" id="hidden-content">
<h1 id="heading1">Hello, world.</h1>

<img id="image" height=100 width=100 src="../resources/cake.png" alt="Cake in front of a glass door">

<ul id="list">
<li id="listitem">List item 1</li>
<li>List item 2</li>
</ul>
</div>
</div>

<script>
let output = "This test verifies that when initial rough frame caching is enabled, frames are eventually correct.\n\n";

if (window.accessibilityController) {
accessibilityController.setForceInitialFrameCaching(true);
window.jsTestIsAsync = true;

setTimeout(async function() {
// Scroll all content onscreen
let container = document.getElementById("scrollarea");
container.scrollTop = container.scrollHeight;

// Enable accessibility & trigger paint
accessibilityController.rootElement;

// Wait for the objects to be painted
await sleep(1000);

output += frameDebugString("heading1");
output += frameDebugString("image");
output += frameDebugString("list");
output += frameDebugString("listitem");

accessibilityController.setForceInitialFrameCaching(false);

debug(output);
finishJSTest();
}, 0);
}


function frameDebugString(id) {
let element = accessibilityController.accessibleElementById(id);
return `${id} has relative frame ${element.stringDescriptionOfAttributeValue('AXRelativeFrame')}.\n`;
}

</script>
</body>
</html>
3 changes: 3 additions & 0 deletions LayoutTests/platform/mac-wk1/TestExpectations
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,9 @@ accessibility/mac/lazy-spellchecking.html [ Skip ]
accessibility/mac/spellcheck-with-voiceover.html [ Skip ]
accessibility/text-marker/text-marker-range-with-unordered-markers.html

# AccessibilityController::setForceInitialFrameCaching not supported on WK1.
accessibility/mac/initial-relative-frame-cached.html [ Skip ]

# New API not supported in WK1
accessibility/mac/intersection-with-selection-range.html [ Skip ]

Expand Down
11 changes: 11 additions & 0 deletions Source/WebCore/accessibility/AXObjectCache.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ bool AXObjectCache::gForceDeferredSpellChecking = false;
#if ENABLE(AX_THREAD_TEXT_APIS)
bool AXObjectCache::gAccessibilityThreadTextApisEnabled = false;
#endif
bool AXObjectCache::gForceInitialFrameCaching = false;

void AXObjectCache::enableAccessibility()
{
Expand All @@ -234,7 +235,17 @@ void AXObjectCache::setEnhancedUserInterfaceAccessibility(bool flag)
#endif
}

void AXObjectCache::setForceInitialFrameCaching(bool shouldForce)
{
gForceInitialFrameCaching = shouldForce;
}

#if ENABLE(ACCESSIBILITY_ISOLATED_TREE)
bool AXObjectCache::shouldServeInitialCachedFrame()
{
return !isTestClient() || forceInitialFrameCaching();
}

static const Seconds updateTreeSnapshotTimerInterval { 100_ms };
#endif

Expand Down
7 changes: 7 additions & 0 deletions Source/WebCore/accessibility/AXObjectCache.h
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,12 @@ class AXObjectCache final : public CanMakeWeakPtr<AXObjectCache>, public CanMake
static bool useAXThreadTextApis() { return gAccessibilityThreadTextApisEnabled && !isMainThread(); }
#endif

static bool forceInitialFrameCaching() { return gForceInitialFrameCaching; }
WEBCORE_EXPORT static void setForceInitialFrameCaching(bool);
#if ENABLE(ACCESSIBILITY_ISOLATED_TREE)
static bool shouldServeInitialCachedFrame();
#endif

const Element* rootAXEditableElement(const Node*);
bool nodeIsTextControl(const Node*);

Expand Down Expand Up @@ -678,6 +684,7 @@ class AXObjectCache final : public CanMakeWeakPtr<AXObjectCache>, public CanMake
WEBCORE_EXPORT static bool gAccessibilityEnabled;
WEBCORE_EXPORT static bool gAccessibilityEnhancedUserInterfaceEnabled;
static bool gForceDeferredSpellChecking;
static bool gForceInitialFrameCaching;

#if ENABLE(AX_THREAD_TEXT_APIS)
static bool gAccessibilityThreadTextApisEnabled;
Expand Down
3 changes: 3 additions & 0 deletions Source/WebCore/accessibility/AccessibilityObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,9 @@ class AccessibilityObject : public AXCoreObject, public CanMakeWeakPtr<Accessibi
void setLastPresentedTextPrediction(Node&, CompositionState, const String&, size_t, bool);
#endif // PLATFORM(IOS_FAMILY)

virtual FloatRect frameRect() const { return { }; }
virtual bool isNonLayerSVGObject() const { return false; }

protected:
AccessibilityObject() = default;

Expand Down
15 changes: 14 additions & 1 deletion Source/WebCore/accessibility/AccessibilityRenderObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
#include "RenderListMarker.h"
#include "RenderMathMLBlock.h"
#include "RenderMenuList.h"
#include "RenderSVGInlineText.h"
#include "RenderSVGRoot.h"
#include "RenderSVGShape.h"
#include "RenderTableCell.h"
Expand Down Expand Up @@ -862,7 +863,13 @@ LayoutRect AccessibilityRenderObject::boundingBoxRect() const

return result;
}


bool AccessibilityRenderObject::isNonLayerSVGObject() const
{
auto* renderer = this->renderer();
return renderer ? is<RenderSVGInlineText>(renderer) || is<LegacyRenderSVGModelObject>(renderer) : false;
}

bool AccessibilityRenderObject::supportsPath() const
{
return is<RenderText>(renderer()) || (renderer() && renderer()->isRenderOrLegacyRenderSVGShape());
Expand Down Expand Up @@ -2716,6 +2723,12 @@ void AccessibilityRenderObject::scrollTo(const IntPoint& point) const
box->layer()->scrollableArea()->scrollToOffset(point);
}

FloatRect AccessibilityRenderObject::frameRect() const
{
auto* box = dynamicDowncast<RenderBox>(renderer());
return box ? convertFrameToSpace(box->frameRect(), AccessibilityConversionSpace::Page) : FloatRect();
}

#if ENABLE(MATHML)
bool AccessibilityRenderObject::isIgnoredElementWithinMathTree() const
{
Expand Down
3 changes: 3 additions & 0 deletions Source/WebCore/accessibility/AccessibilityRenderObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ class AccessibilityRenderObject : public AccessibilityNodeObject {
static Ref<AccessibilityRenderObject> create(RenderObject*);
virtual ~AccessibilityRenderObject();

FloatRect frameRect() const final;
bool isNonLayerSVGObject() const override;

bool isAttachment() const override;
bool isOffScreen() const override;
bool hasBoldFont() const override;
Expand Down
28 changes: 23 additions & 5 deletions Source/WebCore/accessibility/isolatedtree/AXIsolatedObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ void AXIsolatedObject::initializeProperties(const Ref<AccessibilityObject>& axOb
setProperty(AXPropertyName::IsKeyboardFocusable, object.isKeyboardFocusable());
setProperty(AXPropertyName::BrailleRoleDescription, object.brailleRoleDescription().isolatedCopy());
setProperty(AXPropertyName::BrailleLabel, object.brailleLabel().isolatedCopy());
setProperty(AXPropertyName::IsNonLayerSVGObject, object.isNonLayerSVGObject());

RefPtr geometryManager = tree()->geometryManager();
std::optional frame = geometryManager ? geometryManager->cachedRectForID(object.objectID()) : std::nullopt;
Expand All @@ -183,7 +184,8 @@ void AXIsolatedObject::initializeProperties(const Ref<AccessibilityObject>& axOb
} else if (object.isMenuListPopup()) {
// AccessibilityMenuListPopup's elementRect is hardcoded to return an empty rect, so preserve that behavior.
setProperty(AXPropertyName::RelativeFrame, IntRect());
}
} else
setProperty(AXPropertyName::InitialFrameRect, object.frameRect());

if (object.supportsCheckedState()) {
setProperty(AXPropertyName::SupportsCheckedState, true);
Expand Down Expand Up @@ -1243,11 +1245,27 @@ FloatRect AXIsolatedObject::relativeFrame() const
} else if (roleValue() == AccessibilityRole::Column || roleValue() == AccessibilityRole::TableHeaderContainer)
return exposedTableAncestor() ? relativeFrameFromChildren() : FloatRect();

return Accessibility::retrieveValueFromMainThread<FloatRect>([this] () -> FloatRect {
if (auto* axObject = associatedAXObject())
return axObject->relativeFrame();
return { };
// Mock objects and SVG objects need use the main thread since they do not have render nodes and are not painted with layers, respectively.
// FIXME: Remove isNonLayerSVGObject when LBSE is enabled & SVG frames are cached.
if (!AXObjectCache::shouldServeInitialCachedFrame() || isMockObject() || isNonLayerSVGObject()) {
return Accessibility::retrieveValueFromMainThread<FloatRect>([this] () -> FloatRect {
if (auto* axObject = associatedAXObject())
return axObject->relativeFrame();
return { };
});
}

// InitialFrameRect stores the correct size, but not position, of the element before it is painted.
// We find the position of the nearest painted ancestor to use as the position until the object's frame
// is cached during painting.
auto* ancestor = Accessibility::findAncestor<AXIsolatedObject>(*this, false, [] (const auto& object) {
return object.hasCachedRelativeFrame();
});
auto frameRect = rectAttributeValue<FloatRect>(AXPropertyName::InitialFrameRect);
if (ancestor && frameRect.location() == FloatPoint())
frameRect.setLocation(ancestor->relativeFrame().location());

return frameRect;
}

FloatRect AXIsolatedObject::relativeFrameFromChildren() const
Expand Down
2 changes: 2 additions & 0 deletions Source/WebCore/accessibility/isolatedtree/AXIsolatedObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ class AXIsolatedObject final : public AXCoreObject {
bool isMeter() const final { return boolAttributeValue(AXPropertyName::IsMeter); };
FloatPoint screenRelativePosition() const final;
FloatRect relativeFrame() const final;
bool hasCachedRelativeFrame() const { return optionalAttributeValue<IntRect>(AXPropertyName::RelativeFrame).has_value(); }
#if PLATFORM(MAC)
FloatRect primaryScreenRect() const final;
#endif
Expand Down Expand Up @@ -464,6 +465,7 @@ class AXIsolatedObject final : public AXCoreObject {
bool isOnScreen() const final;
bool isOffScreen() const final;
bool isPressed() const final;
bool isNonLayerSVGObject() const { return boolAttributeValue(AXPropertyName::IsNonLayerSVGObject); }
// FIXME: isVisible should be accurate for all objects, not just widgets, on COCOA.
bool isVisible() const final { return boolAttributeValue(AXPropertyName::IsVisible); }
bool isSelectedOptionActive() const final;
Expand Down
2 changes: 2 additions & 0 deletions Source/WebCore/accessibility/isolatedtree/AXIsolatedTree.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1043,6 +1043,8 @@ void AXIsolatedTree::updateFrame(AXID axID, IntRect&& newFrame)

AXPropertyMap propertyMap;
propertyMap.set(AXPropertyName::RelativeFrame, WTFMove(newFrame));
// We can clear the initially-cached rough frame, since the object's frame has been cached
propertyMap.set(AXPropertyName::InitialFrameRect, FloatRect());
Locker locker { m_changeLogLock };
m_pendingPropertyChanges.append({ axID, WTFMove(propertyMap) });
}
Expand Down
2 changes: 2 additions & 0 deletions Source/WebCore/accessibility/isolatedtree/AXIsolatedTree.h
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ enum class AXPropertyName : uint16_t {
HorizontalScrollBar,
IdentifierAttribute,
IncrementButton,
InitialFrameRect,
InnerHTML,
InternalLinkElement,
InsideLink,
Expand Down Expand Up @@ -151,6 +152,7 @@ enum class AXPropertyName : uint16_t {
IsMathToken,
IsMeter,
IsMultiSelectable,
IsNonLayerSVGObject,
IsNonNativeTextControl,
IsPlugin,
IsPressed,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,11 @@ bool WKAccessibilityEnhancedAccessibilityEnabled()
return WebCore::AXObjectCache::accessibilityEnhancedUserInterfaceEnabled();
}

void WKAccessibilitySetForceInitialFrameCaching(bool shouldForce)
{
WebCore::AXObjectCache::setForceInitialFrameCaching(shouldForce);
}

void WKBundlePageStopLoading(WKBundlePageRef pageRef)
{
WebKit::toImpl(pageRef)->stopLoading();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ WK_EXPORT void WKAccessibilityTestingInjectPreference(WKBundlePageRef, WKStringR
WK_EXPORT void WKAccessibilitySetForceDeferredSpellChecking(bool);
WK_EXPORT void WKAccessibilityEnableEnhancedAccessibility(bool);
WK_EXPORT bool WKAccessibilityEnhancedAccessibilityEnabled();
WK_EXPORT void WKAccessibilitySetForceInitialFrameCaching(bool);

WK_EXPORT void WKBundlePageClickMenuItem(WKBundlePageRef, WKContextMenuItemRef);
WK_EXPORT WKArrayRef WKBundlePageCopyContextMenuItems(WKBundlePageRef);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ void AccessibilityController::setForceDeferredSpellChecking(bool shouldForce)
WKAccessibilitySetForceDeferredSpellChecking(shouldForce);
}

void AccessibilityController::setForceInitialFrameCaching(bool shouldForce)
{
WKAccessibilitySetForceInitialFrameCaching(shouldForce);
}

void AccessibilityController::makeWindowObject(JSContextRef context)
{
setGlobalObjectProperty(context, "accessibilityController", this);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class AccessibilityController : public JSWrappable {

void setIsolatedTreeMode(bool);
void setForceDeferredSpellChecking(bool);
void setForceInitialFrameCaching(bool);

JSRetainPtr<JSStringRef> platformName();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ interface AccessibilityController {
undefined setForceDeferredSpellChecking(boolean shouldForce);
undefined setRetainedElement(AccessibilityUIElement axElement);
AccessibilityUIElement retainedElement();
undefined setForceInitialFrameCaching(boolean shouldForce);

readonly attribute DOMString platformName;
readonly attribute AccessibilityUIElement rootElement;
Expand Down

0 comments on commit e6c6fde

Please sign in to comment.