[go: nahoru, domu]

blob: c957ee8527f7600bd21325d68675e970aa7c367f [file] [log] [blame]
/*
* Copyright 2019 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.node
import androidx.compose.testutils.TestViewConfiguration
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.autofill.Autofill
import androidx.compose.ui.autofill.AutofillTree
import androidx.compose.ui.draw.DrawModifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusOwner
import androidx.compose.ui.geometry.MutableRect
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.graphics.RenderEffect
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.hapticfeedback.HapticFeedback
import androidx.compose.ui.input.InputModeManager
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerIconService
import androidx.compose.ui.input.pointer.PointerInputFilter
import androidx.compose.ui.input.pointer.PointerInputModifier
import androidx.compose.ui.layout.AlignmentLine
import androidx.compose.ui.layout.LayoutModifier
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.RootMeasurePolicy.measure
import androidx.compose.ui.layout.layout
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.modifier.ModifierLocalManager
import androidx.compose.ui.platform.AccessibilityManager
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.WindowInfo
import androidx.compose.ui.platform.invertTo
import androidx.compose.ui.semantics.SemanticsConfiguration
import androidx.compose.ui.semantics.SemanticsModifier
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.input.TextInputService
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.toOffset
import androidx.compose.ui.zIndex
import com.google.common.truth.Truth.assertThat
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
@OptIn(ExperimentalComposeUiApi::class)
class LayoutNodeTest {
// Ensure that attach and detach work properly
@Test
fun layoutNodeAttachDetach() {
val node = LayoutNode()
assertNull(node.owner)
val owner = MockOwner()
node.attach(owner)
assertEquals(owner, node.owner)
assertTrue(node.isAttached)
assertEquals(1, owner.onAttachParams.count { it === node })
node.detach()
assertNull(node.owner)
assertFalse(node.isAttached)
assertEquals(1, owner.onDetachParams.count { it === node })
}
// Ensure that LayoutNode's children are ordered properly through add, remove, move
@Test
fun layoutNodeChildrenOrder() {
val (node, child1, child2) = createSimpleLayout()
assertEquals(2, node.children.size)
assertEquals(child1, node.children[0])
assertEquals(child2, node.children[1])
assertEquals(0, child1.children.size)
assertEquals(0, child2.children.size)
node.removeAt(index = 0, count = 1)
assertEquals(1, node.children.size)
assertEquals(child2, node.children[0])
node.insertAt(index = 0, instance = child1)
assertEquals(2, node.children.size)
assertEquals(child1, node.children[0])
assertEquals(child2, node.children[1])
node.removeAt(index = 0, count = 2)
assertEquals(0, node.children.size)
val child3 = LayoutNode()
val child4 = LayoutNode()
node.insertAt(0, child1)
node.insertAt(1, child2)
node.insertAt(2, child3)
node.insertAt(3, child4)
assertEquals(4, node.children.size)
assertEquals(child1, node.children[0])
assertEquals(child2, node.children[1])
assertEquals(child3, node.children[2])
assertEquals(child4, node.children[3])
node.move(from = 3, count = 1, to = 0)
assertEquals(4, node.children.size)
assertEquals(child4, node.children[0])
assertEquals(child1, node.children[1])
assertEquals(child2, node.children[2])
assertEquals(child3, node.children[3])
node.move(from = 0, count = 2, to = 3)
assertEquals(4, node.children.size)
assertEquals(child2, node.children[0])
assertEquals(child3, node.children[1])
assertEquals(child4, node.children[2])
assertEquals(child1, node.children[3])
}
// Ensure that attach of a LayoutNode connects all children
@Test
fun layoutNodeAttach() {
val (node, child1, child2) = createSimpleLayout()
val owner = MockOwner()
node.attach(owner)
assertEquals(owner, node.owner)
assertEquals(owner, child1.owner)
assertEquals(owner, child2.owner)
assertEquals(1, owner.onAttachParams.count { it === node })
assertEquals(1, owner.onAttachParams.count { it === child1 })
assertEquals(1, owner.onAttachParams.count { it === child2 })
}
// Ensure that detach of a LayoutNode detaches all children
@Test
fun layoutNodeDetach() {
val (node, child1, child2) = createSimpleLayout()
val owner = MockOwner()
node.attach(owner)
owner.onAttachParams.clear()
node.detach()
assertEquals(node, child1.parent)
assertEquals(node, child2.parent)
assertNull(node.owner)
assertNull(child1.owner)
assertNull(child2.owner)
assertEquals(1, owner.onDetachParams.count { it === node })
assertEquals(1, owner.onDetachParams.count { it === child1 })
assertEquals(1, owner.onDetachParams.count { it === child2 })
}
// Ensure that dropping a child also detaches it
@Test
fun layoutNodeDropDetaches() {
val (node, child1, child2) = createSimpleLayout()
val owner = MockOwner()
node.attach(owner)
node.removeAt(0, 1)
assertEquals(owner, node.owner)
assertNull(child1.owner)
assertEquals(owner, child2.owner)
assertEquals(0, owner.onDetachParams.count { it === node })
assertEquals(1, owner.onDetachParams.count { it === child1 })
assertEquals(0, owner.onDetachParams.count { it === child2 })
}
// Ensure that adopting a child also attaches it
@Test
fun layoutNodeAdoptAttaches() {
val (node, child1, child2) = createSimpleLayout()
val owner = MockOwner()
node.attach(owner)
node.removeAt(0, 1)
node.insertAt(1, child1)
assertEquals(owner, node.owner)
assertEquals(owner, child1.owner)
assertEquals(owner, child2.owner)
assertEquals(1, owner.onAttachParams.count { it === node })
assertEquals(2, owner.onAttachParams.count { it === child1 })
assertEquals(1, owner.onAttachParams.count { it === child2 })
}
@Test
fun childAdd() {
val node = LayoutNode()
val owner = MockOwner()
node.attach(owner)
assertEquals(1, owner.onAttachParams.count { it === node })
val child = LayoutNode()
node.insertAt(0, child)
assertEquals(1, owner.onAttachParams.count { it === child })
assertEquals(1, node.children.size)
assertEquals(node, child.parent)
assertEquals(owner, child.owner)
}
@Test
fun childCount() {
val node = LayoutNode()
assertEquals(0, node.children.size)
node.insertAt(0, LayoutNode())
assertEquals(1, node.children.size)
}
@Test
fun childGet() {
val node = LayoutNode()
val child = LayoutNode()
node.insertAt(0, child)
assertEquals(child, node.children[0])
}
@Test
fun noMove() {
val (layout, child1, child2) = createSimpleLayout()
layout.move(0, 0, 1)
assertEquals(child1, layout.children[0])
assertEquals(child2, layout.children[1])
}
@Test
fun childRemove() {
val node = LayoutNode()
val owner = MockOwner()
node.attach(owner)
val child = LayoutNode()
node.insertAt(0, child)
node.removeAt(index = 0, count = 1)
assertEquals(1, owner.onDetachParams.count { it === child })
assertEquals(0, node.children.size)
assertEquals(null, child.parent)
assertNull(child.owner)
}
// Ensure that depth is as expected
@Test
fun depth() {
val root = LayoutNode()
val (child, grand1, grand2) = createSimpleLayout()
root.insertAt(0, child)
val owner = MockOwner()
root.attach(owner)
assertEquals(0, root.depth)
assertEquals(1, child.depth)
assertEquals(2, grand1.depth)
assertEquals(2, grand2.depth)
}
// layoutNode hierarchy should be set properly when a LayoutNode is a child of a LayoutNode
@Test
fun directLayoutNodeHierarchy() {
val layoutNode = LayoutNode()
val childLayoutNode = LayoutNode()
layoutNode.insertAt(0, childLayoutNode)
assertNull(layoutNode.parent)
assertEquals(layoutNode, childLayoutNode.parent)
val layoutNodeChildren = layoutNode.children
assertEquals(1, layoutNodeChildren.size)
assertEquals(childLayoutNode, layoutNodeChildren[0])
layoutNode.removeAt(index = 0, count = 1)
assertNull(childLayoutNode.parent)
}
@Test
fun testLayoutNodeAdd() {
val (layout, child1, child2) = createSimpleLayout()
val inserted = LayoutNode()
layout.insertAt(0, inserted)
val children = layout.children
assertEquals(3, children.size)
assertEquals(inserted, children[0])
assertEquals(child1, children[1])
assertEquals(child2, children[2])
}
@Test
fun testLayoutNodeRemove() {
val (layout, child1, _) = createSimpleLayout()
val child3 = LayoutNode()
val child4 = LayoutNode()
layout.insertAt(2, child3)
layout.insertAt(3, child4)
layout.removeAt(index = 1, count = 2)
val children = layout.children
assertEquals(2, children.size)
assertEquals(child1, children[0])
assertEquals(child4, children[1])
}
@Test
fun testMoveChildren() {
val (layout, child1, child2) = createSimpleLayout()
val child3 = LayoutNode()
val child4 = LayoutNode()
layout.insertAt(2, child3)
layout.insertAt(3, child4)
layout.move(from = 2, to = 1, count = 2)
val children = layout.children
assertEquals(4, children.size)
assertEquals(child1, children[0])
assertEquals(child3, children[1])
assertEquals(child4, children[2])
assertEquals(child2, children[3])
layout.move(from = 1, to = 3, count = 2)
assertEquals(4, children.size)
assertEquals(child1, children[0])
assertEquals(child2, children[1])
assertEquals(child3, children[2])
assertEquals(child4, children[3])
}
@Test
fun testPxGlobalToLocal() {
val node0 = ZeroSizedLayoutNode()
node0.attach(MockOwner())
val node1 = ZeroSizedLayoutNode()
node0.insertAt(0, node1)
val x0 = 100
val y0 = 10
val x1 = 50
val y1 = 80
node0.place(x0, y0)
node1.place(x1, y1)
val globalPosition = Offset(250f, 300f)
val expectedX = globalPosition.x - x0.toFloat() - x1.toFloat()
val expectedY = globalPosition.y - y0.toFloat() - y1.toFloat()
val expectedPosition = Offset(expectedX, expectedY)
val result = node1.coordinates.windowToLocal(globalPosition)
assertEquals(expectedPosition, result)
}
@Test
fun testIntPxGlobalToLocal() {
val node0 = ZeroSizedLayoutNode()
node0.attach(MockOwner())
val node1 = ZeroSizedLayoutNode()
node0.insertAt(0, node1)
val x0 = 100
val y0 = 10
val x1 = 50
val y1 = 80
node0.place(x0, y0)
node1.place(x1, y1)
val globalPosition = Offset(250f, 300f)
val expectedX = globalPosition.x - x0.toFloat() - x1.toFloat()
val expectedY = globalPosition.y - y0.toFloat() - y1.toFloat()
val expectedPosition = Offset(expectedX, expectedY)
val result = node1.coordinates.windowToLocal(globalPosition)
assertEquals(expectedPosition, result)
}
@Test
fun testPxLocalToGlobal() {
val node0 = ZeroSizedLayoutNode()
node0.attach(MockOwner())
val node1 = ZeroSizedLayoutNode()
node0.insertAt(0, node1)
val x0 = 100
val y0 = 10
val x1 = 50
val y1 = 80
node0.place(x0, y0)
node1.place(x1, y1)
val localPosition = Offset(5f, 15f)
val expectedX = localPosition.x + x0.toFloat() + x1.toFloat()
val expectedY = localPosition.y + y0.toFloat() + y1.toFloat()
val expectedPosition = Offset(expectedX, expectedY)
val result = node1.coordinates.localToWindow(localPosition)
assertEquals(expectedPosition, result)
}
@Test
fun testIntPxLocalToGlobal() {
val node0 = ZeroSizedLayoutNode()
node0.attach(MockOwner())
val node1 = ZeroSizedLayoutNode()
node0.insertAt(0, node1)
val x0 = 100
val y0 = 10
val x1 = 50
val y1 = 80
node0.place(x0, y0)
node1.place(x1, y1)
val localPosition = Offset(5f, 15f)
val expectedX = localPosition.x + x0.toFloat() + x1.toFloat()
val expectedY = localPosition.y + y0.toFloat() + y1.toFloat()
val expectedPosition = Offset(expectedX, expectedY)
val result = node1.coordinates.localToWindow(localPosition)
assertEquals(expectedPosition, result)
}
@Test
fun testPxLocalToGlobalUsesOwnerPosition() {
val node = ZeroSizedLayoutNode()
node.attach(MockOwner(IntOffset(20, 20)))
node.place(100, 10)
val result = node.coordinates.localToWindow(Offset.Zero)
assertEquals(Offset(120f, 30f), result)
}
@Test
fun testIntPxLocalToGlobalUsesOwnerPosition() {
val node = ZeroSizedLayoutNode()
node.attach(MockOwner(IntOffset(20, 20)))
node.place(100, 10)
val result = node.coordinates.localToWindow(Offset.Zero)
assertEquals(Offset(120f, 30f), result)
}
@Test
fun testChildToLocal() {
val node0 = ZeroSizedLayoutNode()
node0.attach(MockOwner())
val node1 = ZeroSizedLayoutNode()
node0.insertAt(0, node1)
val x1 = 50
val y1 = 80
node0.place(100, 10)
node1.place(x1, y1)
val localPosition = Offset(5f, 15f)
val expectedX = localPosition.x + x1.toFloat()
val expectedY = localPosition.y + y1.toFloat()
val expectedPosition = Offset(expectedX, expectedY)
val result = node0.coordinates.localPositionOf(node1.coordinates, localPosition)
assertEquals(expectedPosition, result)
}
@Test
fun testLocalPositionOfWithSiblings() {
val node0 = LayoutNode()
node0.attach(MockOwner())
val node1 = LayoutNode()
val node2 = LayoutNode()
node0.insertAt(0, node1)
node0.insertAt(1, node2)
node1.place(10, 20)
node2.place(100, 200)
val offset = node2.coordinates.localPositionOf(node1.coordinates, Offset(5f, 15f))
assertEquals(Offset(-85f, -165f), offset)
}
@Test
fun testChildToLocalFailedWhenNotAncestorNoParent() {
val owner = MockOwner()
val node0 = LayoutNode()
node0.attach(owner)
val node1 = LayoutNode()
node1.attach(owner)
Assert.assertThrows(IllegalArgumentException::class.java) {
node1.coordinates.localPositionOf(node0.coordinates, Offset(5f, 15f))
}
}
@Test
fun testChildToLocalTheSameNode() {
val node = LayoutNode()
node.attach(MockOwner())
val position = Offset(5f, 15f)
val result = node.coordinates.localPositionOf(node.coordinates, position)
assertEquals(position, result)
}
@Test
fun testPositionRelativeToRoot() {
val parent = ZeroSizedLayoutNode()
parent.attach(MockOwner())
val child = ZeroSizedLayoutNode()
parent.insertAt(0, child)
parent.place(-100, 10)
child.place(50, 80)
val actual = child.coordinates.positionInRoot()
assertEquals(Offset(-50f, 90f), actual)
}
@Test
fun testPositionRelativeToRootIsNotAffectedByOwnerPosition() {
val parent = LayoutNode()
parent.attach(MockOwner(IntOffset(20, 20)))
val child = ZeroSizedLayoutNode()
parent.insertAt(0, child)
child.place(50, 80)
val actual = child.coordinates.positionInRoot()
assertEquals(Offset(50f, 80f), actual)
}
@Test
fun testPositionRelativeToAncestorWithParent() {
val parent = ZeroSizedLayoutNode()
parent.attach(MockOwner())
val child = ZeroSizedLayoutNode()
parent.insertAt(0, child)
parent.place(-100, 10)
child.place(50, 80)
val actual = parent.coordinates.localPositionOf(child.coordinates, Offset.Zero)
assertEquals(Offset(50f, 80f), actual)
}
@Test
fun testPositionRelativeToAncestorWithGrandParent() {
val grandParent = ZeroSizedLayoutNode()
grandParent.attach(MockOwner())
val parent = ZeroSizedLayoutNode()
val child = ZeroSizedLayoutNode()
grandParent.insertAt(0, parent)
parent.insertAt(0, child)
grandParent.place(-7, 17)
parent.place(23, -13)
child.place(-3, 11)
val actual = grandParent.coordinates.localPositionOf(child.coordinates, Offset.Zero)
assertEquals(Offset(20f, -2f), actual)
}
// LayoutNode shouldn't allow adding beyond the count
@Test
fun testAddBeyondCurrent() {
val node = LayoutNode()
Assert.assertThrows(IndexOutOfBoundsException::class.java) {
node.insertAt(1, LayoutNode())
}
}
// LayoutNode shouldn't allow adding below 0
@Test
fun testAddBelowZero() {
val node = LayoutNode()
Assert.assertThrows(IndexOutOfBoundsException::class.java) {
node.insertAt(-1, LayoutNode())
}
}
// LayoutNode should error when removing at index < 0
@Test
fun testRemoveNegativeIndex() {
val node = LayoutNode()
node.insertAt(0, LayoutNode())
Assert.assertThrows(IndexOutOfBoundsException::class.java) {
node.removeAt(-1, 1)
}
}
// LayoutNode should error when removing at index > count
@Test
fun testRemoveBeyondIndex() {
val node = LayoutNode()
node.insertAt(0, LayoutNode())
Assert.assertThrows(IndexOutOfBoundsException::class.java) {
node.removeAt(1, 1)
}
}
// LayoutNode should error when removing at count < 0
@Test
fun testRemoveNegativeCount() {
val node = LayoutNode()
node.insertAt(0, LayoutNode())
Assert.assertThrows(IllegalArgumentException::class.java) {
node.removeAt(0, -1)
}
}
// LayoutNode should error when removing at count > entry count
@Test
fun testRemoveWithIndexBeyondSize() {
val node = LayoutNode()
node.insertAt(0, LayoutNode())
Assert.assertThrows(IndexOutOfBoundsException::class.java) {
node.removeAt(0, 2)
}
}
// LayoutNode should error when there aren't enough items
@Test
fun testRemoveWithIndexEqualToSize() {
val node = LayoutNode()
Assert.assertThrows(IndexOutOfBoundsException::class.java) {
node.removeAt(0, 1)
}
}
// LayoutNode should allow removing two items
@Test
fun testRemoveTwoItems() {
val node = LayoutNode()
node.insertAt(0, LayoutNode())
node.insertAt(0, LayoutNode())
node.removeAt(0, 2)
assertEquals(0, node.children.size)
}
// The layout coordinates of a LayoutNode should be attached when
// the layout node is attached.
@Test
fun coordinatesAttachedWhenLayoutNodeAttached() {
val layoutNode = LayoutNode()
val layoutModifier = Modifier.graphicsLayer { }
layoutNode.modifier = layoutModifier
assertFalse(layoutNode.coordinates.isAttached)
assertFalse(layoutNode.coordinates.isAttached)
layoutNode.attach(MockOwner())
assertTrue(layoutNode.coordinates.isAttached)
assertTrue(layoutNode.coordinates.isAttached)
layoutNode.detach()
assertFalse(layoutNode.coordinates.isAttached)
assertFalse(layoutNode.coordinates.isAttached)
}
// The NodeCoordinator should be reused when it has been replaced with the same type
@Test
fun nodeCoordinatorSameWithReplacementModifier() {
val layoutNode = LayoutNode()
val layoutModifier = Modifier.graphicsLayer { }
layoutNode.modifier = layoutModifier
val oldNodeCoordinator = layoutNode.outerCoordinator
assertFalse(oldNodeCoordinator.isAttached)
layoutNode.attach(MockOwner())
assertTrue(oldNodeCoordinator.isAttached)
layoutNode.modifier = Modifier.graphicsLayer { }
val newNodeCoordinator = layoutNode.outerCoordinator
assertSame(newNodeCoordinator, oldNodeCoordinator)
}
// The NodeCoordinator should be reused when it has been replaced with the same type,
// even with multiple NodeCoordinators for one modifier.
@Test
fun nodeCoordinatorSameWithReplacementMultiModifier() {
class TestModifier : DrawModifier, LayoutModifier {
override fun ContentDrawScope.draw() {
drawContent()
}
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
) = layout(0, 0) {}
}
val layoutNode = LayoutNode()
layoutNode.modifier = TestModifier()
val oldNodeCoordinator = layoutNode.outerCoordinator
val oldNodeCoordinator2 = oldNodeCoordinator.wrapped
layoutNode.modifier = TestModifier()
val newNodeCoordinator = layoutNode.outerCoordinator
val newNodeCoordinator2 = newNodeCoordinator.wrapped
assertSame(newNodeCoordinator, oldNodeCoordinator)
assertSame(newNodeCoordinator2, oldNodeCoordinator2)
}
// The NodeCoordinator should be detached when it has been replaced.
@Test
fun nodeCoordinatorAttachedWhenLayoutNodeAttached() {
val layoutNode = LayoutNode()
// 2 modifiers at the start
val layoutModifier = Modifier.graphicsLayer { }.graphicsLayer { }
layoutNode.modifier = layoutModifier
val oldNodeCoordinator = layoutNode.outerCoordinator
val oldInnerNodeCoordinator = oldNodeCoordinator.wrapped
assertFalse(oldNodeCoordinator.isAttached)
assertNotNull(oldInnerNodeCoordinator)
assertFalse(oldInnerNodeCoordinator!!.isAttached)
layoutNode.attach(MockOwner())
assertTrue(oldNodeCoordinator.isAttached)
// only 1 modifier now, so one should be detached and the other can be reused
layoutNode.modifier = Modifier.graphicsLayer()
val newNodeCoordinator = layoutNode.outerCoordinator
// one can be reused, but we don't care which one
val notReused = if (newNodeCoordinator == oldNodeCoordinator) {
oldInnerNodeCoordinator
} else {
oldNodeCoordinator
}
assertTrue(newNodeCoordinator.isAttached)
assertFalse(notReused.isAttached)
}
@Test
fun nodeCoordinatorParentLayoutCoordinates() {
val layoutNode = LayoutNode()
val layoutNode2 = LayoutNode()
val layoutModifier = Modifier.graphicsLayer { }
layoutNode.modifier = layoutModifier
layoutNode2.insertAt(0, layoutNode)
layoutNode2.attach(MockOwner())
assertEquals(
layoutNode2.innerCoordinator,
layoutNode.innerCoordinator.parentLayoutCoordinates
)
assertEquals(
layoutNode2.innerCoordinator,
layoutNode.outerCoordinator.parentLayoutCoordinates
)
}
@Test
fun nodeCoordinatorParentCoordinates() {
val layoutNode = LayoutNode()
val layoutNode2 = LayoutNode()
val layoutModifier = object : LayoutModifier {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
TODO("Not yet implemented")
}
}
val drawModifier = Modifier.drawBehind { }
layoutNode.modifier = layoutModifier.then(drawModifier)
layoutNode2.insertAt(0, layoutNode)
layoutNode2.attach(MockOwner())
val layoutModifierWrapper = layoutNode.outerCoordinator
assertEquals(
layoutModifierWrapper,
layoutNode.innerCoordinator.parentCoordinates
)
assertEquals(
layoutNode2.innerCoordinator,
layoutModifierWrapper.parentCoordinates
)
}
@Test
fun nodeCoordinator_transformFrom_offsets() {
val parent = ZeroSizedLayoutNode()
parent.attach(MockOwner())
val child = ZeroSizedLayoutNode()
parent.insertAt(0, child)
parent.place(-100, 10)
child.place(50, 80)
val matrix = Matrix()
child.innerCoordinator.transformFrom(parent.innerCoordinator, matrix)
assertEquals(Offset(-50f, -80f), matrix.map(Offset.Zero))
parent.innerCoordinator.transformFrom(child.innerCoordinator, matrix)
assertEquals(Offset(50f, 80f), matrix.map(Offset.Zero))
}
@Test
fun nodeCoordinator_transformFrom_translation() {
val parent = ZeroSizedLayoutNode()
parent.attach(MockOwner())
val child = ZeroSizedLayoutNode()
parent.insertAt(0, child)
child.modifier = Modifier.graphicsLayer {
translationX = 5f
translationY = 2f
}
parent.outerCoordinator
.measure(listOf(parent.outerCoordinator), Constraints())
child.outerCoordinator
.measure(listOf(child.outerCoordinator), Constraints())
parent.place(0, 0)
child.place(0, 0)
val matrix = Matrix()
child.innerCoordinator.transformFrom(parent.innerCoordinator, matrix)
assertEquals(-5f, matrix.map(Offset.Zero).x, 0.001f)
assertEquals(-2f, matrix.map(Offset.Zero).y, 0.001f)
parent.innerCoordinator.transformFrom(child.innerCoordinator, matrix)
assertEquals(5f, matrix.map(Offset.Zero).x, 0.001f)
assertEquals(2f, matrix.map(Offset.Zero).y, 0.001f)
}
@Test
fun nodeCoordinator_transformFrom_rotation() {
val parent = ZeroSizedLayoutNode()
parent.attach(MockOwner())
val child = ZeroSizedLayoutNode()
parent.insertAt(0, child)
child.modifier = Modifier.graphicsLayer {
rotationZ = 90f
}
parent.outerCoordinator
.measure(listOf(parent.outerCoordinator), Constraints())
child.outerCoordinator
.measure(listOf(child.outerCoordinator), Constraints())
parent.place(0, 0)
child.place(0, 0)
val matrix = Matrix()
child.innerCoordinator.transformFrom(parent.innerCoordinator, matrix)
assertEquals(0f, matrix.map(Offset(1f, 0f)).x, 0.001f)
assertEquals(-1f, matrix.map(Offset(1f, 0f)).y, 0.001f)
parent.innerCoordinator.transformFrom(child.innerCoordinator, matrix)
assertEquals(0f, matrix.map(Offset(1f, 0f)).x, 0.001f)
assertEquals(1f, matrix.map(Offset(1f, 0f)).y, 0.001f)
}
@Test
fun nodeCoordinator_transformFrom_scale() {
val parent = ZeroSizedLayoutNode()
parent.attach(MockOwner())
val child = ZeroSizedLayoutNode()
parent.insertAt(0, child)
child.modifier = Modifier.graphicsLayer {
scaleX = 0f
}
parent.outerCoordinator
.measure(listOf(parent.outerCoordinator), Constraints())
child.outerCoordinator
.measure(listOf(child.outerCoordinator), Constraints())
parent.place(0, 0)
child.place(0, 0)
val matrix = Matrix()
child.innerCoordinator.transformFrom(parent.innerCoordinator, matrix)
// The X coordinate is somewhat nonsensical since it is scaled to 0
// We've chosen to make it not transform when there's a nonsensical inverse.
assertEquals(1f, matrix.map(Offset(1f, 1f)).x, 0.001f)
assertEquals(1f, matrix.map(Offset(1f, 1f)).y, 0.001f)
parent.innerCoordinator.transformFrom(child.innerCoordinator, matrix)
// This direction works, so we can expect the normal scaling
assertEquals(0f, matrix.map(Offset(1f, 1f)).x, 0.001f)
assertEquals(1f, matrix.map(Offset(1f, 1f)).y, 0.001f)
child.innerCoordinator.onLayerBlockUpdated {
scaleX = 0.5f
scaleY = 0.25f
}
child.innerCoordinator.transformFrom(parent.innerCoordinator, matrix)
assertEquals(2f, matrix.map(Offset(1f, 1f)).x, 0.001f)
assertEquals(4f, matrix.map(Offset(1f, 1f)).y, 0.001f)
parent.innerCoordinator.transformFrom(child.innerCoordinator, matrix)
assertEquals(0.5f, matrix.map(Offset(1f, 1f)).x, 0.001f)
assertEquals(0.25f, matrix.map(Offset(1f, 1f)).y, 0.001f)
}
@Test
fun nodeCoordinator_transformFrom_siblings() {
val parent = ZeroSizedLayoutNode()
parent.attach(MockOwner())
val child1 = ZeroSizedLayoutNode()
parent.insertAt(0, child1)
child1.modifier = Modifier.graphicsLayer {
scaleX = 0.5f
scaleY = 0.25f
transformOrigin = TransformOrigin(0f, 0f)
}
val child2 = ZeroSizedLayoutNode()
parent.insertAt(0, child2)
child2.modifier = Modifier.graphicsLayer {
scaleX = 5f
scaleY = 2f
transformOrigin = TransformOrigin(0f, 0f)
}
parent.outerCoordinator
.measure(listOf(parent.outerCoordinator), Constraints())
child1.outerCoordinator
.measure(listOf(child1.outerCoordinator), Constraints())
child2.outerCoordinator
.measure(listOf(child2.outerCoordinator), Constraints())
parent.place(0, 0)
child1.place(100, 200)
child2.place(5, 11)
val matrix = Matrix()
child2.innerCoordinator.transformFrom(child1.innerCoordinator, matrix)
// (20, 36) should be (10, 9) in real coordinates due to scaling
// Translate to (110, 209) in the parent
// Translate to (105, 198) in child2's coordinates, discounting scale
// Scaled to (21, 99)
val offset = matrix.map(Offset(20f, 36f))
assertEquals(21f, offset.x, 0.001f)
assertEquals(99f, offset.y, 0.001f)
child1.innerCoordinator.transformFrom(child2.innerCoordinator, matrix)
val offset2 = matrix.map(Offset(21f, 99f))
assertEquals(20f, offset2.x, 0.001f)
assertEquals(36f, offset2.y, 0.001f)
}
@Test
fun nodeCoordinator_transformFrom_cousins() {
val parent = ZeroSizedLayoutNode()
parent.attach(MockOwner())
val child1 = ZeroSizedLayoutNode()
parent.insertAt(0, child1)
val child2 = ZeroSizedLayoutNode()
parent.insertAt(1, child2)
val grandChild1 = ZeroSizedLayoutNode()
child1.insertAt(0, grandChild1)
val grandChild2 = ZeroSizedLayoutNode()
child2.insertAt(0, grandChild2)
parent.place(-100, 10)
child1.place(10, 11)
child2.place(22, 33)
grandChild1.place(45, 27)
grandChild2.place(17, 59)
val matrix = Matrix()
grandChild1.innerCoordinator.transformFrom(grandChild2.innerCoordinator, matrix)
// (17, 59) + (22, 33) - (10, 11) - (45, 27) = (-16, 54)
assertEquals(Offset(-16f, 54f), matrix.map(Offset.Zero))
grandChild2.innerCoordinator.transformFrom(grandChild1.innerCoordinator, matrix)
assertEquals(Offset(16f, -54f), matrix.map(Offset.Zero))
}
@Test
fun hitTest_pointerInBounds_pointerInputFilterHit() {
val pointerInputFilter: PointerInputFilter = mockPointerInputFilter()
val layoutNode =
LayoutNode(
0, 0, 1, 1,
PointerInputModifierImpl(pointerInputFilter)
).apply {
attach(MockOwner())
}
val hit = mutableListOf<PointerInputModifierNode>()
layoutNode.hitTest(Offset(0f, 0f), hit)
assertThat(hit.toFilters()).isEqualTo(listOf(pointerInputFilter))
}
@Test
fun hitTest_pointerInMinimumTouchTarget_pointerInputFilterHit() {
val pointerInputFilter: PointerInputFilter = mockPointerInputFilter()
val layoutNode =
LayoutNode(
0, 0, 1, 1,
PointerInputModifierImpl(pointerInputFilter),
DpSize(48.dp, 48.dp)
).apply {
attach(MockOwner())
}
val hit = mutableListOf<PointerInputModifierNode>()
layoutNode.hitTest(Offset(-3f, 3f), hit, true)
assertThat(hit.toFilters()).isEqualTo(listOf(pointerInputFilter))
}
@Test
fun hitTest_pointerInMinimumTouchTarget_pointerInputFilterHit_horizontal() {
val pointerInputFilter: PointerInputFilter = mockPointerInputFilter()
val layoutNode =
LayoutNode(
0, 0, 1000, 1,
PointerInputModifierImpl(pointerInputFilter),
DpSize(48.dp, 48.dp)
).apply {
attach(MockOwner())
}
val hit = mutableListOf<PointerInputModifierNode>()
layoutNode.hitTest(Offset(0f, 3f), hit, true)
assertThat(hit.toFilters()).isEqualTo(listOf(pointerInputFilter))
}
@Test
fun hitTest_pointerInMinimumTouchTarget_pointerInputFilterHit_vertical() {
val pointerInputFilter: PointerInputFilter = mockPointerInputFilter()
val layoutNode =
LayoutNode(
0, 0, 1, 1000,
PointerInputModifierImpl(pointerInputFilter),
DpSize(48.dp, 48.dp)
).apply {
attach(MockOwner())
}
val hit = mutableListOf<PointerInputModifierNode>()
layoutNode.hitTest(Offset(3f, 0f), hit, true)
assertThat(hit.toFilters()).isEqualTo(listOf(pointerInputFilter))
}
@Test
fun hitTest_pointerInMinimumTouchTarget_pointerInputFilterHit_nestedNodes() {
val pointerInputFilter: PointerInputFilter = mockPointerInputFilter()
val outerNode = LayoutNode(0, 0, 1, 1).apply { attach(MockOwner()) }
val layoutNode = LayoutNode(
0, 0, 1, 1,
PointerInputModifierImpl(pointerInputFilter),
DpSize(48.dp, 48.dp)
)
outerNode.add(layoutNode)
layoutNode.onNodePlaced()
val hit = mutableListOf<PointerInputModifierNode>()
outerNode.hitTest(Offset(-3f, 3f), hit, true)
assertThat(hit.toFilters()).isEqualTo(listOf(pointerInputFilter))
}
@Test
fun hitTest_pointerInputFilterHit_outsideParent() {
val outerPointerInputFilter: PointerInputFilter = mockPointerInputFilter()
val outerNode = LayoutNode(
0, 0, 10, 10,
PointerInputModifierImpl(outerPointerInputFilter)
).apply { attach(MockOwner()) }
val pointerInputFilter: PointerInputFilter = mockPointerInputFilter()
val layoutNode = LayoutNode(
20, 20, 30, 30,
PointerInputModifierImpl(pointerInputFilter)
)
outerNode.add(layoutNode)
layoutNode.onNodePlaced()
val hit = mutableListOf<PointerInputModifierNode>()
outerNode.hitTest(Offset(25f, 25f), hit)
assertThat(hit.toFilters()).isEqualTo(listOf(pointerInputFilter))
}
@Test
fun hitTest_pointerInputFilterHit_outsideParent_interceptOutOfBoundsChildEvents() {
val outerPointerInputFilter: PointerInputFilter = mockPointerInputFilter(
interceptChildEvents = true
)
val outerNode = LayoutNode(
0, 0, 10, 10,
PointerInputModifierImpl(outerPointerInputFilter)
).apply { attach(MockOwner()) }
val pointerInputFilter: PointerInputFilter = mockPointerInputFilter()
val layoutNode = LayoutNode(
20, 20, 30, 30,
PointerInputModifierImpl(pointerInputFilter)
)
outerNode.add(layoutNode)
layoutNode.onNodePlaced()
val hit = mutableListOf<PointerInputModifierNode>()
outerNode.hitTest(Offset(25f, 25f), hit)
assertThat(hit.toFilters()).isEqualTo(listOf(outerPointerInputFilter, pointerInputFilter))
}
@Test
fun hitTest_pointerInMinimumTouchTarget_closestHit() {
val pointerInputFilter1: PointerInputFilter = mockPointerInputFilter()
val layoutNode1 = LayoutNode(
0, 0, 5, 5,
PointerInputModifierImpl(pointerInputFilter1),
DpSize(48.dp, 48.dp)
)
val pointerInputFilter2: PointerInputFilter = mockPointerInputFilter()
val layoutNode2 = LayoutNode(
6, 6, 11, 11,
PointerInputModifierImpl(pointerInputFilter2),
DpSize(48.dp, 48.dp)
)
val outerNode = LayoutNode(0, 0, 11, 11).apply { attach(MockOwner()) }
outerNode.add(layoutNode1)
outerNode.add(layoutNode2)
layoutNode1.onNodePlaced()
layoutNode2.onNodePlaced()
val hit = mutableListOf<PointerInputModifierNode>()
// Hit closer to layoutNode1
outerNode.hitTest(Offset(5.1f, 5.5f), hit, true)
assertThat(hit.toFilters()).isEqualTo(listOf(pointerInputFilter1))
hit.clear()
// Hit closer to layoutNode2
outerNode.hitTest(Offset(5.9f, 5.5f), hit, true)
assertThat(hit.toFilters()).isEqualTo(listOf(pointerInputFilter2))
hit.clear()
// Hit closer to layoutNode1
outerNode.hitTest(Offset(5.5f, 5.1f), hit, true)
assertThat(hit.toFilters()).isEqualTo(listOf(pointerInputFilter1))
hit.clear()
// Hit closer to layoutNode2
outerNode.hitTest(Offset(5.5f, 5.9f), hit, true)
assertThat(hit.toFilters()).isEqualTo(listOf(pointerInputFilter2))
hit.clear()
// Hit inside layoutNode1
outerNode.hitTest(Offset(4.9f, 4.9f), hit, true)
assertThat(hit.toFilters()).isEqualTo(listOf(pointerInputFilter1))
hit.clear()
// Hit inside layoutNode2
outerNode.hitTest(Offset(6.1f, 6.1f), hit, true)
assertThat(hit.toFilters()).isEqualTo(listOf(pointerInputFilter2))
}
/**
* When a child is in the minimum touch target area, but the parent is big enough to not
* worry about minimum touch target, the child should still be able to be hit outside the
* parent's bounds.
*/
@Test
fun hitTest_pointerInMinimumTouchTarget_inChild_closestHit() {
test_pointerInMinimumTouchTarget_inChild_closestHit { outerNode, nodeWithChild, soloNode ->
outerNode.add(nodeWithChild)
outerNode.add(soloNode)
}
}
/**
* When a child is in the minimum touch target area, but the parent is big enough to not
* worry about minimum touch target, the child should still be able to be hit outside the
* parent's bounds. This is different from
* [hitTest_pointerInMinimumTouchTarget_inChild_closestHit] because the node with the nested
* child is after the other node.
*/
@Test
fun hitTest_pointerInMinimumTouchTarget_inChildOver_closestHit() {
test_pointerInMinimumTouchTarget_inChild_closestHit { outerNode, nodeWithChild, soloNode ->
outerNode.add(soloNode)
outerNode.add(nodeWithChild)
}
}
private fun test_pointerInMinimumTouchTarget_inChild_closestHit(
block: (outerNode: LayoutNode, nodeWithChild: LayoutNode, soloNode: LayoutNode) -> Unit
) {
val pointerInputFilter1: PointerInputFilter = mockPointerInputFilter()
val layoutNode1 = LayoutNode(
5, 5, 10, 10,
PointerInputModifierImpl(pointerInputFilter1),
DpSize(48.dp, 48.dp)
)
val pointerInputFilter2: PointerInputFilter =
mockPointerInputFilter(interceptChildEvents = true)
val layoutNode2 = LayoutNode(
0, 0, 10, 10,
PointerInputModifierImpl(pointerInputFilter2),
DpSize(48.dp, 48.dp)
)
layoutNode2.add(layoutNode1)
val pointerInputFilter3: PointerInputFilter = mockPointerInputFilter()
val layoutNode3 = LayoutNode(
12, 12, 17, 17,
PointerInputModifierImpl(pointerInputFilter3),
DpSize(48.dp, 48.dp)
)
val outerNode = LayoutNode(0, 0, 20, 20).apply { attach(MockOwner()) }
block(outerNode, layoutNode2, layoutNode3)
layoutNode1.onNodePlaced()
layoutNode2.onNodePlaced()
layoutNode3.onNodePlaced()
val hit = mutableListOf<PointerInputModifierNode>()
// Hit outside of layoutNode2, but near layoutNode1
outerNode.hitTest(Offset(10.1f, 10.1f), hit, true)
assertThat(hit.toFilters()).isEqualTo(listOf(pointerInputFilter2, pointerInputFilter1))
hit.clear()
// Hit closer to layoutNode3
outerNode.hitTest(Offset(11.9f, 11.9f), hit, true)
assertThat(hit.toFilters()).isEqualTo(listOf(pointerInputFilter3))
}
@Test
fun hitTest_pointerInMinimumTouchTarget_closestHitWithOverlap() {
val pointerInputFilter1: PointerInputFilter = mockPointerInputFilter()
val layoutNode1 = LayoutNode(
0, 0, 5, 5, PointerInputModifierImpl(pointerInputFilter1),
DpSize(48.dp, 48.dp)
)
val pointerInputFilter2: PointerInputFilter = mockPointerInputFilter()
val layoutNode2 = LayoutNode(
4, 4, 9, 9,
PointerInputModifierImpl(pointerInputFilter2),
DpSize(48.dp, 48.dp)
)
val outerNode = LayoutNode(0, 0, 9, 9).apply { attach(MockOwner()) }
outerNode.add(layoutNode1)
outerNode.add(layoutNode2)
layoutNode1.onNodePlaced()
layoutNode2.onNodePlaced()
val hit = mutableListOf<PointerInputModifierNode>()
// Hit layoutNode1
outerNode.hitTest(Offset(3.95f, 3.95f), hit, true)
assertThat(hit.toFilters()).isEqualTo(listOf(pointerInputFilter1))
hit.clear()
// Hit layoutNode2
outerNode.hitTest(Offset(4.05f, 4.05f), hit, true)
assertThat(hit.toFilters()).isEqualTo(listOf(pointerInputFilter2))
}
@Test
fun hitTestSemantics_pointerInMinimumTouchTarget_pointerInputFilterHit() {
val semanticsConfiguration = SemanticsConfiguration()
val semanticsModifier = object : SemanticsModifier {
override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
}
val layoutNode =
LayoutNode(
0, 0, 1, 1,
semanticsModifier,
DpSize(48.dp, 48.dp)
).apply {
attach(MockOwner())
}
val hit = HitTestResult<SemanticsModifierNode>()
layoutNode.hitTestSemantics(Offset(-3f, 3f), hit)
assertThat(hit).hasSize(1)
// assertThat(hit[0].modifier).isEqualTo(semanticsModifier)
}
@Test
fun hitTestSemantics_pointerInMinimumTouchTarget_pointerInputFilterHit_nestedNodes() {
val semanticsConfiguration = SemanticsConfiguration()
val semanticsModifier = object : SemanticsModifier {
override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
}
val outerNode = LayoutNode(0, 0, 1, 1).apply { attach(MockOwner()) }
val layoutNode = LayoutNode(0, 0, 1, 1, semanticsModifier, DpSize(48.dp, 48.dp))
outerNode.add(layoutNode)
layoutNode.onNodePlaced()
val hit = HitTestResult<SemanticsModifierNode>()
layoutNode.hitTestSemantics(Offset(-3f, 3f), hit)
assertThat(hit).hasSize(1)
assertThat(hit[0].toModifier()).isEqualTo(semanticsModifier)
}
@Test
fun hitTestSemantics_pointerInMinimumTouchTarget_closestHit() {
val semanticsConfiguration = SemanticsConfiguration()
val semanticsModifier1 = object : SemanticsModifierNode, Modifier.Node() {
override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
}
val semanticsModifier2 = object : SemanticsModifierNode, Modifier.Node() {
override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
}
val semanticsModifierElement1 = modifierElementOf(null, { semanticsModifier1 }, { }, { })
val semanticsModifierElement2 = modifierElementOf(null, { semanticsModifier2 }, { }, { })
val layoutNode1 = LayoutNode(0, 0, 5, 5, semanticsModifierElement1, DpSize(48.dp, 48.dp))
val layoutNode2 = LayoutNode(6, 6, 11, 11, semanticsModifierElement2, DpSize(48.dp, 48.dp))
val outerNode = LayoutNode(0, 0, 11, 11).apply { attach(MockOwner()) }
outerNode.add(layoutNode1)
outerNode.add(layoutNode2)
layoutNode1.onNodePlaced()
layoutNode2.onNodePlaced()
// Hit closer to layoutNode1
val hit1 = HitTestResult<SemanticsModifierNode>()
outerNode.hitTestSemantics(Offset(5.1f, 5.5f), hit1, true)
assertThat(hit1).hasSize(1)
assertThat(hit1[0]).isEqualTo(semanticsModifier1)
// Hit closer to layoutNode2
val hit2 = HitTestResult<SemanticsModifierNode>()
outerNode.hitTestSemantics(Offset(5.9f, 5.5f), hit2, true)
assertThat(hit2).hasSize(1)
assertThat(hit2[0]).isEqualTo(semanticsModifier2)
// Hit closer to layoutNode1
val hit3 = HitTestResult<SemanticsModifierNode>()
outerNode.hitTestSemantics(Offset(5.5f, 5.1f), hit3, true)
assertThat(hit3).hasSize(1)
assertThat(hit3[0]).isEqualTo(semanticsModifier1)
// Hit closer to layoutNode2
val hit4 = HitTestResult<SemanticsModifierNode>()
outerNode.hitTestSemantics(Offset(5.5f, 5.9f), hit4, true)
assertThat(hit4).hasSize(1)
assertThat(hit4[0]).isEqualTo(semanticsModifier2)
// Hit inside layoutNode1
val hit5 = HitTestResult<SemanticsModifierNode>()
outerNode.hitTestSemantics(Offset(4.9f, 4.9f), hit5, true)
assertThat(hit5).hasSize(1)
assertThat(hit5[0]).isEqualTo(semanticsModifier1)
// Hit inside layoutNode2
val hit6 = HitTestResult<SemanticsModifierNode>()
outerNode.hitTestSemantics(Offset(6.1f, 6.1f), hit6, true)
assertThat(hit6).hasSize(1)
assertThat(hit6[0]).isEqualTo(semanticsModifier2)
}
@Test
fun hitTestSemantics_pointerInMinimumTouchTarget_closestHitWithOverlap() {
val semanticsConfiguration = SemanticsConfiguration()
val semanticsModifier1 = object : SemanticsModifier {
override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
}
val semanticsModifier2 = object : SemanticsModifier {
override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
}
val layoutNode1 = LayoutNode(0, 0, 5, 5, semanticsModifier1, DpSize(48.dp, 48.dp))
val layoutNode2 = LayoutNode(4, 4, 9, 9, semanticsModifier2, DpSize(48.dp, 48.dp))
val outerNode = LayoutNode(0, 0, 11, 11).apply { attach(MockOwner()) }
outerNode.add(layoutNode1)
outerNode.add(layoutNode2)
layoutNode1.onNodePlaced()
layoutNode2.onNodePlaced()
// Hit layoutNode1
val hit1 = HitTestResult<SemanticsModifierNode>()
outerNode.hitTestSemantics(Offset(3.95f, 3.95f), hit1, true)
assertThat(hit1).hasSize(1)
assertThat(hit1[0].toModifier()).isEqualTo(semanticsModifier1)
// Hit layoutNode2
val hit2 = HitTestResult<SemanticsModifierNode>()
outerNode.hitTestSemantics(Offset(4.05f, 4.05f), hit2, true)
assertThat(hit2).hasSize(1)
assertThat(hit2[0].toModifier()).isEqualTo(semanticsModifier2)
}
@Test
fun hitTest_pointerOutOfBounds_nothingHit() {
val pointerInputFilter: PointerInputFilter = mockPointerInputFilter()
val layoutNode =
LayoutNode(
0, 0, 1, 1,
PointerInputModifierImpl(pointerInputFilter)
).apply {
attach(MockOwner())
}
val hit = mutableListOf<PointerInputModifierNode>()
layoutNode.hitTest(Offset(-1f, -1f), hit)
layoutNode.hitTest(Offset(0f, -1f), hit)
layoutNode.hitTest(Offset(1f, -1f), hit)
layoutNode.hitTest(Offset(-1f, 0f), hit)
// 0, 0 would hit
layoutNode.hitTest(Offset(1f, 0f), hit)
layoutNode.hitTest(Offset(-1f, 1f), hit)
layoutNode.hitTest(Offset(0f, 1f), hit)
layoutNode.hitTest(Offset(1f, 1f), hit)
assertThat(hit).isEmpty()
}
@Test
fun hitTest_pointerOutOfBounds_nothingHit_extendedBounds() {
val pointerInputFilter: PointerInputFilter = mockPointerInputFilter()
val layoutNode =
LayoutNode(
0, 0, 1, 1,
PointerInputModifierImpl(pointerInputFilter),
minimumTouchTargetSize = DpSize(4.dp, 8.dp)
).apply {
attach(MockOwner())
}
val hit = mutableListOf<PointerInputModifierNode>()
layoutNode.hitTest(Offset(-3f, -5f), hit)
layoutNode.hitTest(Offset(0f, -5f), hit)
layoutNode.hitTest(Offset(3f, -5f), hit)
layoutNode.hitTest(Offset(-3f, 0f), hit)
// 0, 0 would hit
layoutNode.hitTest(Offset(3f, 0f), hit)
layoutNode.hitTest(Offset(-3f, 5f), hit)
layoutNode.hitTest(Offset(0f, 5f), hit)
layoutNode.hitTest(Offset(-3f, 5f), hit)
assertThat(hit).isEmpty()
}
@Test
fun hitTest_nestedOffsetNodesHits3_allHitInCorrectOrder() {
hitTest_nestedOffsetNodes_allHitInCorrectOrder(3)
}
@Test
fun hitTest_nestedOffsetNodesHits2_allHitInCorrectOrder() {
hitTest_nestedOffsetNodes_allHitInCorrectOrder(2)
}
@Test
fun hitTest_nestedOffsetNodesHits1_allHitInCorrectOrder() {
hitTest_nestedOffsetNodes_allHitInCorrectOrder(1)
}
private fun hitTest_nestedOffsetNodes_allHitInCorrectOrder(numberOfChildrenHit: Int) {
// Arrange
val childPointerInputFilter: PointerInputFilter = mockPointerInputFilter()
val middlePointerInputFilter: PointerInputFilter = mockPointerInputFilter()
val parentPointerInputFilter: PointerInputFilter = mockPointerInputFilter()
val childLayoutNode =
LayoutNode(
100, 100, 200, 200,
PointerInputModifierImpl(
childPointerInputFilter
)
)
val middleLayoutNode: LayoutNode =
LayoutNode(
100, 100, 400, 400,
PointerInputModifierImpl(
middlePointerInputFilter
)
).apply {
insertAt(0, childLayoutNode)
}
val parentLayoutNode: LayoutNode =
LayoutNode(
0, 0, 500, 500,
PointerInputModifierImpl(
parentPointerInputFilter
)
).apply {
insertAt(0, middleLayoutNode)
attach(MockOwner())
}
middleLayoutNode.onNodePlaced()
childLayoutNode.onNodePlaced()
val offset = when (numberOfChildrenHit) {
3 -> Offset(250f, 250f)
2 -> Offset(150f, 150f)
1 -> Offset(50f, 50f)
else -> throw IllegalStateException()
}
val hit = mutableListOf<PointerInputModifierNode>()
// Act.
parentLayoutNode.hitTest(offset, hit)
// Assert.
when (numberOfChildrenHit) {
3 ->
assertThat(hit.toFilters())
.isEqualTo(
listOf(
parentPointerInputFilter,
middlePointerInputFilter,
childPointerInputFilter
)
)
2 ->
assertThat(hit.toFilters())
.isEqualTo(
listOf(
parentPointerInputFilter,
middlePointerInputFilter
)
)
1 ->
assertThat(hit.toFilters())
.isEqualTo(
listOf(
parentPointerInputFilter
)
)
else -> throw IllegalStateException()
}
}
/**
* This test creates a layout of this shape:
*
* -------------
* | | |
* | t | |
* | | |
* |-----| |
* | |
* | |-----|
* | | |
* | | t |
* | | |
* -------------
*
* Where there is one child in the top right and one in the bottom left, and 2 pointers where
* one in the top left and one in the bottom right.
*/
@Test
fun hitTest_2PointersOver2DifferentPointerInputModifiers_resultIsCorrect() {
// Arrange
val childPointerInputFilter1: PointerInputFilter = mockPointerInputFilter()
val childPointerInputFilter2: PointerInputFilter = mockPointerInputFilter()
val childLayoutNode1 =
LayoutNode(
0, 0, 50, 50,
PointerInputModifierImpl(
childPointerInputFilter1
)
)
val childLayoutNode2 =
LayoutNode(
50, 50, 100, 100,
PointerInputModifierImpl(
childPointerInputFilter2
)
)
val parentLayoutNode = LayoutNode(0, 0, 100, 100).apply {
insertAt(0, childLayoutNode1)
insertAt(1, childLayoutNode2)
attach(MockOwner())
}
childLayoutNode1.onNodePlaced()
childLayoutNode2.onNodePlaced()
val offset1 = Offset(25f, 25f)
val offset2 = Offset(75f, 75f)
val hit1 = mutableListOf<PointerInputModifierNode>()
val hit2 = mutableListOf<PointerInputModifierNode>()
// Act
parentLayoutNode.hitTest(offset1, hit1)
parentLayoutNode.hitTest(offset2, hit2)
// Assert
assertThat(hit1.toFilters()).isEqualTo(listOf(childPointerInputFilter1))
assertThat(hit2.toFilters()).isEqualTo(listOf(childPointerInputFilter2))
}
/**
* This test creates a layout of this shape:
*
* ---------------
* | t | |
* | | |
* | |-------| |
* | | t | |
* | | | |
* | | | |
* |--| |-------|
* | | | t |
* | | | |
* | | | |
* | |--| |
* | | |
* ---------------
*
* There are 3 staggered children and 3 pointers, the first is on child 1, the second is on
* child 2 in a space that overlaps child 1, and the third is in a space in child 3 that
* overlaps child 2.
*/
@Test
fun hitTest_3DownOnOverlappingPointerInputModifiers_resultIsCorrect() {
val childPointerInputFilter1: PointerInputFilter = mockPointerInputFilter()
val childPointerInputFilter2: PointerInputFilter = mockPointerInputFilter()
val childPointerInputFilter3: PointerInputFilter = mockPointerInputFilter()
val childLayoutNode1 =
LayoutNode(
0, 0, 100, 100,
PointerInputModifierImpl(
childPointerInputFilter1
)
)
val childLayoutNode2 =
LayoutNode(
50, 50, 150, 150,
PointerInputModifierImpl(
childPointerInputFilter2
)
)
val childLayoutNode3 =
LayoutNode(
100, 100, 200, 200,
PointerInputModifierImpl(
childPointerInputFilter3
)
)
val parentLayoutNode = LayoutNode(0, 0, 200, 200).apply {
insertAt(0, childLayoutNode1)
insertAt(1, childLayoutNode2)
insertAt(2, childLayoutNode3)
attach(MockOwner())
}
childLayoutNode1.onNodePlaced()
childLayoutNode2.onNodePlaced()
childLayoutNode3.onNodePlaced()
val offset1 = Offset(25f, 25f)
val offset2 = Offset(75f, 75f)
val offset3 = Offset(125f, 125f)
val hit1 = mutableListOf<PointerInputModifierNode>()
val hit2 = mutableListOf<PointerInputModifierNode>()
val hit3 = mutableListOf<PointerInputModifierNode>()
parentLayoutNode.hitTest(offset1, hit1)
parentLayoutNode.hitTest(offset2, hit2)
parentLayoutNode.hitTest(offset3, hit3)
assertThat(hit1.toFilters()).isEqualTo(listOf(childPointerInputFilter1))
assertThat(hit2.toFilters()).isEqualTo(listOf(childPointerInputFilter2))
assertThat(hit3.toFilters()).isEqualTo(listOf(childPointerInputFilter3))
}
/**
* This test creates a layout of this shape:
*
* ---------------
* | |
* | t |
* | |
* | |-------| |
* | | | |
* | | t | |
* | | | |
* | |-------| |
* | |
* | t |
* | |
* ---------------
*
* There are 2 children with one over the other and 3 pointers: the first is on background
* child, the second is on the foreground child, and the third is again on the background child.
*/
@Test
fun hitTest_3DownOnFloatingPointerInputModifierV_resultIsCorrect() {
val childPointerInputFilter1: PointerInputFilter = mockPointerInputFilter()
val childPointerInputFilter2: PointerInputFilter = mockPointerInputFilter()
val childLayoutNode1 = LayoutNode(
0, 0, 100, 150,
PointerInputModifierImpl(
childPointerInputFilter1
)
)
val childLayoutNode2 = LayoutNode(
25, 50, 75, 100,
PointerInputModifierImpl(
childPointerInputFilter2
)
)
val parentLayoutNode = LayoutNode(0, 0, 150, 150).apply {
insertAt(0, childLayoutNode1)
insertAt(1, childLayoutNode2)
attach(MockOwner())
}
childLayoutNode1.onNodePlaced()
childLayoutNode2.onNodePlaced()
val offset1 = Offset(50f, 25f)
val offset2 = Offset(50f, 75f)
val offset3 = Offset(50f, 125f)
val hit1 = mutableListOf<PointerInputModifierNode>()
val hit2 = mutableListOf<PointerInputModifierNode>()
val hit3 = mutableListOf<PointerInputModifierNode>()
// Act
parentLayoutNode.hitTest(offset1, hit1)
parentLayoutNode.hitTest(offset2, hit2)
parentLayoutNode.hitTest(offset3, hit3)
// Assert
assertThat(hit1.toFilters()).isEqualTo(listOf(childPointerInputFilter1))
assertThat(hit2.toFilters()).isEqualTo(listOf(childPointerInputFilter2))
assertThat(hit3.toFilters()).isEqualTo(listOf(childPointerInputFilter1))
}
/**
* This test creates a layout of this shape:
*
* -----------------
* | |
* | |-------| |
* | | | |
* | t | t | t |
* | | | |
* | |-------| |
* | |
* -----------------
*
* There are 2 children with one over the other and 3 pointers: the first is on background
* child, the second is on the foreground child, and the third is again on the background child.
*/
@Test
fun hitTest_3DownOnFloatingPointerInputModifierH_resultIsCorrect() {
val childPointerInputFilter1: PointerInputFilter = mockPointerInputFilter()
val childPointerInputFilter2: PointerInputFilter = mockPointerInputFilter()
val childLayoutNode1 = LayoutNode(
0, 0, 150, 100,
PointerInputModifierImpl(
childPointerInputFilter1
)
)
val childLayoutNode2 = LayoutNode(
50, 25, 100, 75,
PointerInputModifierImpl(
childPointerInputFilter2
)
)
val parentLayoutNode = LayoutNode(0, 0, 150, 150).apply {
insertAt(0, childLayoutNode1)
insertAt(1, childLayoutNode2)
attach(MockOwner())
}
childLayoutNode2.onNodePlaced()
childLayoutNode1.onNodePlaced()
val offset1 = Offset(25f, 50f)
val offset2 = Offset(75f, 50f)
val offset3 = Offset(125f, 50f)
val hit1 = mutableListOf<PointerInputModifierNode>()
val hit2 = mutableListOf<PointerInputModifierNode>()
val hit3 = mutableListOf<PointerInputModifierNode>()
// Act
parentLayoutNode.hitTest(offset1, hit1)
parentLayoutNode.hitTest(offset2, hit2)
parentLayoutNode.hitTest(offset3, hit3)
// Assert
assertThat(hit1.toFilters()).isEqualTo(listOf(childPointerInputFilter1))
assertThat(hit2.toFilters()).isEqualTo(listOf(childPointerInputFilter2))
assertThat(hit3.toFilters()).isEqualTo(listOf(childPointerInputFilter1))
}
/**
* This test creates a layout of this shape:
* 0 1 2 3 4
* ......... .........
* 0 . t . . t .
* . |---|---|---| .
* 1 . t | t | | t | t .
* ....|---| |---|....
* 2 | |
* ....|---| |---|....
* 3 . t | t | | t | t .
* . |---|---|---| .
* 4 . t . . t .
* ......... .........
*
* 4 LayoutNodes with PointerInputModifiers that are clipped by their parent LayoutNode. 4
* touches touch just inside the parent LayoutNode and inside the child LayoutNodes. 8
* touches touch just outside the parent LayoutNode but inside the child LayoutNodes.
*
* Because layout node bounds are not used to clip pointer input hit testing, all pointers
* should hit.
*/
@Test
fun hitTest_4DownInClippedAreaOfLnsWithPims_resultIsCorrect() {
// Arrange
val pointerInputFilter1: PointerInputFilter = mockPointerInputFilter()
val pointerInputFilter2: PointerInputFilter = mockPointerInputFilter()
val pointerInputFilter3: PointerInputFilter = mockPointerInputFilter()
val pointerInputFilter4: PointerInputFilter = mockPointerInputFilter()
val layoutNode1 = LayoutNode(
-1, -1, 1, 1,
PointerInputModifierImpl(
pointerInputFilter1
)
)
val layoutNode2 = LayoutNode(
2, -1, 4, 1,
PointerInputModifierImpl(
pointerInputFilter2
)
)
val layoutNode3 = LayoutNode(
-1, 2, 1, 4,
PointerInputModifierImpl(
pointerInputFilter3
)
)
val layoutNode4 = LayoutNode(
2, 2, 4, 4,
PointerInputModifierImpl(
pointerInputFilter4
)
)
val parentLayoutNode = LayoutNode(1, 1, 4, 4).apply {
insertAt(0, layoutNode1)
insertAt(1, layoutNode2)
insertAt(2, layoutNode3)
insertAt(3, layoutNode4)
attach(MockOwner())
}
layoutNode1.onNodePlaced()
layoutNode2.onNodePlaced()
layoutNode3.onNodePlaced()
layoutNode4.onNodePlaced()
val offsetsThatHit1 =
listOf(
Offset(0f, 1f),
Offset(1f, 0f),
Offset(1f, 1f)
)
val offsetsThatHit2 =
listOf(
Offset(3f, 0f),
Offset(3f, 1f),
Offset(4f, 1f)
)
val offsetsThatHit3 =
listOf(
Offset(0f, 3f),
Offset(1f, 3f),
Offset(1f, 4f)
)
val offsetsThatHit4 =
listOf(
Offset(3f, 3f),
Offset(3f, 4f),
Offset(4f, 3f)
)
val hit = mutableListOf<PointerInputModifierNode>()
// Act and Assert
offsetsThatHit1.forEach {
hit.clear()
parentLayoutNode.hitTest(it, hit)
assertThat(hit.toFilters()).isEqualTo(listOf(pointerInputFilter1))
}
offsetsThatHit2.forEach {
hit.clear()
parentLayoutNode.hitTest(it, hit)
assertThat(hit.toFilters()).isEqualTo(listOf(pointerInputFilter2))
}
offsetsThatHit3.forEach {
hit.clear()
parentLayoutNode.hitTest(it, hit)
assertThat(hit.toFilters()).isEqualTo(listOf(pointerInputFilter3))
}
offsetsThatHit4.forEach {
hit.clear()
parentLayoutNode.hitTest(it, hit)
assertThat(hit.toFilters()).isEqualTo(listOf(pointerInputFilter4))
}
}
@Test
fun hitTest_pointerOn3NestedPointerInputModifiers_allPimsHitInCorrectOrder() {
// Arrange.
val pointerInputFilter1: PointerInputFilter = mockPointerInputFilter()
val pointerInputFilter2: PointerInputFilter = mockPointerInputFilter()
val pointerInputFilter3: PointerInputFilter = mockPointerInputFilter()
val modifier =
PointerInputModifierImpl(
pointerInputFilter1
) then PointerInputModifierImpl(
pointerInputFilter2
) then PointerInputModifierImpl(
pointerInputFilter3
)
val layoutNode = LayoutNode(
25, 50, 75, 100,
modifier
).apply {
attach(MockOwner())
}
val offset1 = Offset(50f, 75f)
val hit = mutableListOf<PointerInputModifierNode>()
// Act.
layoutNode.hitTest(offset1, hit)
// Assert.
assertThat(hit.toFilters()).isEqualTo(
listOf(
pointerInputFilter1,
pointerInputFilter2,
pointerInputFilter3
)
)
}
@Test
fun hitTest_pointerOnDeeplyNestedPointerInputModifier_pimIsHit() {
// Arrange.
val pointerInputFilter: PointerInputFilter = mockPointerInputFilter()
val layoutNode1 =
LayoutNode(
1, 5, 500, 500,
PointerInputModifierImpl(
pointerInputFilter
)
)
val layoutNode2: LayoutNode = LayoutNode(2, 6, 500, 500).apply {
insertAt(0, layoutNode1)
}
val layoutNode3: LayoutNode = LayoutNode(3, 7, 500, 500).apply {
insertAt(0, layoutNode2)
}
val layoutNode4: LayoutNode = LayoutNode(4, 8, 500, 500).apply {
insertAt(0, layoutNode3)
}.apply {
attach(MockOwner())
}
layoutNode3.onNodePlaced()
layoutNode2.onNodePlaced()
layoutNode1.onNodePlaced()
val offset1 = Offset(499f, 499f)
val hit = mutableListOf<PointerInputModifierNode>()
// Act.
layoutNode4.hitTest(offset1, hit)
// Assert.
assertThat(hit.toFilters()).isEqualTo(listOf(pointerInputFilter))
}
@Test
fun hitTest_pointerOnComplexPointerAndLayoutNodePath_pimsHitInCorrectOrder() {
// Arrange.
val pointerInputFilter1: PointerInputFilter = mockPointerInputFilter()
val pointerInputFilter2: PointerInputFilter = mockPointerInputFilter()
val pointerInputFilter3: PointerInputFilter = mockPointerInputFilter()
val pointerInputFilter4: PointerInputFilter = mockPointerInputFilter()
val layoutNode1 = LayoutNode(
1, 6, 500, 500,
PointerInputModifierImpl(
pointerInputFilter1
) then PointerInputModifierImpl(
pointerInputFilter2
)
)
val layoutNode2: LayoutNode = LayoutNode(2, 7, 500, 500).apply {
insertAt(0, layoutNode1)
}
val layoutNode3 =
LayoutNode(
3, 8, 500, 500,
PointerInputModifierImpl(
pointerInputFilter3
) then PointerInputModifierImpl(
pointerInputFilter4
)
).apply {
insertAt(0, layoutNode2)
}
val layoutNode4: LayoutNode = LayoutNode(4, 9, 500, 500).apply {
insertAt(0, layoutNode3)
}
val layoutNode5: LayoutNode = LayoutNode(5, 10, 500, 500).apply {
insertAt(0, layoutNode4)
}.apply {
attach(MockOwner())
}
layoutNode4.onNodePlaced()
layoutNode3.onNodePlaced()
layoutNode2.onNodePlaced()
layoutNode1.onNodePlaced()
val offset1 = Offset(499f, 499f)
val hit = mutableListOf<PointerInputModifierNode>()
// Act.
layoutNode5.hitTest(offset1, hit)
// Assert.
assertThat(hit.toFilters()).isEqualTo(
listOf(
pointerInputFilter3,
pointerInputFilter4,
pointerInputFilter1,
pointerInputFilter2
)
)
}
@Test
fun hitTest_pointerOnFullyOverlappingPointerInputModifiers_onlyTopPimIsHit() {
val pointerInputFilter1: PointerInputFilter = mockPointerInputFilter()
val pointerInputFilter2: PointerInputFilter = mockPointerInputFilter()
val layoutNode1 = LayoutNode(
0, 0, 100, 100,
PointerInputModifierImpl(
pointerInputFilter1
)
)
val layoutNode2 = LayoutNode(
0, 0, 100, 100,
PointerInputModifierImpl(
pointerInputFilter2
)
)
val parentLayoutNode = LayoutNode(0, 0, 100, 100).apply {
insertAt(0, layoutNode1)
insertAt(1, layoutNode2)
attach(MockOwner())
}
layoutNode1.onNodePlaced()
layoutNode2.onNodePlaced()
val offset = Offset(50f, 50f)
val hit = mutableListOf<PointerInputModifierNode>()
// Act.
parentLayoutNode.hitTest(offset, hit)
// Assert.
assertThat(hit.toFilters()).isEqualTo(listOf(pointerInputFilter2))
}
@Test
fun hitTest_pointerOnPointerInputModifierInLayoutNodeWithNoSize_nothingHit() {
val pointerInputFilter: PointerInputFilter = mockPointerInputFilter()
val layoutNode = LayoutNode(
0, 0, 0, 0,
PointerInputModifierImpl(
pointerInputFilter
)
).apply {
attach(MockOwner())
}
val offset = Offset.Zero
val hit = mutableListOf<PointerInputModifierNode>()
// Act.
layoutNode.hitTest(offset, hit)
// Assert.
assertThat(hit.toFilters()).isEmpty()
}
@Test
fun hitTest_zIndexIsAccounted() {
val pointerInputFilter1: PointerInputFilter = mockPointerInputFilter()
val pointerInputFilter2: PointerInputFilter = mockPointerInputFilter()
val parent = LayoutNode(
0, 0, 2, 2
).apply {
attach(
MockOwner().apply {
measureIteration = 1L
}
)
}
parent.insertAt(
0,
LayoutNode(
0, 0, 2, 2,
PointerInputModifierImpl(
pointerInputFilter1
).zIndex(1f)
)
)
parent.insertAt(
1,
LayoutNode(
0, 0, 2, 2,
PointerInputModifierImpl(
pointerInputFilter2
)
)
)
parent.remeasure()
parent.replace()
val hit = mutableListOf<PointerInputModifierNode>()
// Act.
parent.hitTest(Offset(1f, 1f), hit)
// Assert.
assertThat(hit.toFilters()).isEqualTo(listOf(pointerInputFilter1))
}
@Test
fun onRequestMeasureIsNotCalledOnDetachedNodes() {
val root = LayoutNode()
val node1 = LayoutNode()
root.add(node1)
val node2 = LayoutNode()
node1.add(node2)
val owner = MockOwner()
root.attach(owner)
owner.onAttachParams.clear()
owner.onRequestMeasureParams.clear()
// Dispose
root.removeAt(0, 1)
assertFalse(node1.isAttached)
assertFalse(node2.isAttached)
assertEquals(0, owner.onRequestMeasureParams.count { it === node1 })
assertEquals(0, owner.onRequestMeasureParams.count { it === node2 })
}
@Test
fun modifierMatchesWrapperWithIdentity() {
val modifier1 = Modifier.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
val modifier2 = Modifier.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
placeable.place(1, 1)
}
}
val root = LayoutNode()
root.modifier = modifier1.then(modifier2)
val wrapper1 = root.outerCoordinator
val wrapper2 = root.outerCoordinator.wrapped
assertEquals(
modifier1,
(wrapper1 as LayoutModifierNodeCoordinator).layoutModifierNode.toModifier()
)
assertEquals(
modifier2,
(wrapper2 as LayoutModifierNodeCoordinator).layoutModifierNode.toModifier()
)
root.modifier = modifier2.then(modifier1)
assertEquals(
modifier1,
(root.outerCoordinator.wrapped as LayoutModifierNodeCoordinator)
.layoutModifierNode
.toModifier()
)
assertEquals(
modifier2,
(root.outerCoordinator as LayoutModifierNodeCoordinator).layoutModifierNode.toModifier()
)
}
@Test
fun measureResultAndPositionChangesCallOnLayoutChange() {
val node = LayoutNode(20, 20, 100, 100)
val owner = MockOwner()
node.attach(owner)
node.innerCoordinator.measureResult = object : MeasureResult {
override val width = 50
override val height = 50
override val alignmentLines: Map<AlignmentLine, Int> get() = mapOf()
override fun placeChildren() {}
}
assertEquals(1, owner.layoutChangeCount)
node.place(0, 0)
assertEquals(2, owner.layoutChangeCount)
}
@Test
fun layerParamChangeCallsOnLayoutChange() {
val node = LayoutNode(20, 20, 100, 100, Modifier.graphicsLayer())
val owner = MockOwner()
node.attach(owner)
assertEquals(0, owner.layoutChangeCount)
node.innerCoordinator.onLayerBlockUpdated { scaleX = 0.5f }
assertEquals(1, owner.layoutChangeCount)
repeat(2) {
node.innerCoordinator.onLayerBlockUpdated { scaleX = 1f }
}
assertEquals(2, owner.layoutChangeCount)
node.innerCoordinator.onLayerBlockUpdated(null)
assertEquals(3, owner.layoutChangeCount)
}
@Test
fun reuseModifiersThatImplementMultipleModifierInterfaces() {
val drawAndLayoutModifier: Modifier = object : DrawModifier, LayoutModifier {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val placeable = measurable.measure(constraints)
return layout(placeable.width, placeable.height) {
placeable.placeRelative(IntOffset.Zero)
}
}
override fun ContentDrawScope.draw() {
drawContent()
}
}
val a = Modifier.then(EmptyLayoutModifier()).then(drawAndLayoutModifier)
val b = Modifier.then(EmptyLayoutModifier()).then(drawAndLayoutModifier)
val node = LayoutNode(20, 20, 100, 100)
val owner = MockOwner()
node.attach(owner)
node.modifier = a
assertEquals(2, node.getModifierInfo().size)
node.modifier = b
assertEquals(2, node.getModifierInfo().size)
}
@Test
fun nodeCoordinator_alpha() {
val root = LayoutNode().apply { this.modifier = Modifier.drawBehind {} }
val layoutNode1 = LayoutNode().apply {
this.modifier = Modifier.graphicsLayer { }.graphicsLayer { }.drawBehind {}
}
val layoutNode2 = LayoutNode().apply { this.modifier = Modifier.drawBehind {} }
val owner = MockOwner()
root.insertAt(0, layoutNode1)
layoutNode1.insertAt(0, layoutNode2)
root.attach(owner)
// provide alpha to the graphics layer
layoutNode1.outerCoordinator.wrapped!!.onLayerBlockUpdated {
alpha = 0f
}
layoutNode1.outerCoordinator.wrapped!!.wrapped!!.onLayerBlockUpdated {
alpha = 0.5f
}
assertFalse(layoutNode1.outerCoordinator.isTransparent())
assertTrue(layoutNode1.innerCoordinator.isTransparent())
assertTrue(layoutNode2.outerCoordinator.isTransparent())
assertTrue(layoutNode2.innerCoordinator.isTransparent())
}
private fun createSimpleLayout(): Triple<LayoutNode, LayoutNode, LayoutNode> {
val layoutNode = ZeroSizedLayoutNode()
val child1 = ZeroSizedLayoutNode()
val child2 = ZeroSizedLayoutNode()
layoutNode.insertAt(0, child1)
layoutNode.insertAt(1, child2)
return Triple(layoutNode, child1, child2)
}
private fun ZeroSizedLayoutNode() = LayoutNode(0, 0, 0, 0)
private class PointerInputModifierImpl(override val pointerInputFilter: PointerInputFilter) :
PointerInputModifier
}
private class EmptyLayoutModifier : LayoutModifier {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val placeable = measurable.measure(constraints)
return layout(placeable.width, placeable.height) {
placeable.placeRelative(IntOffset.Zero)
}
}
}
@OptIn(InternalCoreApi::class)
internal class MockOwner(
val position: IntOffset = IntOffset.Zero,
override val root: LayoutNode = LayoutNode()
) : Owner {
val onRequestMeasureParams = mutableListOf<LayoutNode>()
val onAttachParams = mutableListOf<LayoutNode>()
val onDetachParams = mutableListOf<LayoutNode>()
var layoutChangeCount = 0
override val rootForTest: RootForTest
get() = TODO("Not yet implemented")
override val hapticFeedBack: HapticFeedback
get() = TODO("Not yet implemented")
override val inputModeManager: InputModeManager
get() = TODO("Not yet implemented")
override val clipboardManager: ClipboardManager
get() = TODO("Not yet implemented")
override val accessibilityManager: AccessibilityManager
get() = TODO("Not yet implemented")
override val textToolbar: TextToolbar
get() = TODO("Not yet implemented")
@OptIn(ExperimentalComposeUiApi::class)
override val autofillTree: AutofillTree
get() = TODO("Not yet implemented")
@OptIn(ExperimentalComposeUiApi::class)
override val autofill: Autofill?
get() = TODO("Not yet implemented")
override val density: Density
get() = Density(1f)
override val textInputService: TextInputService
get() = TODO("Not yet implemented")
override val pointerIconService: PointerIconService
get() = TODO("Not yet implemented")
override val focusOwner: FocusOwner
get() = TODO("Not yet implemented")
override val windowInfo: WindowInfo
get() = TODO("Not yet implemented")
@Deprecated(
"fontLoader is deprecated, use fontFamilyResolver",
replaceWith = ReplaceWith("fontFamilyResolver")
)
@Suppress("DEPRECATION")
override val fontLoader: Font.ResourceLoader
get() = TODO("Not yet implemented")
override val fontFamilyResolver: FontFamily.Resolver
get() = TODO("Not yet implemented")
override val layoutDirection: LayoutDirection
get() = LayoutDirection.Ltr
override var showLayoutBounds: Boolean = false
override val snapshotObserver = OwnerSnapshotObserver { it.invoke() }
override val modifierLocalManager: ModifierLocalManager = ModifierLocalManager(this)
override fun onRequestMeasure(
layoutNode: LayoutNode,
affectsLookahead: Boolean,
forceRequest: Boolean
) {
onRequestMeasureParams += layoutNode
if (affectsLookahead) {
layoutNode.markLookaheadMeasurePending()
}
layoutNode.markMeasurePending()
}
override fun onRequestRelayout(
layoutNode: LayoutNode,
affectsLookahead: Boolean,
forceRequest: Boolean
) {
if (affectsLookahead) {
layoutNode.markLookaheadLayoutPending()
}
layoutNode.markLayoutPending()
}
override fun requestOnPositionedCallback(layoutNode: LayoutNode) {
}
override fun onAttach(node: LayoutNode) {
onAttachParams += node
}
override fun onDetach(node: LayoutNode) {
onDetachParams += node
}
override fun calculatePositionInWindow(localPosition: Offset): Offset =
localPosition + position.toOffset()
override fun calculateLocalPosition(positionInWindow: Offset): Offset =
positionInWindow - position.toOffset()
override fun requestFocus(): Boolean = false
override fun measureAndLayout(sendPointerUpdate: Boolean) {
}
override fun measureAndLayout(layoutNode: LayoutNode, constraints: Constraints) {
}
override fun forceMeasureTheSubtree(layoutNode: LayoutNode) {
}
override fun registerOnEndApplyChangesListener(listener: () -> Unit) {
listener()
}
override fun onEndApplyChanges() {
}
override fun registerOnLayoutCompletedListener(listener: Owner.OnLayoutCompletedListener) {
TODO("Not yet implemented")
}
override fun createLayer(
drawBlock: (Canvas) -> Unit,
invalidateParentLayer: () -> Unit
): OwnedLayer {
val transform = Matrix()
val inverseTransform = Matrix()
return object : OwnedLayer {
override fun updateLayerProperties(
scaleX: Float,
scaleY: Float,
alpha: Float,
translationX: Float,
translationY: Float,
shadowElevation: Float,
rotationX: Float,
rotationY: Float,
rotationZ: Float,
cameraDistance: Float,
transformOrigin: TransformOrigin,
shape: Shape,
clip: Boolean,
renderEffect: RenderEffect?,
ambientShadowColor: Color,
spotShadowColor: Color,
compositingStrategy: CompositingStrategy,
layoutDirection: LayoutDirection,
density: Density
) {
transform.reset()
// This is not expected to be 100% accurate
transform.scale(scaleX, scaleY)
transform.rotateZ(rotationZ)
transform.translate(translationX, translationY)
transform.invertTo(inverseTransform)
}
override fun isInLayer(position: Offset) = true
override fun move(position: IntOffset) {
}
override fun resize(size: IntSize) {
}
override fun drawLayer(canvas: Canvas) {
drawBlock(canvas)
}
override fun updateDisplayList() {
}
override fun invalidate() {
}
override fun destroy() {
}
override fun mapBounds(rect: MutableRect, inverse: Boolean) {
}
override fun reuseLayer(
drawBlock: (Canvas) -> Unit,
invalidateParentLayer: () -> Unit
) {
}
override fun transform(matrix: Matrix) {
matrix.timesAssign(transform)
}
override fun inverseTransform(matrix: Matrix) {
matrix.timesAssign(inverseTransform)
}
override fun mapOffset(point: Offset, inverse: Boolean) = point
}
}
override fun onSemanticsChange() {
}
override fun onLayoutChange(layoutNode: LayoutNode) {
layoutChangeCount++
}
override fun getFocusDirection(keyEvent: KeyEvent): FocusDirection? {
TODO("Not yet implemented")
}
override var measureIteration: Long = 0
override val viewConfiguration: ViewConfiguration
get() = TODO("Not yet implemented")
override val sharedDrawScope = LayoutNodeDrawScope()
}
@OptIn(ExperimentalComposeUiApi::class)
private fun LayoutNode.hitTest(
pointerPosition: Offset,
hitPointerInputFilters: MutableList<PointerInputModifierNode>,
isTouchEvent: Boolean = false
) {
val hitTestResult = HitTestResult<PointerInputModifierNode>()
hitTest(pointerPosition, hitTestResult, isTouchEvent)
hitPointerInputFilters.addAll(hitTestResult)
}
internal fun LayoutNode(
x: Int,
y: Int,
x2: Int,
y2: Int,
modifier: Modifier = Modifier,
minimumTouchTargetSize: DpSize = DpSize.Zero
) = LayoutNode().apply {
this.viewConfiguration = TestViewConfiguration(minimumTouchTargetSize = minimumTouchTargetSize)
this.modifier = modifier
measurePolicy = object : LayoutNode.NoIntrinsicsMeasurePolicy("not supported") {
override fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints
): MeasureResult =
layout(x2 - x, y2 - y) {
measurables.forEach { it.measure(constraints).place(0, 0) }
}
}
attach(MockOwner())
markMeasurePending()
remeasure(Constraints())
var wrapper: NodeCoordinator? = outerCoordinator
while (wrapper != null) {
wrapper.measureResult = innerCoordinator.measureResult
wrapper = (wrapper as? NodeCoordinator)?.wrapped
}
place(x, y)
detach()
}
private fun mockPointerInputFilter(
interceptChildEvents: Boolean = false
): PointerInputFilter = object : PointerInputFilter() {
override fun onPointerEvent(
pointerEvent: PointerEvent,
pass: PointerEventPass,
bounds: IntSize
) {
}
override fun onCancel() {
}
override val interceptOutOfBoundsChildEvents: Boolean
get() = interceptChildEvents
}
// This returns the corresponding modifier that produced the PointerInputNode. This is only
// possible for PointerInputNodes that are BackwardsCompatNodes and once we refactor the
// pointerInput modifier to use Modifier.Nodes directly, the tests that use this should be rewritten
@OptIn(ExperimentalComposeUiApi::class)
fun PointerInputModifierNode.toFilter(): PointerInputFilter {
val node = this as? BackwardsCompatNode
?: error("Incorrectly assumed PointerInputNode was a BackwardsCompatNode")
val modifier = node.element as? PointerInputModifier
?: error("Incorrectly assumed Modifier.Element was a PointerInputModifier")
return modifier.pointerInputFilter
}
@OptIn(ExperimentalComposeUiApi::class)
fun List<PointerInputModifierNode>.toFilters(): List<PointerInputFilter> = map { it.toFilter() }
// This returns the corresponding modifier that produced the Node. This is only possible for
// Nodes that are BackwardsCompatNodes and once we refactor semantics / pointer input to use
// Modifier.Nodes directly, the tests that use this should be rewritten
@OptIn(ExperimentalComposeUiApi::class)
fun DelegatableNode.toModifier(): Modifier.Element {
val node = node as? BackwardsCompatNode
?: error("Incorrectly assumed Modifier.Node was a BackwardsCompatNode")
return node.element
}