From bf149c8710ce95415450a00ffaa60f8470ff9ed5 Mon Sep 17 00:00:00 2001 From: "Andrei.Salavei" Date: Fri, 17 Jan 2025 15:15:35 +0100 Subject: [PATCH] Move getAllUncoveredSemanticsNodesToIntObjectMap to common Move the semantic tree traversal function, including helper utils, to common code. Introduce SemanticRegion as a wrapper around the platform's Region class. Test: AndroidComposeViewAccessibilityDelegateCompatTest, ScrollCaptureTest Change-Id: Ibb25e7eac48ed0b61a9318b004669f0d7d0c8b97 --- .../AndroidContentCaptureManager.android.kt | 10 +- ...ViewAccessibilityDelegateCompat.android.kt | 13 +- .../ui/platform/SemanticsUtils.android.kt | 135 +----------------- .../ui/scrollcapture/ScrollCapture.android.kt | 2 +- .../ui/semantics/SemanticsRegion.android.kt | 47 ++++++ .../compose/ui/semantics/SemanticsOwner.kt | 114 +++++++++++++++ .../compose/ui/semantics/SemanticsRegion.kt | 34 +++++ .../semantics/SemanticsRegion.commonStubs.kt | 22 +++ .../ui/semantics/SemanticsRegion.skiko.kt | 48 +++++++ 9 files changed, 284 insertions(+), 141 deletions(-) create mode 100644 compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/semantics/SemanticsRegion.android.kt create mode 100644 compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsRegion.kt create mode 100644 compose/ui/ui/src/commonStubsMain/kotlin/androidx/compose/ui/semantics/SemanticsRegion.commonStubs.kt create mode 100644 compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/semantics/SemanticsRegion.skiko.kt diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/contentcapture/AndroidContentCaptureManager.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/contentcapture/AndroidContentCaptureManager.android.kt index 9299d389245dc..c8763a3b099d8 100644 --- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/contentcapture/AndroidContentCaptureManager.android.kt +++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/contentcapture/AndroidContentCaptureManager.android.kt @@ -37,21 +37,22 @@ import androidx.compose.ui.internal.checkPreconditionNotNull import androidx.compose.ui.node.LayoutNode import androidx.compose.ui.platform.AndroidComposeView import androidx.compose.ui.platform.SemanticsNodeCopy -import androidx.compose.ui.platform.SemanticsNodeWithAdjustedBounds import androidx.compose.ui.platform.coreshims.ContentCaptureSessionCompat import androidx.compose.ui.platform.coreshims.ViewCompatShims import androidx.compose.ui.platform.coreshims.ViewStructureCompat -import androidx.compose.ui.platform.getAllUncoveredSemanticsNodesToIntObjectMap import androidx.compose.ui.platform.getTextLayoutResult import androidx.compose.ui.platform.toLegacyClassName import androidx.compose.ui.semantics.SemanticsActions import androidx.compose.ui.semantics.SemanticsNode +import androidx.compose.ui.semantics.SemanticsNodeWithAdjustedBounds import androidx.compose.ui.semantics.SemanticsProperties +import androidx.compose.ui.semantics.getAllUncoveredSemanticsNodesToIntObjectMap import androidx.compose.ui.semantics.getOrNull import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastJoinToString import androidx.compose.ui.util.fastMap +import androidx.core.view.accessibility.AccessibilityNodeProviderCompat import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import java.util.function.Consumer @@ -109,7 +110,10 @@ internal class AndroidContentCaptureManager( get() { if (currentSemanticsNodesInvalidated) { // first instance of retrieving all nodes currentSemanticsNodesInvalidated = false - field = view.semanticsOwner.getAllUncoveredSemanticsNodesToIntObjectMap() + field = + view.semanticsOwner.getAllUncoveredSemanticsNodesToIntObjectMap( + customRootNodeId = AccessibilityNodeProviderCompat.HOST_VIEW_ID + ) currentSemanticsNodesSnapshotTimestampMillis = System.currentTimeMillis() } return field diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt index 02d9d5627b69d..50364567c35db 100644 --- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt +++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt @@ -58,7 +58,6 @@ import androidx.compose.ui.contentcapture.ContentCaptureManager import androidx.compose.ui.focus.FocusDirection.Companion.Exit import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.graphics.toComposeRect import androidx.compose.ui.internal.checkPreconditionNotNull import androidx.compose.ui.layout.boundsInParent import androidx.compose.ui.layout.positionInRoot @@ -84,9 +83,13 @@ import androidx.compose.ui.semantics.SemanticsActions.PageRight import androidx.compose.ui.semantics.SemanticsActions.PageUp import androidx.compose.ui.semantics.SemanticsConfiguration import androidx.compose.ui.semantics.SemanticsNode +import androidx.compose.ui.semantics.SemanticsNodeWithAdjustedBounds import androidx.compose.ui.semantics.SemanticsProperties import androidx.compose.ui.semantics.SemanticsPropertiesAndroid +import androidx.compose.ui.semantics.getAllUncoveredSemanticsNodesToIntObjectMap import androidx.compose.ui.semantics.getOrNull +import androidx.compose.ui.semantics.isHidden +import androidx.compose.ui.semantics.isImportantForAccessibility import androidx.compose.ui.semantics.subtreeSortedByGeometryGrouping import androidx.compose.ui.state.ToggleableState import androidx.compose.ui.text.AnnotatedString @@ -95,6 +98,7 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.platform.URLSpanCache import androidx.compose.ui.text.platform.toAccessibilitySpannableString import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.toRect import androidx.compose.ui.unit.toSize import androidx.compose.ui.util.fastCoerceIn import androidx.compose.ui.util.fastForEach @@ -310,7 +314,10 @@ internal class AndroidComposeViewAccessibilityDelegateCompat(val view: AndroidCo get() { if (currentSemanticsNodesInvalidated) { // first instance of retrieving all nodes currentSemanticsNodesInvalidated = false - field = view.semanticsOwner.getAllUncoveredSemanticsNodesToIntObjectMap() + field = + view.semanticsOwner.getAllUncoveredSemanticsNodesToIntObjectMap( + customRootNodeId = AccessibilityNodeProviderCompat.HOST_VIEW_ID + ) if (isEnabled) { setTraversalValues(field, idToBeforeMap, idToAfterMap, view.context.resources) } @@ -402,7 +409,7 @@ internal class AndroidComposeViewAccessibilityDelegateCompat(val view: AndroidCo // avoid overlapping siblings. Because position is a float (touch event can happen in- // between pixels), convert the int-based Android Rect to a float-based Compose Rect // before doing the comparison. - if (!node.adjustedBounds.toComposeRect().contains(position)) { + if (!node.adjustedBounds.toRect().contains(position)) { return@forEachValue } diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/SemanticsUtils.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/SemanticsUtils.android.kt index 27e66550d8b2c..f001290fa1add 100644 --- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/SemanticsUtils.android.kt +++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/SemanticsUtils.android.kt @@ -17,28 +17,19 @@ package androidx.compose.ui.platform import android.annotation.SuppressLint -import android.graphics.Region import android.view.View import androidx.collection.IntObjectMap -import androidx.collection.MutableIntObjectMap import androidx.collection.MutableIntSet -import androidx.collection.emptyIntObjectMap -import androidx.compose.ui.geometry.Rect import androidx.compose.ui.node.OwnerScope import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.ScrollAxisRange import androidx.compose.ui.semantics.SemanticsActions import androidx.compose.ui.semantics.SemanticsConfiguration import androidx.compose.ui.semantics.SemanticsNode -import androidx.compose.ui.semantics.SemanticsOwner -import androidx.compose.ui.semantics.SemanticsProperties -import androidx.compose.ui.semantics.SemanticsProperties.HideFromAccessibility -import androidx.compose.ui.semantics.SemanticsProperties.InvisibleToUser +import androidx.compose.ui.semantics.SemanticsNodeWithAdjustedBounds import androidx.compose.ui.semantics.getOrNull import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.util.fastForEach -import androidx.compose.ui.util.fastRoundToInt -import androidx.core.view.accessibility.AccessibilityNodeProviderCompat /** * A snapshot of the semantics node. The children here is fixed and are taken from the time this @@ -128,130 +119,6 @@ internal fun Role.toLegacyClassName(): String? = else -> null } -internal fun SemanticsNode.isImportantForAccessibility() = - !isHidden && - (unmergedConfig.isMergingSemanticsOfDescendants || - unmergedConfig.containsImportantForAccessibility()) - -@Suppress("DEPRECATION") -internal val SemanticsNode.isHidden: Boolean - // A node is considered hidden if it is transparent, or explicitly is hidden from accessibility. - // This also checks if the node has been marked as `invisibleToUser`, which is what the - // `hiddenFromAccessibility` API used to be named. - get() = - isTransparent || - (unmergedConfig.contains(HideFromAccessibility) || - unmergedConfig.contains(InvisibleToUser)) - -internal val DefaultFakeNodeBounds = Rect(0f, 0f, 10f, 10f) - -/** Semantics node with adjusted bounds for the uncovered(by siblings) part. */ -internal class SemanticsNodeWithAdjustedBounds( - val semanticsNode: SemanticsNode, - val adjustedBounds: android.graphics.Rect -) - /** This function retrieves the View corresponding to a semanticsId, if it exists. */ internal fun AndroidViewsHandler.semanticsIdToView(id: Int): View? = layoutNodeToHolder.entries.firstOrNull { it.key.semanticsId == id }?.value - -/** - * Finds pruned [SemanticsNode]s in the tree owned by this [SemanticsOwner]. A semantics node - * completely covered by siblings drawn on top of it will be pruned. Return the results in a map. - */ -internal fun SemanticsOwner.getAllUncoveredSemanticsNodesToIntObjectMap(): - IntObjectMap { - val root = unmergedRootSemanticsNode - if (!root.layoutNode.isPlaced || !root.layoutNode.isAttached) { - return emptyIntObjectMap() - } - - // Default capacity chosen to accommodate common scenarios - val nodes = MutableIntObjectMap(48) - - val unaccountedSpace = - with(root.boundsInRoot) { - Region( - left.fastRoundToInt(), - top.fastRoundToInt(), - right.fastRoundToInt(), - bottom.fastRoundToInt() - ) - } - - fun findAllSemanticNodesRecursive(currentNode: SemanticsNode, region: Region) { - val notAttachedOrPlaced = - !currentNode.layoutNode.isPlaced || !currentNode.layoutNode.isAttached - if ( - (unaccountedSpace.isEmpty && currentNode.id != root.id) || - (notAttachedOrPlaced && !currentNode.isFake) - ) { - return - } - val touchBoundsInRoot = currentNode.touchBoundsInRoot - val left = touchBoundsInRoot.left.fastRoundToInt() - val top = touchBoundsInRoot.top.fastRoundToInt() - val right = touchBoundsInRoot.right.fastRoundToInt() - val bottom = touchBoundsInRoot.bottom.fastRoundToInt() - - region.set(left, top, right, bottom) - - val virtualViewId = - if (currentNode.id == root.id) { - AccessibilityNodeProviderCompat.HOST_VIEW_ID - } else { - currentNode.id - } - if (region.op(unaccountedSpace, Region.Op.INTERSECT)) { - nodes[virtualViewId] = SemanticsNodeWithAdjustedBounds(currentNode, region.bounds) - // Children could be drawn outside of parent, but we are using clipped bounds for - // accessibility now, so let's put the children recursion inside of this if. If later - // we decide to support children drawn outside of parent, we can move it out of the - // if block. - val children = currentNode.replacedChildren - for (i in children.size - 1 downTo 0) { - // Links in text nodes are semantics children. But for Android accessibility support - // we don't publish them to the accessibility services because they are exposed - // as UrlSpan/ClickableSpan spans instead - if (children[i].config.contains(SemanticsProperties.LinkTestMarker)) { - continue - } - findAllSemanticNodesRecursive(children[i], region) - } - if (currentNode.isImportantForAccessibility()) { - unaccountedSpace.op(left, top, right, bottom, Region.Op.DIFFERENCE) - } - } else { - if (currentNode.isFake) { - val parentNode = currentNode.parent - // use parent bounds for fake node - val boundsForFakeNode = - if (parentNode?.layoutInfo?.isPlaced == true) { - parentNode.boundsInRoot - } else { - DefaultFakeNodeBounds - } - nodes[virtualViewId] = - SemanticsNodeWithAdjustedBounds( - currentNode, - android.graphics.Rect( - boundsForFakeNode.left.fastRoundToInt(), - boundsForFakeNode.top.fastRoundToInt(), - boundsForFakeNode.right.fastRoundToInt(), - boundsForFakeNode.bottom.fastRoundToInt(), - ) - ) - } else if (virtualViewId == AccessibilityNodeProviderCompat.HOST_VIEW_ID) { - // Root view might have WRAP_CONTENT layout params in which case it will have zero - // bounds if there is no other content with semantics. But we need to always send - // the - // root view info as there are some other apps (e.g. Google Assistant) that depend - // on accessibility info - nodes[virtualViewId] = SemanticsNodeWithAdjustedBounds(currentNode, region.bounds) - } - } - } - - findAllSemanticNodesRecursive(root, Region()) - return nodes -} diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/scrollcapture/ScrollCapture.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/scrollcapture/ScrollCapture.android.kt index 95f83bd4e6916..bb453a655730b 100644 --- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/scrollcapture/ScrollCapture.android.kt +++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/scrollcapture/ScrollCapture.android.kt @@ -30,13 +30,13 @@ import androidx.compose.ui.internal.checkPreconditionNotNull import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.boundsInRoot import androidx.compose.ui.layout.boundsInWindow -import androidx.compose.ui.platform.isHidden import androidx.compose.ui.semantics.SemanticsActions.ScrollByOffset import androidx.compose.ui.semantics.SemanticsNode import androidx.compose.ui.semantics.SemanticsOwner import androidx.compose.ui.semantics.SemanticsProperties.Disabled import androidx.compose.ui.semantics.SemanticsProperties.VerticalScrollAxisRange import androidx.compose.ui.semantics.getOrNull +import androidx.compose.ui.semantics.isHidden import androidx.compose.ui.unit.IntRect import androidx.compose.ui.unit.roundToIntRect import java.util.function.Consumer diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/semantics/SemanticsRegion.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/semantics/SemanticsRegion.android.kt new file mode 100644 index 0000000000000..d978519055d29 --- /dev/null +++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/semantics/SemanticsRegion.android.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.semantics + +import android.graphics.Region +import androidx.compose.ui.graphics.toComposeIntRect +import androidx.compose.ui.unit.IntRect + +/** Wrapper around platform-specific [android.graphics.Region] class */ +private class SemanticRegionImpl : SemanticsRegion { + val region = Region() + + override fun set(rect: IntRect) { + region.set(rect.left, rect.top, rect.right, rect.bottom) + } + + override val bounds: IntRect + get() = region.bounds.toComposeIntRect() + + override val isEmpty: Boolean + get() = region.isEmpty + + override fun intersect(region: SemanticsRegion): Boolean { + return this.region.op((region as SemanticRegionImpl).region, Region.Op.INTERSECT) + } + + override fun difference(rect: IntRect): Boolean { + return region.op(rect.left, rect.top, rect.right, rect.bottom, Region.Op.DIFFERENCE) + } +} + +/** Builder that creates wrapper around platform-specific Region class */ +internal actual fun SemanticsRegion(): SemanticsRegion = SemanticRegionImpl() diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsOwner.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsOwner.kt index dffed0f32e525..a002155fa01ea 100644 --- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsOwner.kt +++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsOwner.kt @@ -16,7 +16,15 @@ package androidx.compose.ui.semantics +import androidx.collection.IntObjectMap +import androidx.collection.MutableIntObjectMap +import androidx.collection.emptyIntObjectMap +import androidx.compose.ui.geometry.Rect import androidx.compose.ui.node.LayoutNode +import androidx.compose.ui.semantics.SemanticsProperties.HideFromAccessibility +import androidx.compose.ui.semantics.SemanticsProperties.InvisibleToUser +import androidx.compose.ui.unit.IntRect +import androidx.compose.ui.unit.roundToIntRect import androidx.compose.ui.util.fastForEach /** Owns [SemanticsNode] objects and notifies listeners of changes to the semantics tree */ @@ -98,3 +106,109 @@ internal fun SemanticsOwner.getAllSemanticsNodesToMap( } return nodes } + +internal fun SemanticsNode.isImportantForAccessibility() = + !isHidden && + (unmergedConfig.isMergingSemanticsOfDescendants || + unmergedConfig.containsImportantForAccessibility()) + +@Suppress("DEPRECATION") +internal val SemanticsNode.isHidden: Boolean + // A node is considered hidden if it is transparent, or explicitly is hidden from accessibility. + // This also checks if the node has been marked as `invisibleToUser`, which is what the + // `hiddenFromAccessibility` API used to be named. + get() = + isTransparent || + (unmergedConfig.contains(HideFromAccessibility) || + unmergedConfig.contains(InvisibleToUser)) + +private val DefaultFakeNodeBounds = Rect(0f, 0f, 10f, 10f) + +/** Semantics node with adjusted bounds for the uncovered(by siblings) part. */ +internal class SemanticsNodeWithAdjustedBounds( + val semanticsNode: SemanticsNode, + val adjustedBounds: IntRect +) + +/** + * Finds pruned [SemanticsNode]s in the tree owned by this [SemanticsOwner]. A semantics node + * completely covered by siblings drawn on top of it will be pruned. Return the results in a map. + */ +internal fun SemanticsOwner.getAllUncoveredSemanticsNodesToIntObjectMap( + customRootNodeId: Int +): IntObjectMap { + val root = unmergedRootSemanticsNode + if (!root.layoutNode.isPlaced || !root.layoutNode.isAttached) { + return emptyIntObjectMap() + } + + // Default capacity chosen to accommodate common scenarios + val nodes = MutableIntObjectMap(48) + + val unaccountedSpace = SemanticsRegion() + unaccountedSpace.set(root.boundsInRoot.roundToIntRect()) + + fun findAllSemanticNodesRecursive(currentNode: SemanticsNode, region: SemanticsRegion) { + val notAttachedOrPlaced = + !currentNode.layoutNode.isPlaced || !currentNode.layoutNode.isAttached + if ( + (unaccountedSpace.isEmpty && currentNode.id != root.id) || + (notAttachedOrPlaced && !currentNode.isFake) + ) { + return + } + val touchBoundsInRoot = currentNode.touchBoundsInRoot.roundToIntRect() + + region.set(touchBoundsInRoot) + + val virtualViewId = + if (currentNode.id == root.id) { + customRootNodeId + } else { + currentNode.id + } + if (region.intersect(unaccountedSpace)) { + nodes[virtualViewId] = SemanticsNodeWithAdjustedBounds(currentNode, region.bounds) + // Children could be drawn outside of parent, but we are using clipped bounds for + // accessibility now, so let's put the children recursion inside of this if. If later + // we decide to support children drawn outside of parent, we can move it out of the + // if block. + val children = currentNode.replacedChildren + for (i in children.size - 1 downTo 0) { + // Links in text nodes are semantics children. But for Android accessibility support + // we don't publish them to the accessibility services because they are exposed + // as UrlSpan/ClickableSpan spans instead + if (children[i].config.contains(SemanticsProperties.LinkTestMarker)) { + continue + } + findAllSemanticNodesRecursive(children[i], region) + } + if (currentNode.isImportantForAccessibility()) { + unaccountedSpace.difference(touchBoundsInRoot) + } + } else { + if (currentNode.isFake) { + val parentNode = currentNode.parent + // use parent bounds for fake node + val boundsForFakeNode = + if (parentNode?.layoutInfo?.isPlaced == true) { + parentNode.boundsInRoot + } else { + DefaultFakeNodeBounds + } + nodes[virtualViewId] = + SemanticsNodeWithAdjustedBounds(currentNode, boundsForFakeNode.roundToIntRect()) + } else if (virtualViewId == customRootNodeId) { + // Root view might have WRAP_CONTENT layout params in which case it will have zero + // bounds if there is no other content with semantics. But we need to always send + // the + // root view info as there are some other apps (e.g. Google Assistant) that depend + // on accessibility info + nodes[virtualViewId] = SemanticsNodeWithAdjustedBounds(currentNode, region.bounds) + } + } + } + + findAllSemanticNodesRecursive(root, SemanticsRegion()) + return nodes +} diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsRegion.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsRegion.kt new file mode 100644 index 0000000000000..fdcdb918d6953 --- /dev/null +++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsRegion.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.semantics + +import androidx.compose.ui.unit.IntRect + +/** Wrapper around platform-specific Region class */ +internal interface SemanticsRegion { + fun set(rect: IntRect) + + fun intersect(region: SemanticsRegion): Boolean + + fun difference(rect: IntRect): Boolean + + val bounds: IntRect + val isEmpty: Boolean +} + +/** Builder that creates wrapper around platform-specific Region class */ +internal expect fun SemanticsRegion(): SemanticsRegion diff --git a/compose/ui/ui/src/commonStubsMain/kotlin/androidx/compose/ui/semantics/SemanticsRegion.commonStubs.kt b/compose/ui/ui/src/commonStubsMain/kotlin/androidx/compose/ui/semantics/SemanticsRegion.commonStubs.kt new file mode 100644 index 0000000000000..02b3b4d7acd09 --- /dev/null +++ b/compose/ui/ui/src/commonStubsMain/kotlin/androidx/compose/ui/semantics/SemanticsRegion.commonStubs.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.semantics + +import androidx.compose.ui.implementedInJetBrainsFork + +/** Builder that creates wrapper around platform-specific Region class */ +internal actual fun SemanticsRegion(): SemanticsRegion = implementedInJetBrainsFork() diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/semantics/SemanticsRegion.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/semantics/SemanticsRegion.skiko.kt new file mode 100644 index 0000000000000..dec8d9c87c26b --- /dev/null +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/semantics/SemanticsRegion.skiko.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.semantics + +import androidx.compose.ui.unit.IntRect +import org.jetbrains.skia.IRect +import org.jetbrains.skia.Region + +private class SemanticRegionImpl : SemanticsRegion { + val region = Region() + + override fun set(rect: IntRect) { + region.setRect(IRect.makeLTRB(rect.left, rect.top, rect.right, rect.bottom)) + } + + override val bounds: IntRect + get() = region.bounds.let { + IntRect(it.left, it.top, it.right, it.bottom) + } + + override val isEmpty: Boolean + get() = region.isEmpty + + override fun intersect(region: SemanticsRegion): Boolean { + return this.region.op((region as SemanticRegionImpl).region, Region.Op.INTERSECT) + } + + override fun difference(rect: IntRect): Boolean { + return region.op(IRect.makeLTRB(rect.left, rect.top, rect.right, rect.bottom), Region.Op.DIFFERENCE) + } +} + +/** Builder that creates wrapper around platform-specific Region class */ +internal actual fun SemanticsRegion(): SemanticsRegion = SemanticRegionImpl()