1 /*
2  * Copyright 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.compose.foundation.layout
18 
19 import androidx.compose.foundation.layout.PaddingValues.Absolute
20 import androidx.compose.foundation.layout.internal.requirePrecondition
21 import androidx.compose.runtime.Immutable
22 import androidx.compose.runtime.Stable
23 import androidx.compose.ui.Modifier
24 import androidx.compose.ui.layout.Measurable
25 import androidx.compose.ui.layout.MeasureResult
26 import androidx.compose.ui.layout.MeasureScope
27 import androidx.compose.ui.node.LayoutModifierNode
28 import androidx.compose.ui.node.ModifierNodeElement
29 import androidx.compose.ui.platform.InspectorInfo
30 import androidx.compose.ui.unit.Constraints
31 import androidx.compose.ui.unit.Dp
32 import androidx.compose.ui.unit.LayoutDirection
33 import androidx.compose.ui.unit.constrainHeight
34 import androidx.compose.ui.unit.constrainWidth
35 import androidx.compose.ui.unit.dp
36 import androidx.compose.ui.unit.isUnspecified
37 import androidx.compose.ui.unit.offset
38 
39 /**
40  * Apply additional space along each edge of the content in [Dp]: [start], [top], [end] and
41  * [bottom]. The start and end edges will be determined by the current [LayoutDirection]. Padding is
42  * applied before content measurement and takes precedence; content may only be as large as the
43  * remaining space.
44  *
45  * Negative padding is not permitted — it will cause [IllegalArgumentException]. See
46  * [Modifier.offset].
47  *
48  * Example usage:
49  *
50  * @sample androidx.compose.foundation.layout.samples.PaddingModifier
51  */
52 @Stable
paddingnull53 fun Modifier.padding(start: Dp = 0.dp, top: Dp = 0.dp, end: Dp = 0.dp, bottom: Dp = 0.dp) =
54     this then
55         PaddingElement(
56             start = start,
57             top = top,
58             end = end,
59             bottom = bottom,
60             rtlAware = true,
61             inspectorInfo = {
62                 name = "padding"
63                 properties["start"] = start
64                 properties["top"] = top
65                 properties["end"] = end
66                 properties["bottom"] = bottom
67             }
68         )
69 
70 /**
71  * Apply [horizontal] dp space along the left and right edges of the content, and [vertical] dp
72  * space along the top and bottom edges. Padding is applied before content measurement and takes
73  * precedence; content may only be as large as the remaining space.
74  *
75  * Negative padding is not permitted — it will cause [IllegalArgumentException]. See
76  * [Modifier.offset].
77  *
78  * Example usage:
79  *
80  * @sample androidx.compose.foundation.layout.samples.SymmetricPaddingModifier
81  */
82 @Stable
paddingnull83 fun Modifier.padding(horizontal: Dp = 0.dp, vertical: Dp = 0.dp) =
84     this then
85         PaddingElement(
86             start = horizontal,
87             top = vertical,
88             end = horizontal,
89             bottom = vertical,
90             rtlAware = true,
91             inspectorInfo = {
92                 name = "padding"
93                 properties["horizontal"] = horizontal
94                 properties["vertical"] = vertical
95             }
96         )
97 
98 /**
99  * Apply [all] dp of additional space along each edge of the content, left, top, right and bottom.
100  * Padding is applied before content measurement and takes precedence; content may only be as large
101  * as the remaining space.
102  *
103  * Negative padding is not permitted — it will cause [IllegalArgumentException]. See
104  * [Modifier.offset].
105  *
106  * Example usage:
107  *
108  * @sample androidx.compose.foundation.layout.samples.PaddingAllModifier
109  */
110 @Stable
paddingnull111 fun Modifier.padding(all: Dp) =
112     this then
113         PaddingElement(
114             start = all,
115             top = all,
116             end = all,
117             bottom = all,
118             rtlAware = true,
119             inspectorInfo = {
120                 name = "padding"
121                 value = all
122             }
123         )
124 
125 /**
126  * Apply [PaddingValues] to the component as additional space along each edge of the content's left,
127  * top, right and bottom. Padding is applied before content measurement and takes precedence;
128  * content may only be as large as the remaining space.
129  *
130  * Negative padding is not permitted — it will cause [IllegalArgumentException]. See
131  * [Modifier.offset].
132  *
133  * Example usage:
134  *
135  * @sample androidx.compose.foundation.layout.samples.PaddingValuesModifier
136  */
137 @Stable
paddingnull138 fun Modifier.padding(paddingValues: PaddingValues) =
139     this then
140         PaddingValuesElement(
141             paddingValues = paddingValues,
142             inspectorInfo = {
143                 name = "padding"
144                 properties["paddingValues"] = paddingValues
145             }
146         )
147 
148 /**
149  * Apply additional space along each edge of the content in [Dp]: [left], [top], [right] and
150  * [bottom]. These paddings are applied without regard to the current [LayoutDirection], see
151  * [padding] to apply relative paddings. Padding is applied before content measurement and takes
152  * precedence; content may only be as large as the remaining space.
153  *
154  * Negative padding is not permitted — it will cause [IllegalArgumentException]. See
155  * [Modifier.offset].
156  *
157  * Example usage:
158  *
159  * @sample androidx.compose.foundation.layout.samples.AbsolutePaddingModifier
160  */
161 @Stable
absolutePaddingnull162 fun Modifier.absolutePadding(left: Dp = 0.dp, top: Dp = 0.dp, right: Dp = 0.dp, bottom: Dp = 0.dp) =
163     this then
164         (PaddingElement(
165             start = left,
166             top = top,
167             end = right,
168             bottom = bottom,
169             rtlAware = false,
170             inspectorInfo = {
171                 name = "absolutePadding"
172                 properties["left"] = left
173                 properties["top"] = top
174                 properties["right"] = right
175                 properties["bottom"] = bottom
176             }
177         ))
178 
179 /**
180  * Describes a padding to be applied along the edges inside a box. See the [PaddingValues] factories
181  * and [Absolute] for convenient ways to build [PaddingValues].
182  */
183 @Stable
184 interface PaddingValues {
185     /** The padding to be applied along the left edge inside a box. */
calculateLeftPaddingnull186     fun calculateLeftPadding(layoutDirection: LayoutDirection): Dp
187 
188     /** The padding to be applied along the top edge inside a box. */
189     fun calculateTopPadding(): Dp
190 
191     /** The padding to be applied along the right edge inside a box. */
192     fun calculateRightPadding(layoutDirection: LayoutDirection): Dp
193 
194     /** The padding to be applied along the bottom edge inside a box. */
195     fun calculateBottomPadding(): Dp
196 
197     /** Describes an absolute (RTL unaware) padding to be applied along the edges inside a box. */
198     @Immutable
199     class Absolute(
200         @Stable private val left: Dp = 0.dp,
201         @Stable private val top: Dp = 0.dp,
202         @Stable private val right: Dp = 0.dp,
203         @Stable private val bottom: Dp = 0.dp
204     ) : PaddingValues {
205 
206         init {
207             requirePrecondition(
208                 (left.value >= 0f) and
209                     (top.value >= 0f) and
210                     (right.value >= 0f) and
211                     (bottom.value >= 0f)
212             ) {
213                 "Padding must be non-negative"
214             }
215         }
216 
217         override fun calculateLeftPadding(layoutDirection: LayoutDirection) = left
218 
219         override fun calculateTopPadding() = top
220 
221         override fun calculateRightPadding(layoutDirection: LayoutDirection) = right
222 
223         override fun calculateBottomPadding() = bottom
224 
225         override fun equals(other: Any?): Boolean {
226             if (other !is Absolute) return false
227             return left == other.left &&
228                 top == other.top &&
229                 right == other.right &&
230                 bottom == other.bottom
231         }
232 
233         override fun hashCode() =
234             ((left.hashCode() * 31 + top.hashCode()) * 31 + right.hashCode()) * 31 +
235                 bottom.hashCode()
236 
237         override fun toString() =
238             "PaddingValues.Absolute(left=$left, top=$top, right=$right, bottom=$bottom)"
239     }
240 
241     companion object {
242         /** PaddingValues with all values `0.dp`. */
243         @Stable @get:Stable val Zero: PaddingValues = Absolute()
244     }
245 }
246 
247 /**
248  * The padding to be applied along the start edge inside a box: along the left edge if the layout
249  * direction is LTR, or along the right edge for RTL.
250  */
251 @Stable
PaddingValuesnull252 fun PaddingValues.calculateStartPadding(layoutDirection: LayoutDirection) =
253     if (layoutDirection == LayoutDirection.Ltr) {
254         calculateLeftPadding(layoutDirection)
255     } else {
256         calculateRightPadding(layoutDirection)
257     }
258 
259 /**
260  * The padding to be applied along the end edge inside a box: along the right edge if the layout
261  * direction is LTR, or along the left edge for RTL.
262  */
263 @Stable
PaddingValuesnull264 fun PaddingValues.calculateEndPadding(layoutDirection: LayoutDirection) =
265     if (layoutDirection == LayoutDirection.Ltr) {
266         calculateRightPadding(layoutDirection)
267     } else {
268         calculateLeftPadding(layoutDirection)
269     }
270 
271 /** Creates a padding of [all] dp along all 4 edges. */
PaddingValuesnull272 @Stable fun PaddingValues(all: Dp): PaddingValues = PaddingValuesImpl(all, all, all, all)
273 
274 /**
275  * Creates a padding of [horizontal] dp along the left and right edges, and of [vertical] dp along
276  * the top and bottom edges.
277  */
278 @Stable
279 fun PaddingValues(horizontal: Dp = 0.dp, vertical: Dp = 0.dp): PaddingValues =
280     PaddingValuesImpl(horizontal, vertical, horizontal, vertical)
281 
282 /**
283  * Creates a padding to be applied along the edges inside a box. In LTR contexts [start] will be
284  * applied along the left edge and [end] will be applied along the right edge. In RTL contexts,
285  * [start] will correspond to the right edge and [end] to the left.
286  */
287 @Stable
288 fun PaddingValues(
289     start: Dp = 0.dp,
290     top: Dp = 0.dp,
291     end: Dp = 0.dp,
292     bottom: Dp = 0.dp
293 ): PaddingValues = PaddingValuesImpl(start, top, end, bottom)
294 
295 @Immutable
296 internal class PaddingValuesImpl(
297     @Stable val start: Dp = 0.dp,
298     @Stable val top: Dp = 0.dp,
299     @Stable val end: Dp = 0.dp,
300     @Stable val bottom: Dp = 0.dp
301 ) : PaddingValues {
302 
303     init {
304         requirePrecondition(
305             (start.value >= 0f) and (top.value >= 0f) and (end.value >= 0f) and (bottom.value >= 0f)
306         ) {
307             "Padding must be non-negative"
308         }
309     }
310 
311     override fun calculateLeftPadding(layoutDirection: LayoutDirection) =
312         if (layoutDirection == LayoutDirection.Ltr) start else end
313 
314     override fun calculateTopPadding() = top
315 
316     override fun calculateRightPadding(layoutDirection: LayoutDirection) =
317         if (layoutDirection == LayoutDirection.Ltr) end else start
318 
319     override fun calculateBottomPadding() = bottom
320 
321     override fun equals(other: Any?): Boolean {
322         if (other !is PaddingValuesImpl) return false
323         return start == other.start &&
324             top == other.top &&
325             end == other.end &&
326             bottom == other.bottom
327     }
328 
329     override fun hashCode() =
330         ((start.hashCode() * 31 + top.hashCode()) * 31 + end.hashCode()) * 31 + bottom.hashCode()
331 
332     override fun toString() = "PaddingValues(start=$start, top=$top, end=$end, bottom=$bottom)"
333 }
334 
335 private class PaddingElement(
336     var start: Dp = 0.dp,
337     var top: Dp = 0.dp,
338     var end: Dp = 0.dp,
339     var bottom: Dp = 0.dp,
340     var rtlAware: Boolean,
341     val inspectorInfo: InspectorInfo.() -> Unit
342 ) : ModifierNodeElement<PaddingNode>() {
343 
344     init {
345         requirePrecondition(
346             (start.value >= 0f || start.isUnspecified) and
347                 (top.value >= 0f || top.isUnspecified) and
348                 (end.value >= 0f || end.isUnspecified) and
349                 (bottom.value >= 0f || bottom.isUnspecified)
<lambda>null350         ) {
351             "Padding must be non-negative"
352         }
353     }
354 
createnull355     override fun create(): PaddingNode {
356         return PaddingNode(start, top, end, bottom, rtlAware)
357     }
358 
updatenull359     override fun update(node: PaddingNode) {
360         node.start = start
361         node.top = top
362         node.end = end
363         node.bottom = bottom
364         node.rtlAware = rtlAware
365     }
366 
hashCodenull367     override fun hashCode(): Int {
368         var result = start.hashCode()
369         result = 31 * result + top.hashCode()
370         result = 31 * result + end.hashCode()
371         result = 31 * result + bottom.hashCode()
372         result = 31 * result + rtlAware.hashCode()
373         return result
374     }
375 
equalsnull376     override fun equals(other: Any?): Boolean {
377         val otherModifierElement = other as? PaddingElement ?: return false
378         return start == otherModifierElement.start &&
379             top == otherModifierElement.top &&
380             end == otherModifierElement.end &&
381             bottom == otherModifierElement.bottom &&
382             rtlAware == otherModifierElement.rtlAware
383     }
384 
inspectablePropertiesnull385     override fun InspectorInfo.inspectableProperties() {
386         inspectorInfo()
387     }
388 }
389 
390 private class PaddingNode(
391     var start: Dp = 0.dp,
392     var top: Dp = 0.dp,
393     var end: Dp = 0.dp,
394     var bottom: Dp = 0.dp,
395     var rtlAware: Boolean
396 ) : LayoutModifierNode, Modifier.Node() {
397 
measurenull398     override fun MeasureScope.measure(
399         measurable: Measurable,
400         constraints: Constraints
401     ): MeasureResult {
402 
403         val horizontal = start.roundToPx() + end.roundToPx()
404         val vertical = top.roundToPx() + bottom.roundToPx()
405 
406         val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))
407 
408         val width = constraints.constrainWidth(placeable.width + horizontal)
409         val height = constraints.constrainHeight(placeable.height + vertical)
410         return layout(width, height) {
411             if (rtlAware) {
412                 placeable.placeRelative(start.roundToPx(), top.roundToPx())
413             } else {
414                 placeable.place(start.roundToPx(), top.roundToPx())
415             }
416         }
417     }
418 }
419 
420 private class PaddingValuesElement(
421     val paddingValues: PaddingValues,
422     val inspectorInfo: InspectorInfo.() -> Unit
423 ) : ModifierNodeElement<PaddingValuesModifier>() {
createnull424     override fun create(): PaddingValuesModifier {
425         return PaddingValuesModifier(paddingValues)
426     }
427 
updatenull428     override fun update(node: PaddingValuesModifier) {
429         node.paddingValues = paddingValues
430     }
431 
hashCodenull432     override fun hashCode(): Int = paddingValues.hashCode()
433 
434     override fun equals(other: Any?): Boolean {
435         val otherElement = other as? PaddingValuesElement ?: return false
436         return paddingValues == otherElement.paddingValues
437     }
438 
inspectablePropertiesnull439     override fun InspectorInfo.inspectableProperties() {
440         inspectorInfo()
441     }
442 }
443 
444 private class PaddingValuesModifier(var paddingValues: PaddingValues) :
445     LayoutModifierNode, Modifier.Node() {
measurenull446     override fun MeasureScope.measure(
447         measurable: Measurable,
448         constraints: Constraints
449     ): MeasureResult {
450         val leftPadding = paddingValues.calculateLeftPadding(layoutDirection)
451         val topPadding = paddingValues.calculateTopPadding()
452         val rightPadding = paddingValues.calculateRightPadding(layoutDirection)
453         val bottomPadding = paddingValues.calculateBottomPadding()
454 
455         requirePrecondition(
456             (leftPadding >= 0.dp) and
457                 (topPadding >= 0.dp) and
458                 (rightPadding >= 0.dp) and
459                 (bottomPadding >= 0.dp)
460         ) {
461             "Padding must be non-negative"
462         }
463 
464         val roundedLeftPadding = leftPadding.roundToPx()
465         val horizontal = roundedLeftPadding + rightPadding.roundToPx()
466 
467         val roundedTopPadding = topPadding.roundToPx()
468         val vertical = roundedTopPadding + bottomPadding.roundToPx()
469 
470         val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))
471 
472         val width = constraints.constrainWidth(placeable.width + horizontal)
473         val height = constraints.constrainHeight(placeable.height + vertical)
474         return layout(width, height) { placeable.place(roundedLeftPadding, roundedTopPadding) }
475     }
476 }
477