blob: 4bc3a9851a4f1d19ad70bd21534a2dae12a1549a [file]
/*
* 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.foundation.layout
import androidx.compose.foundation.layout.internal.requirePrecondition
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.AlignmentLine
import androidx.compose.ui.layout.FirstBaseline
import androidx.compose.ui.layout.HorizontalAlignmentLine
import androidx.compose.ui.layout.LastBaseline
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.node.LayoutModifierNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.isSpecified
import androidx.compose.ui.unit.isUnspecified
import kotlin.math.max
/**
* A [Modifier] that can add padding to position the content according to specified distances from
* its bounds to an [alignment line][AlignmentLine]. Whether the positioning is vertical or
* horizontal is defined by the orientation of the given [alignmentLine] (if the line is horizontal,
* [before] and [after] will refer to distances from top and bottom, otherwise they will refer to
* distances from start and end). The opposite axis sizing and positioning will remain unaffected.
* The modified layout will try to include the required padding, subject to the incoming max layout
* constraints, such that the distance from its bounds to the [alignmentLine] of the content will be
* [before] and [after], respectively. When the max constraints do not allow this, satisfying the
* [before] requirement will have priority over [after]. When the modified layout is min constrained
* in the affected layout direction and the padded layout is smaller than the constraint, the
* modified layout will satisfy the min constraint and the content will be positioned to satisfy the
* [before] requirement if specified, or the [after] requirement otherwise.
*
* Example usage:
*
* @sample androidx.compose.foundation.layout.samples.PaddingFromSample
* @param alignmentLine the alignment line relative to which the padding is defined
* @param before the distance between the container's top edge and the horizontal alignment line, or
* the container's start edge and the vertical alignment line
* @param after the distance between the container's bottom edge and the horizontal alignment line,
* or the container's end edge and the vertical alignment line
* @see paddingFromBaseline
*/
@Stable
fun Modifier.paddingFrom(
alignmentLine: AlignmentLine,
before: Dp = Dp.Unspecified,
after: Dp = Dp.Unspecified,
): Modifier =
this.then(
AlignmentLineOffsetDpElement(
alignmentLine,
before,
after,
debugInspectorInfo {
name = "paddingFrom"
properties["alignmentLine"] = alignmentLine
properties["before"] = before
properties["after"] = after
},
)
)
/**
* A [Modifier] that can add padding to position the content according to specified distances from
* its bounds to an [alignment line][AlignmentLine]. Whether the positioning is vertical or
* horizontal is defined by the orientation of the given [alignmentLine] (if the line is horizontal,
* [before] and [after] will refer to distances from top and bottom, otherwise they will refer to
* distances from start and end). The opposite axis sizing and positioning will remain unaffected.
* The modified layout will try to include the required padding, subject to the incoming max layout
* constraints, such that the distance from its bounds to the [alignmentLine] of the content will be
* [before] and [after], respectively. When the max constraints do not allow this, satisfying the
* [before] requirement will have priority over [after]. When the modified layout is min constrained
* in the affected layout direction and the padded layout is smaller than the constraint, the
* modified layout will satisfy the min constraint and the content will be positioned to satisfy the
* [before] requirement if specified, or the [after] requirement otherwise.
*
* Example usage:
*
* @sample androidx.compose.foundation.layout.samples.PaddingFromSample
* @param alignmentLine the alignment line relative to which the padding is defined
* @param before the distance between the container's top edge and the horizontal alignment line, or
* the container's start edge and the vertical alignment line
* @param after the distance between the container's bottom edge and the horizontal alignment line,
* or the container's end edge and the vertical alignment line
* @see paddingFromBaseline
*/
@Stable
fun Modifier.paddingFrom(
alignmentLine: AlignmentLine,
before: TextUnit = TextUnit.Unspecified,
after: TextUnit = TextUnit.Unspecified,
): Modifier =
this.then(
AlignmentLineOffsetTextUnitElement(
alignmentLine,
before,
after,
debugInspectorInfo {
name = "paddingFrom"
properties["alignmentLine"] = alignmentLine
properties["before"] = before
properties["after"] = after
},
)
)
/**
* A [Modifier] that positions the content in a layout such that the distance from the top of the
* layout to the [baseline of the first line of text in the content][FirstBaseline] is [top], and
* the distance from the [baseline of the last line of text in the content][LastBaseline] to the
* bottom of the layout is [bottom].
*
* When the modified layout is min height constrained and the padded layout is smaller than the
* constraint, the modified layout will satisfy the min constraint and the content will be
* positioned to satisfy the [top] requirement if specified, or the [bottom] requirement otherwise.
*
* Example usage:
*
* @sample androidx.compose.foundation.layout.samples.PaddingFromBaselineSampleDp
* @see paddingFrom
*/
@Stable
fun Modifier.paddingFromBaseline(top: Dp = Dp.Unspecified, bottom: Dp = Dp.Unspecified) =
this.then(
if (top.isSpecified) {
Modifier.paddingFrom(FirstBaseline, before = top)
} else {
Modifier
}
)
.then(
if (bottom.isSpecified) {
Modifier.paddingFrom(LastBaseline, after = bottom)
} else {
Modifier
}
)
/**
* A [Modifier] that positions the content in a layout such that the distance from the top of the
* layout to the [baseline of the first line of text in the content][FirstBaseline] is [top], and
* the distance from the [baseline of the last line of text in the content][LastBaseline] to the
* bottom of the layout is [bottom].
*
* When the modified layout is min height constrained and the padded layout is smaller than the
* constraint, the modified layout will satisfy the min constraint and the content will be
* positioned to satisfy the [top] requirement if specified, or the [bottom] requirement otherwise.
*
* Example usage:
*
* @sample androidx.compose.foundation.layout.samples.PaddingFromBaselineSampleTextUnit
* @see paddingFrom
*/
@Stable
fun Modifier.paddingFromBaseline(
top: TextUnit = TextUnit.Unspecified,
bottom: TextUnit = TextUnit.Unspecified,
) =
this.then(
if (!top.isUnspecified) Modifier.paddingFrom(FirstBaseline, before = top) else Modifier
)
.then(
if (!bottom.isUnspecified) Modifier.paddingFrom(LastBaseline, after = bottom)
else Modifier
)
private class AlignmentLineOffsetDpElement(
val alignmentLine: AlignmentLine,
val before: Dp,
val after: Dp,
val inspectorInfo: InspectorInfo.() -> Unit,
) : ModifierNodeElement<AlignmentLineOffsetDpNode>() {
init {
requirePrecondition(
(before.value >= 0f || before.isUnspecified) and
(after.value >= 0f || after.isUnspecified)
) {
"Padding from alignment line must be a non-negative number"
}
}
override fun create(): AlignmentLineOffsetDpNode {
return AlignmentLineOffsetDpNode(alignmentLine, before, after)
}
override fun update(node: AlignmentLineOffsetDpNode) {
node.alignmentLine = alignmentLine
node.before = before
node.after = after
}
override fun InspectorInfo.inspectableProperties() {
inspectorInfo()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
val otherModifier = other as? AlignmentLineOffsetDpElement ?: return false
return alignmentLine == otherModifier.alignmentLine &&
before == otherModifier.before &&
after == otherModifier.after
}
override fun hashCode(): Int {
var result = alignmentLine.hashCode()
result = 31 * result + before.hashCode()
result = 31 * result + after.hashCode()
return result
}
}
private class AlignmentLineOffsetDpNode(
var alignmentLine: AlignmentLine,
var before: Dp,
var after: Dp,
) : LayoutModifierNode, Modifier.Node() {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints,
): MeasureResult {
return alignmentLineOffsetMeasure(alignmentLine, before, after, measurable, constraints)
}
}
private class AlignmentLineOffsetTextUnitElement(
val alignmentLine: AlignmentLine,
val before: TextUnit,
val after: TextUnit,
val inspectorInfo: InspectorInfo.() -> Unit,
) : ModifierNodeElement<AlignmentLineOffsetTextUnitNode>() {
override fun create(): AlignmentLineOffsetTextUnitNode {
return AlignmentLineOffsetTextUnitNode(alignmentLine, before, after)
}
override fun hashCode(): Int {
var result = alignmentLine.hashCode()
result = 31 * result + before.hashCode()
result = 31 * result + after.hashCode()
return result
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
val otherModifier = other as? AlignmentLineOffsetTextUnitElement ?: return false
return alignmentLine == otherModifier.alignmentLine &&
before == otherModifier.before &&
after == otherModifier.after
}
override fun InspectorInfo.inspectableProperties() {
return inspectorInfo()
}
override fun update(node: AlignmentLineOffsetTextUnitNode) {
node.alignmentLine = alignmentLine
node.before = before
node.after = after
}
}
private class AlignmentLineOffsetTextUnitNode(
var alignmentLine: AlignmentLine,
var before: TextUnit,
var after: TextUnit,
) : LayoutModifierNode, Modifier.Node() {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints,
): MeasureResult {
return alignmentLineOffsetMeasure(
alignmentLine,
if (!before.isUnspecified) before.toDp() else Dp.Unspecified,
if (!after.isUnspecified) after.toDp() else Dp.Unspecified,
measurable,
constraints,
)
}
}
private fun MeasureScope.alignmentLineOffsetMeasure(
alignmentLine: AlignmentLine,
before: Dp,
after: Dp,
measurable: Measurable,
constraints: Constraints,
): MeasureResult {
val placeable =
measurable.measure(
// Loose constraints perpendicular on the alignment line.
if (alignmentLine.horizontal) constraints.copy(minHeight = 0)
else constraints.copy(minWidth = 0)
)
val linePosition =
placeable[alignmentLine].let { if (it != AlignmentLine.Unspecified) it else 0 }
val axis = if (alignmentLine.horizontal) placeable.height else placeable.width
val axisMax = if (alignmentLine.horizontal) constraints.maxHeight else constraints.maxWidth
// Compute padding required to satisfy the total before and after offsets.
val paddingBefore =
((if (before.isSpecified) before.roundToPx() else 0) - linePosition).coerceIn(
0,
axisMax - axis,
)
val paddingAfter =
((if (after.isSpecified) after.roundToPx() else 0) - axis + linePosition).coerceIn(
0,
axisMax - axis - paddingBefore,
)
val width =
if (alignmentLine.horizontal) {
placeable.width
} else {
max(paddingBefore + placeable.width + paddingAfter, constraints.minWidth)
}
val height =
if (alignmentLine.horizontal) {
max(paddingBefore + placeable.height + paddingAfter, constraints.minHeight)
} else {
placeable.height
}
return layout(width, height) {
val x =
when {
alignmentLine.horizontal -> 0
before != Dp.Unspecified -> paddingBefore
else -> width - paddingAfter - placeable.width
}
val y =
when {
!alignmentLine.horizontal -> 0
before != Dp.Unspecified -> paddingBefore
else -> height - paddingAfter - placeable.height
}
placeable.placeRelative(x, y)
}
}
private val AlignmentLine.horizontal: Boolean
get() = this is HorizontalAlignmentLine