[go: nahoru, domu]

blob: e71c2e26e8f76d5f68eeb5e5c3ca8830f3f37a90 [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.ui.core
import androidx.compose.Emittable
import androidx.ui.core.semantics.SemanticsConfiguration
import androidx.ui.graphics.Canvas
import androidx.ui.engine.geometry.Shape
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
/**
* Owner implements the connection to the underlying view system. On Android, this connects
* to Android [android.view.View]s and all layout, draw, input, and accessibility is hooked
* through them.
*/
interface Owner {
val density: Density
/**
* Called from a [DrawNode], this registers with the underlying view system that a
* redraw of the given [drawNode] is required. It may cause other nodes to redraw, if
* necessary.
*/
fun onInvalidate(drawNode: DrawNode)
/**
* Called by [LayoutNode] to indicate the new size of [layoutNode].
* The owner may need to track updated layouts.
*/
fun onSizeChange(layoutNode: LayoutNode)
/**
* Called by [LayoutNode] to indicate the new position of [layoutNode].
* The owner may need to track updated layouts.
*/
fun onPositionChange(layoutNode: LayoutNode)
/**
* Called by [LayoutNode] to request the Owner a new measurement+layout.
*/
fun onRequestMeasure(layoutNode: LayoutNode)
/**
* Called by [ComponentNode] when it is attached to the view system and now has an owner.
* This is used by [Owner] to update [ComponentNode.ownerData] and track which nodes are
* associated with it. It will only be called when [node] is not already attached to an
* owner.
*/
fun onAttach(node: ComponentNode)
/**
* Called by [ComponentNode] when it is detached from the view system, such as during
* [ComponentNode.emitRemoveAt]. This will only be called for [node]s that are already
* [ComponentNode.attach]ed.
*/
fun onDetach(node: ComponentNode)
/**
* Called when measure starts.
*/
fun onStartMeasure(layoutNode: LayoutNode)
/**
* Called when measure ends.
*/
fun onEndMeasure(layoutNode: LayoutNode)
/**
* Called when layout (placement) starts.
*/
fun onStartLayout(layoutNode: LayoutNode)
/**
* Called when layout (placement) ends.
*/
fun onEndLayout(layoutNode: LayoutNode)
/**
* Returns a position of the owner in its window.
*/
fun calculatePosition(): PxPosition
/**
* Called when some params of [RepaintBoundaryNode] are updated.
* This is not causing re-recording of the RepaintBoundary, but updates params
* like outline, clipping, elevation or alpha.
*/
fun onRepaintBoundaryParamsChange(repaintBoundaryNode: RepaintBoundaryNode)
val measureIteration: Long
}
/**
* The base type for all nodes from the tree generated from a component hierarchy.
*
* Specific components are backed by a tree of nodes: Draw, Layout, SemanticsComponentNode, GestureDetector.
* All other components are not represented in the backing hierarchy.
*/
sealed class ComponentNode : Emittable {
internal val children = mutableListOf<ComponentNode>()
/**
* The parent node in the ComponentNode hierarchy. This is `null` when the `ComponentNode`
* is attached (has an [owner]) and is the root of the tree or has not had [add] called for it.
*/
var parent: ComponentNode? = null
private set
/**
* The view system [Owner]. This `null` until [attach] is called
*/
var owner: Owner? = null
private set
/**
* The tree depth of the ComponentNode. This is valid only when [isAttached] is true.
*/
var depth: Int = 0
/**
* An opaque value set by the [Owner]. It is `null` when [isAttached] is false, but
* may also be `null` when [isAttached] is true, depending on the needs of the Owner.
*/
var ownerData: Any? = null
/**
* Returns the number of children in this ComponentNode.
*/
val count: Int
get() = children.size
/**
* This is the LayoutNode ancestor that contains this LayoutNode. This will be `null` for the
* root [LayoutNode].
*/
open val parentLayoutNode: LayoutNode?
get() = containingLayoutNode
/**
* Protected method to find the parent's layout node. LayoutNode returns itself, but
* all other ComponentNodes return the parent's `containingLayoutNode`.
*/
protected open val containingLayoutNode: LayoutNode?
get() = parent?.containingLayoutNode
/**
* If this is a [RepaintBoundaryNode], `this` is returned, otherwise the nearest ancestor
* `RepaintBoundaryNode` or `null` if there are no ancestor `RepaintBoundaryNode`s.
*/
open val repaintBoundary: RepaintBoundaryNode? get() = parent?.repaintBoundary
/**
* Execute [block] on all children of this ComponentNode.
*/
inline fun visitChildren(block: (ComponentNode) -> Unit) {
for (i in 0 until count) {
block(this[i])
}
}
/**
* Execute [block] on all children of this ComponentNode in reverse order.
*/
inline fun visitChildrenReverse(block: (ComponentNode) -> Unit) {
for (i in count - 1 downTo 0) {
block(this[i])
}
}
/**
* Inserts a child [ComponentNode] at a particular index. If this ComponentNode [isAttached]
* then [instance] will become [attach]ed also. [instance] must have a `null` [parent].
*/
override fun emitInsertAt(index: Int, instance: Emittable) {
if (instance !is ComponentNode) {
ErrorMessages.OnlyComponents.state()
}
ErrorMessages.ComponentNodeHasParent.validateState(instance.parent == null)
ErrorMessages.OwnerAlreadyAttached.validateState(instance.owner == null)
instance.parent = this
children.add(index, instance)
val owner = this.owner
if (owner != null) {
instance.attach(owner)
}
}
/**
* Removes one or more children, starting at [index].
*/
override fun emitRemoveAt(index: Int, count: Int) {
ErrorMessages.CountOutOfRange.validateArg(count >= 0, count)
val attached = owner != null
for (i in index + count - 1 downTo index) {
val child = children.removeAt(i)
if (attached) {
child.detach()
}
child.parent = null
}
}
override fun emitMove(from: Int, to: Int, count: Int) {
if (from == to) {
return // nothing to do
}
for (i in 0 until count) {
// if "from" is after "to," the from index moves because we're inserting before it
val fromIndex = if (from > to) from + i else from
val toIndex = if (from > to) to + i else to + count - 2
val child = children.removeAt(fromIndex)
children.add(toIndex, child)
}
}
/**
* Returns the child ComponentNode at the given index. An exception will be thrown if there
* is no child at the given index.
*/
operator fun get(index: Int): ComponentNode = children[index]
/**
* Set the [Owner] of this ComponentNode. This ComponentNode must not already be attached.
* [owner] must match its [parent].[owner].
*/
open fun attach(owner: Owner) {
ErrorMessages.OwnerAlreadyAttached.validateState(this.owner == null)
val parent = parent
ErrorMessages.ParentOwnerMustMatchChild.validateState(
parent == null ||
parent.owner == owner
)
this.owner = owner
this.depth = (parent?.depth ?: -1) + 1
owner.onAttach(this)
visitChildren { child ->
child.attach(owner)
}
}
/**
* Remove the ComponentNode from the [Owner]. The [owner] must not be `null` before this call
* and its [parent]'s [owner] must be `null` before calling this. This will also [detach] all
* children. After executing, the [owner] will be `null`.
*/
open fun detach() {
visitChildren { child ->
child.detach()
}
val owner = owner ?: ErrorMessages.OwnerAlreadyDetached.state()
owner.onDetach(this)
this.owner = null
depth = 0
}
}
/**
* Returns true if this [ComponentNode] currently has an [ComponentNode.owner]. Semantically,
* this means that the ComponentNode is currently a part of a component tree.
*/
fun ComponentNode.isAttached() = owner != null
class RepaintBoundaryNode(val name: String?) : ComponentNode() {
/**
* The shape used to calculate an outline of the RepaintBoundary.
*/
var shape: Shape? = null
set(value) {
if (field != value) {
field = value
owner?.onRepaintBoundaryParamsChange(this)
}
}
/**
* If true RepaintBoundary will be clipped by the outline of it's [shape]
*/
var clipToShape: Boolean = false
set(value) {
if (field != value) {
field = value
owner?.onRepaintBoundaryParamsChange(this)
}
}
/**
* The z-coordinate at which to place this physical object.
*/
var elevation: Dp = 0.dp
set(value) {
if (field != value) {
field = value
owner?.onRepaintBoundaryParamsChange(this)
}
}
/**
* The fraction of children's alpha value.
*/
var opacity: Float = 1f
set(value) {
if (field != value) {
require(value in 0f..1f) { "Opacity should be within [0, 1] range" }
field = value
owner?.onRepaintBoundaryParamsChange(this)
}
}
override val repaintBoundary: RepaintBoundaryNode? get() = this
}
/**
* Backing node for handling pointer events.
*/
class PointerInputNode : ComponentNode() {
var pointerInputHandler: PointerInputHandler = { event, _ -> event }
}
/**
* Backing node for the Draw component.
*/
class DrawNode : ComponentNode() {
var onPaintWithChildren: (DrawReceiver.(canvas: Canvas, parentSize: PxSize) -> Unit)? = null
set(value) {
field = value
invalidate()
}
var onPaint: (DensityScope.(canvas: Canvas, parentSize: PxSize) -> Unit)? = null
set(value) {
field = value
invalidate()
}
var needsPaint = false
override fun attach(owner: Owner) {
super.attach(owner)
needsPaint = true
owner.onInvalidate(this)
}
override fun detach() {
invalidate()
super.detach()
needsPaint = false
}
fun invalidate() {
if (!needsPaint) {
needsPaint = true
owner?.onInvalidate(this)
}
}
}
private val Unmeasured = IntPxSize(IntPx.Zero, IntPx.Zero)
private val Origin = IntPxPosition(IntPx.Zero, IntPx.Zero)
/**
* Backing node for Layout component.
*
* Measuring a [LayoutNode] as a [Measurable] will measure the node's content as adjusted by
* [modifier]. All layout state such as [modifiedSize] and [modifiedPosition] also reflect the modified
* state of the node.
*/
class LayoutNode : ComponentNode(), Measurable, MeasureScope {
/**
* The lambda used to measure the child. It must call [MeasureScope.layout] before
* completing.
*/
var measureBlock: MeasureFunction = ErrorMeasureBlock
set(value) {
field = value
requestRemeasure()
}
/**
* The lambda used to calculate [IntrinsicMeasurable.minIntrinsicWidth].
*/
var minIntrinsicWidthBlock:
DensityScope.(List<IntrinsicMeasurable>, IntPx) -> IntPx = ErrorIntrinsicBlock
set(value) {
field = value
requestRemeasure()
}
/**
* The lambda used to calculate [IntrinsicMeasurable.maxIntrinsicWidth].
*/
var maxIntrinsicWidthBlock:
DensityScope.(List<IntrinsicMeasurable>, IntPx) -> IntPx = ErrorIntrinsicBlock
set(value) {
field = value
requestRemeasure()
}
/**
* The lambda used to calculate [IntrinsicMeasurable.minIntrinsicHeight].
*/
var minIntrinsicHeightBlock:
DensityScope.(List<IntrinsicMeasurable>, IntPx) -> IntPx = ErrorIntrinsicBlock
set(value) {
field = value
requestRemeasure()
}
/**
* The lambda used to calculate [IntrinsicMeasurable.maxIntrinsicHeight].
*/
var maxIntrinsicHeightBlock:
DensityScope.(List<IntrinsicMeasurable>, IntPx) -> IntPx = ErrorIntrinsicBlock
set(value) {
field = value
requestRemeasure()
}
/**
* The constraints used the last time [layout] was called.
*/
var constraints: Constraints = Constraints.tightConstraints(IntPx.Zero, IntPx.Zero)
/**
* Implementation oddity around composition; used to capture a reference to this
* [LayoutNode] when composed. This is a reverse property that mutates its right-hand side.
*/
var ref: Ref<LayoutNode>?
get() = null
set(value) {
value?.value = this
}
/**
* The measured width of this layout and all of its [modifier]s. Shortcut for `size.width`.
*/
val width: IntPx get() = modifiedSize.width
/**
* The measured height of this layout and all of its [modifier]s. Shortcut for `size.height`.
*/
val height: IntPx get() = modifiedSize.height
/**
* The alignment lines of this layout, inherited + intrinsic
*/
internal val alignmentLines: MutableMap<AlignmentLine, IntPx> = hashMapOf()
/**
* The alignment lines provided by this layout at the last measurement
*/
internal val providedAlignmentLines: MutableMap<AlignmentLine, IntPx> = hashMapOf()
/**
* The measured size of this layout and all of its [modifier]s.
*/
val modifiedSize: IntPxSize get() = layoutNodeWrapper.size
/**
* The horizontal position of this layout and all of its [modifier]s within its parent.
*/
val x: IntPx get() = modifiedPosition.x
/**
* The vertical position of this layout and all of its [modifier]s within its parent.
*/
val y: IntPx get() = modifiedPosition.y
/**
* The position of this layout and all of its [modifier] within its parent.
*/
val modifiedPosition: IntPxPosition get() = layoutNodeWrapper.position
/**
* The position of the inner layout node content
*/
val contentPosition: IntPxPosition get() = innerLayoutNodeWrapper.position
/**
* The size of the inner layout node content
*/
val contentSize: IntPxSize get() = innerLayoutNodeWrapper.size
/**
* Whether or not this has been placed in the hierarchy.
*/
var isPlaced = false
private set
/**
* `true` when the parent's size depends on this LayoutNode's size
*/
var affectsParentSize: Boolean = true
/**
* `true` when inside [measure]
*/
var isMeasuring: Boolean = false
/**
* `true` when the current node is positioned during the measure pass,
* since it needs to compute alignment lines.
*/
var positionedDuringMeasurePass: Boolean = false
/**
* `true` when the layout has been dirtied by [requestRemeasure]. `false` after
* the measurement has been complete ([place] has been called).
*/
var needsRemeasure = false
internal set
/**
* `true` when the layout has been measured or dirtied because the layout
* lambda accessed a model that has been dirtied.
*/
var needsRelayout = false
internal set
/**
* `true` when the parent reads our alignment lines
*/
internal var alignmentLinesRead = false
/**
* `true` when the alignment lines have to be recomputed because the layout has
* been remeasured
*/
internal var dirtyAlignmentLines = true
/**
* `true` when an ancestor relies on our alignment lines
*/
internal val alignmentLinesRequired
get() = alignmentLinesQueryOwner != null && alignmentLinesQueryOwner!!.alignmentLinesRead
/**
* Used by the parent to identify if the child has been queried for alignment lines since
* last measurement.
*/
internal var alignmentLinesQueriedSinceLastLayout = false
/**
* The closest layout node above in the hierarchy which asked for alignment lines.
*/
internal var alignmentLinesQueryOwner: LayoutNode? = null
override val parentLayoutNode: LayoutNode?
get() = super.containingLayoutNode
override val containingLayoutNode: LayoutNode?
get() = this
/**
* This is the lambda that is passed to [MeasureScope.layout] to
* position children.
*/
private var positioningBlock: Placeable.PlacementScope.() -> Unit = {}
/**
* A local version of [Owner.measureIteration] to ensure that [measureBlock]
* is not called multiple times within a measure pass.
*/
private var measureIteration = 0L
/**
* Identifies when [layoutChildren] needs to be recalculated or if it can use
* the cached value.
*/
private var layoutChildrenDirty = false
/**
* The cached value of [layoutChildren]
*/
private val _layoutChildren = mutableListOf<LayoutNode>()
/**
* All first level [LayoutNode] descendants. All LayoutNodes in the List
* will have this as [parentLayoutNode].
*/
val layoutChildren: List<LayoutNode>
get() {
if (layoutChildrenDirty) {
_layoutChildren.clear()
addLayoutChildren(this, _layoutChildren)
layoutChildrenDirty = false
}
return _layoutChildren
}
/**
* `true` when parentDataNode has to be rediscovered. This is when the
* LayoutNode has been attached.
*/
private var parentDataDirty = false
// TODO: let the ModifiedLayoutNode wrapper provide this and return null here
override val parentData: Any?
get() = parentDataNode?.value
/**
* The parentData [DataNode] for this LayoutNode.
*/
private var parentDataNode: DataNode<*>? = null
get() {
if (parentDataDirty) {
// walk up to find ParentData
field = null
var node = parent
val parentLayoutNode = parentLayoutNode
while (node != null && node !== parentLayoutNode) {
if (node is DataNode<*> && node.key === ParentDataKey) {
field = node
break
}
node = node.parent
}
parentDataDirty = false
}
return field
}
override val density: Density
get() = owner?.density ?: Density(1f)
private val innerLayoutNodeWrapper: LayoutNodeWrapper = InnerPlaceable()
private var layoutNodeWrapper = innerLayoutNodeWrapper
/**
* The [Modifier] currently applied to this node.
*/
var modifier: Modifier = Modifier.None
set(value) {
if (value == field) return
field = value
// Rebuild layoutNodeWrapper
val oldPlaceable = layoutNodeWrapper
layoutNodeWrapper = modifier.foldOut(innerLayoutNodeWrapper) { mod, toWrap ->
if (mod is LayoutModifier) ModifiedLayoutNode(toWrap, mod) else toWrap
}
// Optimize the case where the layout itself is not modified. A common reason for
// this is if no wrapping actually occurs above because no LayoutModifiers are
// present in the modifier chain.
if (oldPlaceable != layoutNodeWrapper) {
requestRemeasure()
}
}
/**
* Measurable and Placeable type that has a position.
*/
private abstract class LayoutNodeWrapper : Placeable(), Measurable {
var position = Origin
/**
* Assigns a layout size to this [LayoutNodeWrapper] given the assigned innermost size
* from the call to [MeasureScope.layout]. Assigns and returns [modifiedSize].
*/
abstract fun layoutSize(innermostSize: IntPxSize): IntPxSize
}
private inner class InnerPlaceable : LayoutNodeWrapper(), DensityScope {
override fun measure(constraints: Constraints): Placeable {
measureBlock(layoutChildren, constraints)
return this
}
override val parentData: Any?
get() = this@LayoutNode.parentData
override fun minIntrinsicWidth(height: IntPx): IntPx =
minIntrinsicWidthBlock(this, layoutChildren, height)
override fun maxIntrinsicWidth(height: IntPx): IntPx =
maxIntrinsicWidthBlock(this, layoutChildren, height)
override fun minIntrinsicHeight(width: IntPx): IntPx =
minIntrinsicHeightBlock(this, layoutChildren, width)
override fun maxIntrinsicHeight(width: IntPx): IntPx =
maxIntrinsicHeightBlock(this, layoutChildren, width)
override var size: IntPxSize = Unmeasured
private set
override fun performPlace(position: IntPxPosition) {
isPlaced = true
if (position != this.position) {
this.position = position
owner?.onPositionChange(this@LayoutNode)
}
placeChildren()
}
override val density: Density get() = this@LayoutNode.density
override fun layoutSize(innermostSize: IntPxSize): IntPxSize {
size = innermostSize
return innermostSize
}
override fun get(line: AlignmentLine): IntPx? = calculateAlignmentLines()[line]
}
private inner class ModifiedLayoutNode(
val wrapped: LayoutNodeWrapper,
val layoutModifier: LayoutModifier
) : LayoutNodeWrapper() {
/**
* The [Placeable] returned by measuring [wrapped] in [measure].
* Used to avoid creating more wrapper objects than necessary since [ModifiedLayoutNode]
* also
*/
private var measuredPlaceable: Placeable? = null
/**
* The [Constraints] used in the current measurement of this modified node wrapper.
* See [withMeasuredConstraints]
*/
private var measuredConstraints: Constraints? = null
/**
* Sets [measuredConstraints] for the duration of [block].
*/
private inline fun <R> withMeasuredConstraints(
constraints: Constraints,
block: () -> R
): R = try {
measuredConstraints = constraints
block()
} finally {
measuredConstraints = null
}
// TODO change this once modifiers can take over for parentData
override val parentData: Any?
get() = wrapped.parentData
override fun measure(constraints: Constraints): Placeable = with(layoutModifier) {
val measureResult = withMeasuredConstraints(constraints) {
wrapped.measure(modifyConstraints(constraints))
}
measuredPlaceable = measureResult
this@ModifiedLayoutNode
}
override fun minIntrinsicWidth(height: IntPx): IntPx = with(layoutModifier) {
minIntrinsicWidthOf(wrapped, height)
}
override fun maxIntrinsicWidth(height: IntPx): IntPx = with(layoutModifier) {
maxIntrinsicWidthOf(wrapped, height)
}
override fun minIntrinsicHeight(width: IntPx): IntPx = with(layoutModifier) {
minIntrinsicHeightOf(wrapped, width)
}
override fun maxIntrinsicHeight(width: IntPx): IntPx = with(layoutModifier) {
maxIntrinsicHeightOf(wrapped, width)
}
override var size: IntPxSize = Unmeasured
private set
override fun performPlace(position: IntPxPosition) {
val placeable = measuredPlaceable ?: error("Placeable not measured")
placeable.place(with(layoutModifier) { modifyPosition(position, placeable.size, size) })
}
override fun get(line: AlignmentLine): IntPx? = with(layoutModifier) {
modifyAlignmentLine(line, wrapped[line])
}
override fun layoutSize(innermostSize: IntPxSize): IntPxSize = with(layoutModifier) {
val constraints = measuredConstraints ?: error("must be called during measurement")
modifySize(constraints, wrapped.layoutSize(innermostSize)).also { size = it }
}
}
internal val coordinates: LayoutCoordinates = LayoutNodeCoordinates(this)
override fun attach(owner: Owner) {
super.attach(owner)
requestRemeasure()
parentDataDirty = true
parentLayoutNode?.layoutChildrenDirty = true
}
override fun detach() {
parentLayoutNode?.layoutChildrenDirty = true
parentLayoutNode?.requestRemeasure()
parentDataDirty = true
alignmentLinesQueryOwner = null
super.detach()
}
override fun measure(constraints: Constraints): Placeable {
val owner = owner
// The more idiomatic `val iteration = owner?.measureIteration ?: 0L` causes boxing.
@Suppress("IfThenToElvis")
val iteration = if (owner == null) 0L else owner.measureIteration
check(measureIteration != iteration) {
"measure() may not be called multiple times on the same Measurable"
}
measureIteration = iteration
if (this.constraints == constraints && !needsRemeasure) {
val parent = parentLayoutNode
if (parent != null && parent.isMeasuring) {
affectsParentSize = true
}
return layoutNodeWrapper // we're already measured to this size, don't do anything
}
isMeasuring = true
dirtyAlignmentLines = true
layoutChildren.forEach { child ->
child.affectsParentSize = false
}
owner?.onStartMeasure(this)
try {
this.constraints = constraints
layoutNodeWrapper.measure(constraints)
} finally {
owner?.onEndMeasure(this)
isMeasuring = false
}
needsRemeasure = false
needsRelayout = true
return layoutNodeWrapper
}
override fun minIntrinsicWidth(height: IntPx): IntPx =
layoutNodeWrapper.minIntrinsicWidth(height)
override fun maxIntrinsicWidth(height: IntPx): IntPx =
layoutNodeWrapper.maxIntrinsicWidth(height)
override fun minIntrinsicHeight(width: IntPx): IntPx =
layoutNodeWrapper.minIntrinsicHeight(width)
override fun maxIntrinsicHeight(width: IntPx): IntPx =
layoutNodeWrapper.maxIntrinsicHeight(width)
fun place(x: IntPx, y: IntPx) {
with(Placeable.PlacementScope) {
layoutNodeWrapper.place(x, y)
}
}
fun placeChildren() {
if (needsRelayout) {
owner?.onStartLayout(this)
layoutChildren.forEach { child ->
child.isPlaced = false
if (alignmentLinesRequired && child.dirtyAlignmentLines) child.needsRelayout = true
if (!child.alignmentLinesRequired) {
child.alignmentLinesQueryOwner = alignmentLinesQueryOwner
}
child.alignmentLinesQueriedSinceLastLayout = false
}
positionedDuringMeasurePass = parentLayoutNode?.isMeasuring ?: false ||
parentLayoutNode?.positionedDuringMeasurePass ?: false
Placeable.positioningBlock()
layoutChildren.forEach { child ->
child.alignmentLinesRead = child.alignmentLinesQueriedSinceLastLayout
}
owner?.onEndLayout(this)
needsRelayout = false
if (alignmentLinesRequired && dirtyAlignmentLines) {
alignmentLines.clear()
layoutChildren.forEach { child ->
if (!child.isPlaced) return@forEach
child.alignmentLines.entries.forEach { (childLine, linePosition) ->
val linePositionInContainer = linePosition +
if (childLine.horizontal) child.y else child.x
// If the line was already provided by a previous child, merge the values.
alignmentLines[childLine] = if (childLine in alignmentLines) {
childLine.merge(
alignmentLines.getValue(childLine),
linePositionInContainer
)
} else {
linePositionInContainer
}
}
}
alignmentLines += providedAlignmentLines
dirtyAlignmentLines = false
}
}
}
internal fun calculateAlignmentLines(): Map<AlignmentLine, IntPx> {
alignmentLinesRead = true
alignmentLinesQueryOwner = this
alignmentLinesQueriedSinceLastLayout = true
if (dirtyAlignmentLines) {
needsRelayout = true
placeChildren()
}
return alignmentLines
}
override fun layout(
width: IntPx,
height: IntPx,
vararg alignmentLines: Pair<AlignmentLine, IntPx>,
placementBlock: Placeable.PlacementScope.() -> Unit
): MeasureScope.LayoutResult {
val oldSize = modifiedSize
val outerSize = layoutNodeWrapper.layoutSize(IntPxSize(width, height))
// The more idiomatic, `if (parentLayoutNode?.isMeasuring == true)` causes boxing
val parent = parentLayoutNode
@Suppress("SimplifyBooleanWithConstants")
if (parent != null && parent.isMeasuring == true) {
affectsParentSize = true
}
if (oldSize != outerSize) {
// TODO this may also need to be updated when the modifier chain size changes
owner?.onSizeChange(this@LayoutNode)
}
this.providedAlignmentLines.clear()
this.providedAlignmentLines += alignmentLines
this.positioningBlock = placementBlock
return MeasureScope.LayoutResult
}
/**
* Used by `ComplexLayoutState` to request a new measurement + layout pass from the owner.
*/
fun requestRemeasure() {
owner?.onRequestMeasure(this)
}
internal fun dispatchOnPositionedCallbacks() {
// There are two types of callbacks:
// a) when the Layout is positioned - `onPositioned`
// b) when the child of the Layout is positioned - `onChildPositioned`
walkOnPosition(this, this.coordinates)
walkOnChildPositioned(this, this.coordinates)
}
internal companion object {
@Suppress("UNCHECKED_CAST")
private fun walkOnPosition(node: ComponentNode, coordinates: LayoutCoordinates) {
node.visitChildren { child ->
if (child !is LayoutNode) {
if (child is DataNode<*> && child.key === OnPositionedKey) {
val method = child.value as (LayoutCoordinates) -> Unit
method(coordinates)
}
walkOnPosition(child, coordinates)
} else {
if (!child.needsRelayout) {
child.dispatchOnPositionedCallbacks()
}
}
}
}
@Suppress("UNCHECKED_CAST")
private fun walkOnChildPositioned(layoutNode: LayoutNode, coordinates: LayoutCoordinates) {
var node = layoutNode.parent
while (node != null && node !is LayoutNode) {
if (node is DataNode<*> && node.key === OnChildPositionedKey) {
val method = node.value as (LayoutCoordinates) -> Unit
method(coordinates)
}
node = node.parent
}
}
private val ErrorMeasureBlock: MeasureFunction = { _, _ ->
error("Undefined measure and it is required")
}
private val ErrorIntrinsicBlock:
DensityScope.(List<IntrinsicMeasurable>, IntPx) -> IntPx = { _, _ ->
error("Undefined intrinsics block and it is required")
}
private fun addLayoutChildren(node: ComponentNode, list: MutableList<LayoutNode>) {
node.visitChildren { child ->
if (child is LayoutNode) {
list += child
} else {
addLayoutChildren(child, list)
}
}
}
}
}
private class InvalidatingProperty<T>(private var value: T) :
ReadWriteProperty<SemanticsComponentNode, T> {
override fun getValue(thisRef: SemanticsComponentNode, property: KProperty<*>): T {
return value
}
override fun setValue(
thisRef: SemanticsComponentNode,
property: KProperty<*>,
value: T
) {
if (this.value == value) {
return
}
this.value = value
thisRef.markNeedsSemanticsUpdate()
}
}
class SemanticsComponentNode(
// TODO(ryanmentley): probably take away these default values
semanticsConfiguration: SemanticsConfiguration = SemanticsConfiguration(),
/**
* If [container] is true, this widget will introduce a new
* node in the semantics tree. Otherwise, the semantics will be
* merged with the semantics of any ancestors (if the ancestor allows that).
*
* Whether descendants of this widget can add their semantic information to the
* [SemanticsNode] introduced by this configuration is controlled by
* [explicitChildNodes].
*/
container: Boolean = false,
/**
* Whether descendants of this widget are allowed to add semantic information
* to the [SemanticsNode] annotated by this widget.
*
* When set to false descendants are allowed to annotate [SemanticNode]s of
* their parent with the semantic information they want to contribute to the
* semantic tree.
* When set to true the only way for descendants to contribute semantic
* information to the semantic tree is to introduce new explicit
* [SemanticNode]s to the tree.
*
* If the semantics properties of this node include
* [SemanticsProperties.scopesRoute] set to true, then [explicitChildNodes]
* must be true also.
*
* This setting is often used in combination with [SemanticsConfiguration.isSemanticBoundary]
* to create semantic boundaries that are either writable or not for children.
*/
explicitChildNodes: Boolean = false
) : ComponentNode() {
private var needsSemanticsUpdate = true
var container: Boolean by InvalidatingProperty(container)
var explicitChildNodes: Boolean by InvalidatingProperty(explicitChildNodes)
// TODO(ryanmentley): this should be smarter and invalidate less
var semanticsConfiguration: SemanticsConfiguration
by InvalidatingProperty(semanticsConfiguration)
internal fun markNeedsSemanticsUpdate() {
needsSemanticsUpdate = true
}
}
/**
* The key used in DataNode.
* TODO(mount): Make this inline
*
* @param T Identifies the type used in the value
* @property name A unique name identifying the type of the key.
*/
class DataNodeKey<T>(val name: String)
/**
* A ComponentNode that stores a value in the emitted hierarchy
*
* @param T The type used for the value
* @property key The key object used to identify the key
* @property value The value of the data being stored in the hierarchy
*/
class DataNode<T>(val key: DataNodeKey<T>, var value: T) : ComponentNode() {
override fun attach(owner: Owner) {
super.attach(owner)
parentLayoutNode?.requestRemeasure()
}
}
/**
* Returns [ComponentNode.owner] or throws if it is null.
*/
fun ComponentNode.requireOwner(): Owner = owner ?: ErrorMessages.NodeShouldBeAttached.state()
/**
* Inserts a child [ComponentNode] at a last index. If this ComponentNode [isAttached]
* then [child] will become [isAttached]ed also. [child] must have a `null` [ComponentNode.parent].
*/
fun ComponentNode.add(child: ComponentNode) {
emitInsertAt(count, child)
}
class Ref<T> {
var value: T? = null
}
/**
* Converts a global position into a local position within this LayoutNode.
*/
fun LayoutNode.globalToLocal(global: PxPosition, withOwnerOffset: Boolean = true): PxPosition {
var x: Px = global.x
var y: Px = global.y
var node: LayoutNode? = this
while (node != null) {
val pos = node.contentPosition
x -= pos.x.toPx()
y -= pos.y.toPx()
node = node.parentLayoutNode
}
if (withOwnerOffset) {
val ownerPosition = requireOwner().calculatePosition()
x -= ownerPosition.x
y -= ownerPosition.y
}
return PxPosition(x, y)
}
/**
* Converts a local position within this LayoutNode into a global one.
*/
fun LayoutNode.localToGlobal(local: PxPosition, withOwnerOffset: Boolean = true): PxPosition {
var x: Px = local.x
var y: Px = local.y
var node: LayoutNode? = this
while (node != null) {
val pos = node.contentPosition
x += pos.x.toPx()
y += pos.y.toPx()
node = node.parentLayoutNode
}
if (withOwnerOffset) {
val ownerPosition = requireOwner().calculatePosition()
x += ownerPosition.x
y += ownerPosition.y
}
return PxPosition(x, y)
}
/**
* Converts a child LayoutNode position into a local position within this LayoutNode.
*/
fun LayoutNode.childToLocal(child: LayoutNode, childLocal: PxPosition): PxPosition {
if (child === this) {
return childLocal
}
var x: Px = childLocal.x
var y: Px = childLocal.y
var node: LayoutNode? = child
while (true) {
checkNotNull(node) {
"Current layout is not an ancestor of the provided child layout"
}
val pos = node.contentPosition
x += pos.x.toPx()
y += pos.y.toPx()
node = node.parentLayoutNode
if (node === this) {
// found the node
break
}
}
return PxPosition(x, y)
}
/**
* Calculates the position of this [LayoutNode] relative to the root of the ui tree.
*/
fun LayoutNode.positionRelativeToRoot() = localToGlobal(PxPosition.Origin, false)
/**
* Calculates the position of this [LayoutNode] relative to the provided ancestor.
*/
fun LayoutNode.positionRelativeToAncestor(ancestor: LayoutNode) =
ancestor.childToLocal(this, PxPosition.Origin)
/**
* Executes [block] on first level of [LayoutNode] descendants of this ComponentNode.
*/
fun ComponentNode.visitLayoutChildren(block: (LayoutNode) -> Unit) {
visitChildren { child ->
if (child is LayoutNode) {
block(child)
} else {
child.visitLayoutChildren(block)
}
}
}
/**
* Executes [block] on first level of [LayoutNode] descendants of this ComponentNode
* and returns the last `LayoutNode` to return `true` from [block].
*/
fun ComponentNode.findLastLayoutChild(block: (LayoutNode) -> Boolean): LayoutNode? {
for (i in count - 1 downTo 0) {
val child = this[i]
if (child is LayoutNode) {
if (block(child)) {
return child
}
} else {
val layoutNode = child.findLastLayoutChild(block)
if (layoutNode != null) {
return layoutNode
}
}
}
return null
}
/**
* Returns `true` if this ComponentNode has no descendant [LayoutNode]s.
*/
fun ComponentNode.hasNoLayoutDescendants() = findLastLayoutChild { true } == null
/**
* DataNodeKey for ParentData
*/
val ParentDataKey = DataNodeKey<Any>("Compose:ParentData")
/**
* DataNodeKey for OnPositioned callback
*/
val OnPositionedKey = DataNodeKey<(LayoutCoordinates) -> Unit>("Compose:OnPositioned")
/**
* DataNodeKey for OnChildPositioned callback
*/
val OnChildPositionedKey =
DataNodeKey<(LayoutCoordinates) -> Unit>("Compose:OnChildPositioned")
/**
* A LayoutCoordinates implementation based on LayoutNode.
*/
private class LayoutNodeCoordinates(
private val layoutNode: LayoutNode
) : LayoutCoordinates {
override val position get() = layoutNode.contentPosition.toPxPosition()
override val size get() = layoutNode.contentSize.toPxSize()
override val parentCoordinates get() = layoutNode.parentLayoutNode?.coordinates
override fun globalToLocal(global: PxPosition) = layoutNode.globalToLocal(global)
override fun localToGlobal(local: PxPosition) = layoutNode.localToGlobal(local)
override fun localToRoot(local: PxPosition) = layoutNode.localToGlobal(local, false)
override fun childToLocal(child: LayoutCoordinates, childLocal: PxPosition): PxPosition {
if (child !is LayoutNodeCoordinates) {
throw IllegalArgumentException("Incorrect child provided.")
}
return layoutNode.childToLocal(child.layoutNode, childLocal)
}
}