| /* |
| * 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.PaddingValues.Absolute |
| import androidx.compose.foundation.layout.internal.requirePrecondition |
| import androidx.compose.runtime.Immutable |
| import androidx.compose.runtime.Stable |
| import androidx.compose.ui.Modifier |
| 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.unit.Constraints |
| import androidx.compose.ui.unit.Dp |
| import androidx.compose.ui.unit.LayoutDirection |
| import androidx.compose.ui.unit.coerceAtLeast |
| import androidx.compose.ui.unit.constrainHeight |
| import androidx.compose.ui.unit.constrainWidth |
| import androidx.compose.ui.unit.dp |
| import androidx.compose.ui.unit.isUnspecified |
| import androidx.compose.ui.unit.offset |
| |
| /** |
| * Apply additional space along each edge of the content in [Dp]: [start], [top], [end] and |
| * [bottom]. The start and end edges will be determined by the current [LayoutDirection]. Padding is |
| * applied before content measurement and takes precedence; content may only be as large as the |
| * remaining space. |
| * |
| * Negative padding is not permitted — it will cause [IllegalArgumentException]. See |
| * [Modifier.offset]. |
| * |
| * Example usage: |
| * |
| * @sample androidx.compose.foundation.layout.samples.PaddingModifier |
| */ |
| @Stable |
| fun Modifier.padding(start: Dp = 0.dp, top: Dp = 0.dp, end: Dp = 0.dp, bottom: Dp = 0.dp) = |
| this then |
| PaddingElement( |
| start = start, |
| top = top, |
| end = end, |
| bottom = bottom, |
| rtlAware = true, |
| inspectorInfo = { |
| name = "padding" |
| properties["start"] = start |
| properties["top"] = top |
| properties["end"] = end |
| properties["bottom"] = bottom |
| }, |
| ) |
| |
| /** |
| * Apply [horizontal] dp space along the left and right edges of the content, and [vertical] dp |
| * space along the top and bottom edges. Padding is applied before content measurement and takes |
| * precedence; content may only be as large as the remaining space. |
| * |
| * Negative padding is not permitted — it will cause [IllegalArgumentException]. See |
| * [Modifier.offset]. |
| * |
| * Example usage: |
| * |
| * @sample androidx.compose.foundation.layout.samples.SymmetricPaddingModifier |
| */ |
| @Stable |
| fun Modifier.padding(horizontal: Dp = 0.dp, vertical: Dp = 0.dp) = |
| this then |
| PaddingElement( |
| start = horizontal, |
| top = vertical, |
| end = horizontal, |
| bottom = vertical, |
| rtlAware = true, |
| inspectorInfo = { |
| name = "padding" |
| properties["horizontal"] = horizontal |
| properties["vertical"] = vertical |
| }, |
| ) |
| |
| /** |
| * Apply [all] dp of additional space along each edge of the content, left, top, right and bottom. |
| * Padding is applied before content measurement and takes precedence; content may only be as large |
| * as the remaining space. |
| * |
| * Negative padding is not permitted — it will cause [IllegalArgumentException]. See |
| * [Modifier.offset]. |
| * |
| * Example usage: |
| * |
| * @sample androidx.compose.foundation.layout.samples.PaddingAllModifier |
| */ |
| @Stable |
| fun Modifier.padding(all: Dp) = |
| this then |
| PaddingElement( |
| start = all, |
| top = all, |
| end = all, |
| bottom = all, |
| rtlAware = true, |
| inspectorInfo = { |
| name = "padding" |
| value = all |
| }, |
| ) |
| |
| /** |
| * Apply [PaddingValues] to the component as additional space along each edge of the content's left, |
| * top, right and bottom. Padding is applied before content measurement and takes precedence; |
| * content may only be as large as the remaining space. |
| * |
| * Negative padding is not permitted — it will cause [IllegalArgumentException]. See |
| * [Modifier.offset]. |
| * |
| * Example usage: |
| * |
| * @sample androidx.compose.foundation.layout.samples.PaddingValuesModifier |
| */ |
| @Stable |
| fun Modifier.padding(paddingValues: PaddingValues) = |
| this then |
| PaddingValuesElement( |
| paddingValues = paddingValues, |
| inspectorInfo = { |
| name = "padding" |
| properties["paddingValues"] = paddingValues |
| }, |
| ) |
| |
| /** |
| * Apply additional space along each edge of the content in [Dp]: [left], [top], [right] and |
| * [bottom]. These paddings are applied without regard to the current [LayoutDirection], see |
| * [padding] to apply relative paddings. Padding is applied before content measurement and takes |
| * precedence; content may only be as large as the remaining space. |
| * |
| * Negative padding is not permitted — it will cause [IllegalArgumentException]. See |
| * [Modifier.offset]. |
| * |
| * Example usage: |
| * |
| * @sample androidx.compose.foundation.layout.samples.AbsolutePaddingModifier |
| */ |
| @Stable |
| fun Modifier.absolutePadding(left: Dp = 0.dp, top: Dp = 0.dp, right: Dp = 0.dp, bottom: Dp = 0.dp) = |
| this then |
| (PaddingElement( |
| start = left, |
| top = top, |
| end = right, |
| bottom = bottom, |
| rtlAware = false, |
| inspectorInfo = { |
| name = "absolutePadding" |
| properties["left"] = left |
| properties["top"] = top |
| properties["right"] = right |
| properties["bottom"] = bottom |
| }, |
| )) |
| |
| /** |
| * Describes a padding to be applied along the edges inside a box. See the [PaddingValues] factories |
| * and [Absolute] for convenient ways to build [PaddingValues]. |
| */ |
| @Stable |
| interface PaddingValues { |
| /** The padding to be applied along the left edge inside a box. */ |
| fun calculateLeftPadding(layoutDirection: LayoutDirection): Dp |
| |
| /** The padding to be applied along the top edge inside a box. */ |
| fun calculateTopPadding(): Dp |
| |
| /** The padding to be applied along the right edge inside a box. */ |
| fun calculateRightPadding(layoutDirection: LayoutDirection): Dp |
| |
| /** The padding to be applied along the bottom edge inside a box. */ |
| fun calculateBottomPadding(): Dp |
| |
| /** Describes an absolute (RTL unaware) padding to be applied along the edges inside a box. */ |
| @Immutable |
| class Absolute( |
| @Stable private val left: Dp = 0.dp, |
| @Stable private val top: Dp = 0.dp, |
| @Stable private val right: Dp = 0.dp, |
| @Stable private val bottom: Dp = 0.dp, |
| ) : PaddingValues { |
| |
| init { |
| requirePrecondition( |
| (left.value >= 0f) and |
| (top.value >= 0f) and |
| (right.value >= 0f) and |
| (bottom.value >= 0f) |
| ) { |
| "Padding must be non-negative" |
| } |
| } |
| |
| override fun calculateLeftPadding(layoutDirection: LayoutDirection) = left |
| |
| override fun calculateTopPadding() = top |
| |
| override fun calculateRightPadding(layoutDirection: LayoutDirection) = right |
| |
| override fun calculateBottomPadding() = bottom |
| |
| override fun equals(other: Any?): Boolean { |
| if (other !is Absolute) return false |
| return left == other.left && |
| top == other.top && |
| right == other.right && |
| bottom == other.bottom |
| } |
| |
| override fun hashCode() = |
| ((left.hashCode() * 31 + top.hashCode()) * 31 + right.hashCode()) * 31 + |
| bottom.hashCode() |
| |
| override fun toString() = |
| "PaddingValues.Absolute(left=$left, top=$top, right=$right, bottom=$bottom)" |
| } |
| |
| companion object { |
| /** PaddingValues with all values `0.dp`. */ |
| @Stable @get:Stable val Zero: PaddingValues = Absolute() |
| } |
| } |
| |
| /** Adds two [PaddingValues] together. */ |
| @Stable |
| operator fun PaddingValues.plus(other: PaddingValues): PaddingValues = |
| object : PaddingValues { |
| override fun calculateLeftPadding(layoutDirection: LayoutDirection): Dp = |
| this@plus.calculateLeftPadding(layoutDirection) + |
| other.calculateLeftPadding(layoutDirection) |
| |
| override fun calculateTopPadding(): Dp = |
| this@plus.calculateTopPadding() + other.calculateTopPadding() |
| |
| override fun calculateRightPadding(layoutDirection: LayoutDirection): Dp = |
| this@plus.calculateRightPadding(layoutDirection) + |
| other.calculateRightPadding(layoutDirection) |
| |
| override fun calculateBottomPadding(): Dp = |
| this@plus.calculateBottomPadding() + other.calculateBottomPadding() |
| } |
| |
| /** Subtracts a [PaddingValues] from another one and ensures that the result is non-negative. */ |
| @Stable |
| operator fun PaddingValues.minus(other: PaddingValues): PaddingValues = |
| object : PaddingValues { |
| override fun calculateLeftPadding(layoutDirection: LayoutDirection): Dp = |
| (this@minus.calculateLeftPadding(layoutDirection) - |
| other.calculateLeftPadding(layoutDirection)) |
| .coerceAtLeast(0.dp) |
| |
| override fun calculateTopPadding(): Dp = |
| (this@minus.calculateTopPadding() - other.calculateTopPadding()).coerceAtLeast(0.dp) |
| |
| override fun calculateRightPadding(layoutDirection: LayoutDirection): Dp = |
| (this@minus.calculateRightPadding(layoutDirection) - |
| other.calculateRightPadding(layoutDirection)) |
| .coerceAtLeast(0.dp) |
| |
| override fun calculateBottomPadding(): Dp = |
| (this@minus.calculateBottomPadding() - other.calculateBottomPadding()).coerceAtLeast( |
| 0.dp |
| ) |
| } |
| |
| /** |
| * The padding to be applied along the start edge inside a box: along the left edge if the layout |
| * direction is LTR, or along the right edge for RTL. |
| */ |
| @Stable |
| fun PaddingValues.calculateStartPadding(layoutDirection: LayoutDirection) = |
| if (layoutDirection == LayoutDirection.Ltr) { |
| calculateLeftPadding(layoutDirection) |
| } else { |
| calculateRightPadding(layoutDirection) |
| } |
| |
| /** |
| * The padding to be applied along the end edge inside a box: along the right edge if the layout |
| * direction is LTR, or along the left edge for RTL. |
| */ |
| @Stable |
| fun PaddingValues.calculateEndPadding(layoutDirection: LayoutDirection) = |
| if (layoutDirection == LayoutDirection.Ltr) { |
| calculateRightPadding(layoutDirection) |
| } else { |
| calculateLeftPadding(layoutDirection) |
| } |
| |
| /** Creates a padding of [all] dp along all 4 edges. */ |
| @Stable fun PaddingValues(all: Dp): PaddingValues = PaddingValuesImpl(all, all, all, all) |
| |
| /** |
| * Creates a padding of [horizontal] dp along the left and right edges, and of [vertical] dp along |
| * the top and bottom edges. |
| */ |
| @Stable |
| fun PaddingValues(horizontal: Dp = 0.dp, vertical: Dp = 0.dp): PaddingValues = |
| PaddingValuesImpl(horizontal, vertical, horizontal, vertical) |
| |
| /** |
| * Creates a padding to be applied along the edges inside a box. In LTR contexts [start] will be |
| * applied along the left edge and [end] will be applied along the right edge. In RTL contexts, |
| * [start] will correspond to the right edge and [end] to the left. |
| */ |
| @Stable |
| fun PaddingValues( |
| start: Dp = 0.dp, |
| top: Dp = 0.dp, |
| end: Dp = 0.dp, |
| bottom: Dp = 0.dp, |
| ): PaddingValues = PaddingValuesImpl(start, top, end, bottom) |
| |
| @Immutable |
| internal class PaddingValuesImpl( |
| @Stable val start: Dp = 0.dp, |
| @Stable val top: Dp = 0.dp, |
| @Stable val end: Dp = 0.dp, |
| @Stable val bottom: Dp = 0.dp, |
| ) : PaddingValues { |
| |
| init { |
| requirePrecondition( |
| (start.value >= 0f) and (top.value >= 0f) and (end.value >= 0f) and (bottom.value >= 0f) |
| ) { |
| "Padding must be non-negative" |
| } |
| } |
| |
| override fun calculateLeftPadding(layoutDirection: LayoutDirection) = |
| if (layoutDirection == LayoutDirection.Ltr) start else end |
| |
| override fun calculateTopPadding() = top |
| |
| override fun calculateRightPadding(layoutDirection: LayoutDirection) = |
| if (layoutDirection == LayoutDirection.Ltr) end else start |
| |
| override fun calculateBottomPadding() = bottom |
| |
| override fun equals(other: Any?): Boolean { |
| if (other !is PaddingValuesImpl) return false |
| return start == other.start && |
| top == other.top && |
| end == other.end && |
| bottom == other.bottom |
| } |
| |
| override fun hashCode() = |
| ((start.hashCode() * 31 + top.hashCode()) * 31 + end.hashCode()) * 31 + bottom.hashCode() |
| |
| override fun toString() = "PaddingValues(start=$start, top=$top, end=$end, bottom=$bottom)" |
| } |
| |
| private class PaddingElement( |
| var start: Dp = 0.dp, |
| var top: Dp = 0.dp, |
| var end: Dp = 0.dp, |
| var bottom: Dp = 0.dp, |
| var rtlAware: Boolean, |
| val inspectorInfo: InspectorInfo.() -> Unit, |
| ) : ModifierNodeElement<PaddingNode>() { |
| |
| init { |
| requirePrecondition( |
| (start.value >= 0f || start.isUnspecified) and |
| (top.value >= 0f || top.isUnspecified) and |
| (end.value >= 0f || end.isUnspecified) and |
| (bottom.value >= 0f || bottom.isUnspecified) |
| ) { |
| "Padding must be non-negative" |
| } |
| } |
| |
| override fun create(): PaddingNode { |
| return PaddingNode(start, top, end, bottom, rtlAware) |
| } |
| |
| override fun update(node: PaddingNode) { |
| node.start = start |
| node.top = top |
| node.end = end |
| node.bottom = bottom |
| node.rtlAware = rtlAware |
| } |
| |
| override fun hashCode(): Int { |
| var result = start.hashCode() |
| result = 31 * result + top.hashCode() |
| result = 31 * result + end.hashCode() |
| result = 31 * result + bottom.hashCode() |
| result = 31 * result + rtlAware.hashCode() |
| return result |
| } |
| |
| override fun equals(other: Any?): Boolean { |
| val otherModifierElement = other as? PaddingElement ?: return false |
| return start == otherModifierElement.start && |
| top == otherModifierElement.top && |
| end == otherModifierElement.end && |
| bottom == otherModifierElement.bottom && |
| rtlAware == otherModifierElement.rtlAware |
| } |
| |
| override fun InspectorInfo.inspectableProperties() { |
| inspectorInfo() |
| } |
| } |
| |
| private class PaddingNode( |
| var start: Dp = 0.dp, |
| var top: Dp = 0.dp, |
| var end: Dp = 0.dp, |
| var bottom: Dp = 0.dp, |
| var rtlAware: Boolean, |
| ) : LayoutModifierNode, Modifier.Node() { |
| |
| override fun MeasureScope.measure( |
| measurable: Measurable, |
| constraints: Constraints, |
| ): MeasureResult { |
| |
| val horizontal = start.roundToPx() + end.roundToPx() |
| val vertical = top.roundToPx() + bottom.roundToPx() |
| |
| val placeable = measurable.measure(constraints.offset(-horizontal, -vertical)) |
| |
| val width = constraints.constrainWidth(placeable.width + horizontal) |
| val height = constraints.constrainHeight(placeable.height + vertical) |
| return layout(width, height) { |
| if (rtlAware) { |
| placeable.placeRelative(start.roundToPx(), top.roundToPx()) |
| } else { |
| placeable.place(start.roundToPx(), top.roundToPx()) |
| } |
| } |
| } |
| } |
| |
| private class PaddingValuesElement( |
| val paddingValues: PaddingValues, |
| val inspectorInfo: InspectorInfo.() -> Unit, |
| ) : ModifierNodeElement<PaddingValuesModifier>() { |
| override fun create(): PaddingValuesModifier { |
| return PaddingValuesModifier(paddingValues) |
| } |
| |
| override fun update(node: PaddingValuesModifier) { |
| node.paddingValues = paddingValues |
| } |
| |
| override fun hashCode(): Int = paddingValues.hashCode() |
| |
| override fun equals(other: Any?): Boolean { |
| val otherElement = other as? PaddingValuesElement ?: return false |
| return paddingValues == otherElement.paddingValues |
| } |
| |
| override fun InspectorInfo.inspectableProperties() { |
| inspectorInfo() |
| } |
| } |
| |
| private class PaddingValuesModifier(var paddingValues: PaddingValues) : |
| LayoutModifierNode, Modifier.Node() { |
| override fun MeasureScope.measure( |
| measurable: Measurable, |
| constraints: Constraints, |
| ): MeasureResult { |
| val leftPadding = paddingValues.calculateLeftPadding(layoutDirection) |
| val topPadding = paddingValues.calculateTopPadding() |
| val rightPadding = paddingValues.calculateRightPadding(layoutDirection) |
| val bottomPadding = paddingValues.calculateBottomPadding() |
| |
| requirePrecondition( |
| (leftPadding >= 0.dp) and |
| (topPadding >= 0.dp) and |
| (rightPadding >= 0.dp) and |
| (bottomPadding >= 0.dp) |
| ) { |
| "Padding must be non-negative" |
| } |
| |
| val roundedLeftPadding = leftPadding.roundToPx() |
| val horizontal = roundedLeftPadding + rightPadding.roundToPx() |
| |
| val roundedTopPadding = topPadding.roundToPx() |
| val vertical = roundedTopPadding + bottomPadding.roundToPx() |
| |
| val placeable = measurable.measure(constraints.offset(-horizontal, -vertical)) |
| |
| val width = constraints.constrainWidth(placeable.width + horizontal) |
| val height = constraints.constrainHeight(placeable.height + vertical) |
| return layout(width, height) { placeable.place(roundedLeftPadding, roundedTopPadding) } |
| } |
| } |