Add fill and wrapContent modifiers (#252)

This commit is contained in:
EpicDima
2023-11-19 06:26:59 +03:00
committed by GitHub
parent e8e498e34f
commit d491a59894

View File

@ -2,11 +2,14 @@ package com.jakewharton.mosaic.layout
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import com.jakewharton.mosaic.modifier.Modifier import com.jakewharton.mosaic.modifier.Modifier
import com.jakewharton.mosaic.ui.Alignment
import com.jakewharton.mosaic.ui.unit.Constraints import com.jakewharton.mosaic.ui.unit.Constraints
import com.jakewharton.mosaic.ui.unit.IntOffset
import com.jakewharton.mosaic.ui.unit.IntSize import com.jakewharton.mosaic.ui.unit.IntSize
import com.jakewharton.mosaic.ui.unit.constrain import com.jakewharton.mosaic.ui.unit.constrain
import com.jakewharton.mosaic.ui.unit.constrainHeight import com.jakewharton.mosaic.ui.unit.constrainHeight
import com.jakewharton.mosaic.ui.unit.constrainWidth import com.jakewharton.mosaic.ui.unit.constrainWidth
import kotlin.math.roundToInt
/** /**
* Declare the preferred width of the content to be exactly [width]. The incoming measurement * Declare the preferred width of the content to be exactly [width]. The incoming measurement
@ -316,6 +319,155 @@ public fun Modifier.requiredSizeIn(
), ),
) )
/**
* Have the content fill (possibly only partially) the [Constraints.maxWidth] of the incoming
* measurement constraints, by setting the [minimum width][Constraints.minWidth] and the
* [maximum width][Constraints.maxWidth] to be equal to the [maximum width][Constraints.maxWidth]
* multiplied by [fraction]. Note that, by default, the [fraction] is 1, so the modifier will
* make the content fill the whole available width. If the incoming maximum width is
* [Constraints.Infinity] this modifier will have no effect.
*
* @param fraction The fraction of the maximum width to use, between `0` and `1`, inclusive.
*/
@Stable
public fun Modifier.fillMaxWidth(fraction: Float = 1f): Modifier {
require(fraction in 0.0f..1.0f) { "Fraction must be >= 0 and <= 1" }
return this.then(if (fraction == 1f) FillWholeMaxWidth else FillModifier.width(fraction))
}
private val FillWholeMaxWidth = FillModifier.width(1f)
/**
* Have the content fill (possibly only partially) the [Constraints.maxHeight] of the incoming
* measurement constraints, by setting the [minimum height][Constraints.minHeight] and the
* [maximum height][Constraints.maxHeight] to be equal to the
* [maximum height][Constraints.maxHeight] multiplied by [fraction]. Note that, by default,
* the [fraction] is 1, so the modifier will make the content fill the whole available height.
* If the incoming maximum height is [Constraints.Infinity] this modifier will have no effect.
*
* @param fraction The fraction of the maximum height to use, between `0` and `1`, inclusive.
*/
@Stable
public fun Modifier.fillMaxHeight(fraction: Float = 1f): Modifier {
require(fraction in 0.0f..1.0f) { "Fraction must be >= 0 and <= 1" }
return this.then(if (fraction == 1f) FillWholeMaxHeight else FillModifier.height(fraction))
}
private val FillWholeMaxHeight = FillModifier.height(1f)
/**
* Have the content fill (possibly only partially) the [Constraints.maxWidth] and
* [Constraints.maxHeight] of the incoming measurement constraints, by setting the
* [minimum width][Constraints.minWidth] and the [maximum width][Constraints.maxWidth] to be
* equal to the [maximum width][Constraints.maxWidth] multiplied by [fraction], as well as
* the [minimum height][Constraints.minHeight] and the [maximum height][Constraints.minHeight]
* to be equal to the [maximum height][Constraints.maxHeight] multiplied by [fraction].
* Note that, by default, the [fraction] is 1, so the modifier will make the content fill
* the whole available space.
* If the incoming maximum width or height is [Constraints.Infinity] this modifier will have no
* effect in that dimension.
*
* @param fraction The fraction of the maximum size to use, between `0` and `1`, inclusive.
*/
@Stable
public fun Modifier.fillMaxSize(fraction: Float = 1f): Modifier {
require(fraction in 0.0f..1.0f) { "Fraction must be >= 0 and <= 1" }
return this.then(if (fraction == 1f) FillWholeMaxSize else FillModifier.size(fraction))
}
private val FillWholeMaxSize = FillModifier.size(1f)
/**
* Allow the content to measure at its desired width without regard for the incoming measurement
* [minimum width constraint][Constraints.minWidth], and, if [unbounded] is true, also without
* regard for the incoming measurement [maximum width constraint][Constraints.maxWidth]. If
* the content's measured size is smaller than the minimum width constraint, [align]
* it within that minimum width space. If the content's measured size is larger than the maximum
* width constraint (only possible when [unbounded] is true), [align] over the maximum
* width space.
*/
@Stable
public fun Modifier.wrapContentWidth(
align: Alignment.Horizontal = Alignment.CenterHorizontally,
unbounded: Boolean = false,
): Modifier = this.then(
if (align == Alignment.CenterHorizontally && !unbounded) {
WrapContentWidthCenter
} else if (align == Alignment.Start && !unbounded) {
WrapContentWidthStart
} else {
WrapContentModifier.width(align, unbounded)
},
)
private val WrapContentWidthCenter =
WrapContentModifier.width(Alignment.CenterHorizontally, false)
private val WrapContentWidthStart = WrapContentModifier.width(Alignment.Start, false)
/**
* Allow the content to measure at its desired height without regard for the incoming measurement
* [minimum height constraint][Constraints.minHeight], and, if [unbounded] is true, also without
* regard for the incoming measurement [maximum height constraint][Constraints.maxHeight]. If the
* content's measured size is smaller than the minimum height constraint, [align] it within
* that minimum height space. If the content's measured size is larger than the maximum height
* constraint (only possible when [unbounded] is true), [align] over the maximum height space.
*/
@Stable
public fun Modifier.wrapContentHeight(
align: Alignment.Vertical = Alignment.CenterVertically,
unbounded: Boolean = false,
): Modifier = this.then(
if (align == Alignment.CenterVertically && !unbounded) {
WrapContentHeightCenter
} else if (align == Alignment.Top && !unbounded) {
WrapContentHeightTop
} else {
WrapContentModifier.height(align, unbounded)
},
)
private val WrapContentHeightCenter =
WrapContentModifier.height(Alignment.CenterVertically, false)
private val WrapContentHeightTop = WrapContentModifier.height(Alignment.Top, false)
/**
* Allow the content to measure at its desired size without regard for the incoming measurement
* [minimum width][Constraints.minWidth] or [minimum height][Constraints.minHeight] constraints,
* and, if [unbounded] is true, also without regard for the incoming maximum constraints.
* If the content's measured size is smaller than the minimum size constraint, [align] it
* within that minimum sized space. If the content's measured size is larger than the maximum
* size constraint (only possible when [unbounded] is true), [align] within the maximum space.
*/
@Stable
public fun Modifier.wrapContentSize(
align: Alignment = Alignment.Center,
unbounded: Boolean = false,
): Modifier = this.then(
if (align == Alignment.Center && !unbounded) {
WrapContentSizeCenter
} else if (align == Alignment.TopStart && !unbounded) {
WrapContentSizeTopStart
} else {
WrapContentModifier.size(align, unbounded)
},
)
private val WrapContentSizeCenter = WrapContentModifier.size(Alignment.Center, false)
private val WrapContentSizeTopStart = WrapContentModifier.size(Alignment.TopStart, false)
/**
* Constrain the size of the wrapped layout only when it would be otherwise unconstrained:
* the [minWidth] and [minHeight] constraints are only applied when the incoming corresponding
* constraint is `0`.
* The modifier can be used, for example, to define a default min size of a component,
* while still allowing it to be overidden with smaller min sizes across usages.
*/
@Stable
public fun Modifier.defaultMinSize(
minWidth: Int = Unspecified,
minHeight: Int = Unspecified,
): Modifier = this.then(UnspecifiedConstraintsModifier(minWidth = minWidth, minHeight = minHeight))
private class SizeModifier( private class SizeModifier(
private val minWidth: Int = Unspecified, private val minWidth: Int = Unspecified,
private val minHeight: Int = Unspecified, private val minHeight: Int = Unspecified,
@ -467,4 +619,214 @@ private class SizeModifier(
} }
} }
private class FillModifier(
private val direction: Direction,
private val fraction: Float,
) : LayoutModifier {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints,
): MeasureResult {
val minWidth: Int
val maxWidth: Int
if (constraints.hasBoundedWidth && direction != Direction.Vertical) {
val width = (constraints.maxWidth * fraction).roundToInt()
.coerceIn(constraints.minWidth, constraints.maxWidth)
minWidth = width
maxWidth = width
} else {
minWidth = constraints.minWidth
maxWidth = constraints.maxWidth
}
val minHeight: Int
val maxHeight: Int
if (constraints.hasBoundedHeight && direction != Direction.Horizontal) {
val height = (constraints.maxHeight * fraction).roundToInt()
.coerceIn(constraints.minHeight, constraints.maxHeight)
minHeight = height
maxHeight = height
} else {
minHeight = constraints.minHeight
maxHeight = constraints.maxHeight
}
val placeable = measurable.measure(
Constraints(minWidth, maxWidth, minHeight, maxHeight),
)
return layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
override fun toString(): String = "Fill(direction=$direction, fraction=$fraction)"
companion object {
@Stable
fun width(fraction: Float) = FillModifier(
direction = Direction.Horizontal,
fraction = fraction,
)
@Stable
fun height(fraction: Float) = FillModifier(
direction = Direction.Vertical,
fraction = fraction,
)
@Stable
fun size(fraction: Float) = FillModifier(
direction = Direction.Both,
fraction = fraction,
)
}
}
private class WrapContentModifier(
private val direction: Direction,
private val unbounded: Boolean,
private val alignmentCallback: (IntSize) -> IntOffset,
) : LayoutModifier {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints,
): MeasureResult {
val wrappedConstraints = Constraints(
minWidth = if (direction != Direction.Vertical) 0 else constraints.minWidth,
minHeight = if (direction != Direction.Horizontal) 0 else constraints.minHeight,
maxWidth = if (direction != Direction.Vertical && unbounded) {
Constraints.Infinity
} else {
constraints.maxWidth
},
maxHeight = if (direction != Direction.Horizontal && unbounded) {
Constraints.Infinity
} else {
constraints.maxHeight
},
)
val placeable = measurable.measure(wrappedConstraints)
val wrapperWidth = placeable.width.coerceIn(constraints.minWidth, constraints.maxWidth)
val wrapperHeight = placeable.height.coerceIn(constraints.minHeight, constraints.maxHeight)
return layout(
wrapperWidth,
wrapperHeight,
) {
val position = alignmentCallback(
IntSize(wrapperWidth - placeable.width, wrapperHeight - placeable.height),
)
placeable.place(position)
}
}
override fun toString(): String = "WrapContent(direction=$direction, unbounded=$unbounded)"
companion object {
@Stable
fun width(
align: Alignment.Horizontal,
unbounded: Boolean,
) = WrapContentModifier(
direction = Direction.Horizontal,
unbounded = unbounded,
alignmentCallback = { size -> IntOffset(align.align(0, size.width), 0) },
)
@Stable
fun height(
align: Alignment.Vertical,
unbounded: Boolean,
) = WrapContentModifier(
direction = Direction.Vertical,
unbounded = unbounded,
alignmentCallback = { size -> IntOffset(0, align.align(0, size.height)) },
)
@Stable
fun size(
align: Alignment,
unbounded: Boolean,
) = WrapContentModifier(
direction = Direction.Both,
unbounded = unbounded,
alignmentCallback = { size -> align.align(IntSize.Zero, size) },
)
}
}
private class UnspecifiedConstraintsModifier(
private val minWidth: Int = Unspecified,
private val minHeight: Int = Unspecified,
) : LayoutModifier {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints,
): MeasureResult {
val wrappedConstraints = Constraints(
if (minWidth != Unspecified && constraints.minWidth == 0) {
minWidth.coerceAtMost(constraints.maxWidth).coerceAtLeast(0)
} else {
constraints.minWidth
},
constraints.maxWidth,
if (minHeight != Unspecified && constraints.minHeight == 0) {
minHeight.coerceAtMost(constraints.maxHeight).coerceAtLeast(0)
} else {
constraints.minHeight
},
constraints.maxHeight,
)
val placeable = measurable.measure(wrappedConstraints)
return layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
override fun minIntrinsicWidth(
measurable: IntrinsicMeasurable,
height: Int,
) = measurable.minIntrinsicWidth(height).coerceAtLeast(
if (minWidth != Unspecified) minWidth else 0,
)
override fun maxIntrinsicWidth(
measurable: IntrinsicMeasurable,
height: Int,
) = measurable.maxIntrinsicWidth(height).coerceAtLeast(
if (minWidth != Unspecified) minWidth else 0,
)
override fun minIntrinsicHeight(
measurable: IntrinsicMeasurable,
width: Int,
) = measurable.minIntrinsicHeight(width).coerceAtLeast(
if (minHeight != Unspecified) minHeight else 0,
)
override fun maxIntrinsicHeight(
measurable: IntrinsicMeasurable,
width: Int,
) = measurable.maxIntrinsicHeight(width).coerceAtLeast(
if (minHeight != Unspecified) minHeight else 0,
)
override fun toString(): String {
val params = buildList {
if (minWidth != Unspecified) {
add("minW=$minWidth")
}
if (minHeight != Unspecified) {
add("minH=$minHeight")
}
}
return "UnspecifiedConstraints(${params.joinToString(", ")})"
}
}
internal enum class Direction {
Vertical,
Horizontal,
Both,
}
private const val Unspecified = Int.MIN_VALUE private const val Unspecified = Int.MIN_VALUE