mirror of
https://github.com/JakeWharton/mosaic.git
synced 2025-11-01 12:01:22 +08:00
Rewrite node to use modifiers (#172)
This commit is contained in:
@ -17,39 +17,6 @@ internal interface TextCanvas {
|
||||
var translationY: Int
|
||||
|
||||
operator fun get(row: Int, column: Int): TextPixel
|
||||
|
||||
fun write(
|
||||
row: Int,
|
||||
column: Int,
|
||||
string: String,
|
||||
foreground: Color? = null,
|
||||
background: Color? = null,
|
||||
style: TextStyle? = null,
|
||||
) {
|
||||
var pixelIndex = 0
|
||||
var characterColumn = column
|
||||
while (pixelIndex < string.length) {
|
||||
val character = this[row, characterColumn++]
|
||||
|
||||
val pixelEnd = if (string[pixelIndex].isHighSurrogate()) {
|
||||
pixelIndex + 2
|
||||
} else {
|
||||
pixelIndex + 1
|
||||
}
|
||||
character.value = string.substring(pixelIndex, pixelEnd)
|
||||
pixelIndex = pixelEnd
|
||||
|
||||
if (background != null) {
|
||||
character.background = background
|
||||
}
|
||||
if (foreground != null) {
|
||||
character.foreground = foreground
|
||||
}
|
||||
if (style != null) {
|
||||
character.style = style
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val blankPixel = TextPixel(' ')
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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 com.jakewharton.mosaic.layout
|
||||
|
||||
public interface ContentDrawScope : DrawScope {
|
||||
public fun drawContent()
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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 com.jakewharton.mosaic.layout
|
||||
|
||||
import com.jakewharton.mosaic.modifier.Modifier
|
||||
|
||||
public interface DrawModifier : Modifier.Element {
|
||||
public fun ContentDrawScope.draw()
|
||||
}
|
||||
|
||||
public fun Modifier.drawBehind(
|
||||
onDraw: DrawScope.() -> Unit
|
||||
): Modifier = this then DrawBehindElement(onDraw)
|
||||
|
||||
private class DrawBehindElement(
|
||||
val onDraw: DrawScope.() -> Unit
|
||||
): DrawModifier {
|
||||
override fun ContentDrawScope.draw() {
|
||||
onDraw()
|
||||
drawContent()
|
||||
}
|
||||
|
||||
override fun toString() = "DrawBehind"
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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 com.jakewharton.mosaic.layout
|
||||
|
||||
import com.jakewharton.mosaic.TextCanvas
|
||||
import com.jakewharton.mosaic.ui.Color
|
||||
import com.jakewharton.mosaic.ui.TextStyle
|
||||
|
||||
public interface DrawScope {
|
||||
public fun write(
|
||||
row: Int,
|
||||
column: Int,
|
||||
string: String,
|
||||
foreground: Color? = null,
|
||||
background: Color? = null,
|
||||
style: TextStyle? = null,
|
||||
)
|
||||
}
|
||||
|
||||
internal open class TextCanvasDrawScope(
|
||||
private val canvas: TextCanvas,
|
||||
): DrawScope {
|
||||
override fun write(
|
||||
row: Int,
|
||||
column: Int,
|
||||
string: String,
|
||||
foreground: Color?,
|
||||
background: Color?,
|
||||
style: TextStyle?,
|
||||
) {
|
||||
var pixelIndex = 0
|
||||
var characterColumn = column
|
||||
while (pixelIndex < string.length) {
|
||||
val character = canvas[row, characterColumn++]
|
||||
|
||||
val pixelEnd = if (string[pixelIndex].isHighSurrogate()) {
|
||||
pixelIndex + 2
|
||||
} else {
|
||||
pixelIndex + 1
|
||||
}
|
||||
character.value = string.substring(pixelIndex, pixelEnd)
|
||||
pixelIndex = pixelEnd
|
||||
|
||||
if (background != null) {
|
||||
character.background = background
|
||||
}
|
||||
if (foreground != null) {
|
||||
character.foreground = foreground
|
||||
}
|
||||
if (style != null) {
|
||||
character.style = style
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2020 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 com.jakewharton.mosaic.layout
|
||||
|
||||
import com.jakewharton.mosaic.modifier.Modifier
|
||||
|
||||
/**
|
||||
* A [Modifier.Element] that changes how its wrapped content is measured and laid out.
|
||||
* It has the same measurement and layout functionality as the [com.jakewharton.mosaic.layout.Layout]
|
||||
* component, while wrapping exactly one layout due to it being a modifier. In contrast,
|
||||
* the [com.jakewharton.mosaic.layout.Layout] component is used to define the layout behavior of
|
||||
* multiple children.
|
||||
*
|
||||
* @see com.jakewharton.mosaic.layout.Layout
|
||||
*/
|
||||
public interface LayoutModifier : Modifier.Element {
|
||||
/**
|
||||
* The function used to measure the modifier. The [measurable] corresponds to the
|
||||
* wrapped content, and it can be measured with the desired constraints according
|
||||
* to the logic of the [LayoutModifier]. The modifier needs to choose its own
|
||||
* size, which can depend on the size chosen by the wrapped content (the obtained
|
||||
* [Placeable]), if the wrapped content was measured. The size needs to be returned
|
||||
* as part of a [MeasureResult], alongside the placement logic of the
|
||||
* [Placeable], which defines how the wrapped content should be positioned inside
|
||||
* the [LayoutModifier]. A convenient way to create the [MeasureResult]
|
||||
* is to use the [MeasureScope.layout] factory function.
|
||||
*
|
||||
* A [LayoutModifier] uses the same measurement and layout concepts and principles as a
|
||||
* [Layout], the only difference is that they apply to exactly one child. For a more detailed
|
||||
* explanation of measurement and layout, see [MeasurePolicy].
|
||||
*/
|
||||
public fun MeasureScope.measure(
|
||||
measurable: Measurable,
|
||||
): MeasureResult
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a [LayoutModifier] that allows changing how the wrapped element is measured and laid out.
|
||||
*
|
||||
* This is a convenience API of creating a custom [LayoutModifier] modifier, without having to
|
||||
* create a class or an object that implements the [LayoutModifier] interface. The intrinsic
|
||||
* measurements follow the default logic provided by the [LayoutModifier].
|
||||
*
|
||||
* @see com.jakewharton.mosaic.layout.LayoutModifier
|
||||
*/
|
||||
public fun Modifier.layout(
|
||||
measure: MeasureScope.(Measurable) -> MeasureResult
|
||||
): Modifier = this then LayoutModifierElement(measure)
|
||||
|
||||
private class LayoutModifierElement(
|
||||
var measureBlock: MeasureScope.(Measurable) -> MeasureResult
|
||||
) : LayoutModifier {
|
||||
override fun MeasureScope.measure(
|
||||
measurable: Measurable,
|
||||
) = measureBlock(measurable)
|
||||
|
||||
override fun toString(): String {
|
||||
return "LayoutModifierImpl(measureBlock=$measureBlock)"
|
||||
}
|
||||
}
|
||||
@ -3,10 +3,7 @@ package com.jakewharton.mosaic.layout
|
||||
import com.jakewharton.mosaic.TextCanvas
|
||||
import com.jakewharton.mosaic.TextSurface
|
||||
import com.jakewharton.mosaic.layout.Placeable.PlacementScope
|
||||
|
||||
internal fun interface DrawPolicy {
|
||||
fun performDraw(canvas: TextCanvas)
|
||||
}
|
||||
import com.jakewharton.mosaic.modifier.Modifier
|
||||
|
||||
internal fun interface StaticPaintPolicy {
|
||||
fun MosaicNode.performPaintStatics(statics: MutableList<TextSurface>)
|
||||
@ -24,25 +21,33 @@ internal fun interface DebugPolicy {
|
||||
fun MosaicNode.renderDebug(): String
|
||||
}
|
||||
|
||||
internal abstract class MosaicNodeLayer : Placeable(), PlacementScope, MeasureScope {
|
||||
abstract fun measure(): MeasureResult
|
||||
internal abstract class MosaicNodeLayer : Measurable, Placeable(), PlacementScope, MeasureScope {
|
||||
abstract fun drawTo(canvas: TextCanvas)
|
||||
}
|
||||
|
||||
internal abstract class AbstractMosaicNodeLayer(
|
||||
private val next: MosaicNodeLayer?,
|
||||
private val isStatic: Boolean,
|
||||
) : MosaicNodeLayer() {
|
||||
private var measureResult: MeasureResult = NotMeasured
|
||||
|
||||
final override val width get() = measureResult.width
|
||||
final override val height get() = measureResult.height
|
||||
|
||||
final override fun measure(): MeasureResult {
|
||||
return doMeasure().also { measureResult = it }
|
||||
override fun measure() = apply {
|
||||
measureResult = doMeasure()
|
||||
}
|
||||
|
||||
open fun doMeasure(): MeasureResult {
|
||||
return checkNotNull(next).measure()
|
||||
protected open fun doMeasure(): MeasureResult {
|
||||
val placeable = next!!.measure()
|
||||
return object : MeasureResult {
|
||||
override val width: Int get() = placeable.width
|
||||
override val height: Int get() = placeable.height
|
||||
|
||||
override fun placeChildren() {
|
||||
placeable.place(0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final override var x = 0
|
||||
@ -51,17 +56,19 @@ internal abstract class AbstractMosaicNodeLayer(
|
||||
private set
|
||||
|
||||
final override fun placeAt(x: Int, y: Int) {
|
||||
this.x = x
|
||||
this.y = y
|
||||
// If this layer belongs to a static node, ignore the placement coordinates from the parent.
|
||||
// We reset the coordinate system to draw at 0,0 since static drawing will be on a canvas
|
||||
// sized to this node's width and height.
|
||||
if (!isStatic) {
|
||||
this.x = x
|
||||
this.y = y
|
||||
}
|
||||
measureResult.placeChildren()
|
||||
}
|
||||
|
||||
final override fun drawTo(canvas: TextCanvas) {
|
||||
drawLayer(canvas)
|
||||
override fun drawTo(canvas: TextCanvas) {
|
||||
next?.drawTo(canvas)
|
||||
}
|
||||
|
||||
open fun drawLayer(canvas: TextCanvas) {}
|
||||
}
|
||||
|
||||
internal object NotMeasured : MeasureResult {
|
||||
@ -73,17 +80,17 @@ internal object NotMeasured : MeasureResult {
|
||||
internal class MosaicNode(
|
||||
var measurePolicy: MeasurePolicy,
|
||||
var staticPaintPolicy: StaticPaintPolicy?,
|
||||
drawPolicy: DrawPolicy?,
|
||||
var debugPolicy: DebugPolicy,
|
||||
val isStatic: Boolean,
|
||||
) : Measurable {
|
||||
val children = mutableListOf<MosaicNode>()
|
||||
|
||||
private val bottomLayer: MosaicNodeLayer = object : AbstractMosaicNodeLayer(null) {
|
||||
private val bottomLayer: MosaicNodeLayer = object : AbstractMosaicNodeLayer(null, isStatic) {
|
||||
override fun doMeasure(): MeasureResult {
|
||||
return measurePolicy.run { measure(children) }
|
||||
}
|
||||
|
||||
override fun drawLayer(canvas: TextCanvas) {
|
||||
override fun drawTo(canvas: TextCanvas) {
|
||||
for (child in children) {
|
||||
if (child.width != 0 && child.height != 0) {
|
||||
child.topLayer.drawTo(canvas)
|
||||
@ -94,19 +101,36 @@ internal class MosaicNode(
|
||||
|
||||
private var topLayer = bottomLayer
|
||||
|
||||
var drawPolicy: DrawPolicy? = drawPolicy
|
||||
var modifiers: Modifier = Modifier
|
||||
set(value) {
|
||||
topLayer = if (value == null) {
|
||||
bottomLayer
|
||||
} else {
|
||||
object : AbstractMosaicNodeLayer(bottomLayer) {
|
||||
override fun drawLayer(canvas: TextCanvas) {
|
||||
canvas.translationX += x
|
||||
canvas.translationY += y
|
||||
value.performDraw(canvas)
|
||||
canvas.translationX -= x
|
||||
canvas.translationY -= y
|
||||
topLayer = value.foldOut(bottomLayer) { element, lowerLayer ->
|
||||
when (element) {
|
||||
is LayoutModifier -> {
|
||||
object : AbstractMosaicNodeLayer(lowerLayer, false) {
|
||||
override fun doMeasure(): MeasureResult {
|
||||
return element.run { measure(lowerLayer) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is DrawModifier -> {
|
||||
object : AbstractMosaicNodeLayer(lowerLayer, false) {
|
||||
override fun drawTo(canvas: TextCanvas) {
|
||||
canvas.translationX += x
|
||||
canvas.translationY += y
|
||||
val scope = object : TextCanvasDrawScope(canvas), ContentDrawScope {
|
||||
override fun drawContent() {
|
||||
lowerLayer.drawTo(canvas)
|
||||
}
|
||||
}
|
||||
element.run { scope.draw() }
|
||||
canvas.translationX -= x
|
||||
canvas.translationY -= y
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> lowerLayer
|
||||
}
|
||||
}
|
||||
field = value
|
||||
|
||||
@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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 com.jakewharton.mosaic.layout
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.jakewharton.mosaic.modifier.Modifier
|
||||
|
||||
@Stable
|
||||
public fun Modifier.padding(
|
||||
left: Int = 0,
|
||||
top: Int = 0,
|
||||
right: Int = 0,
|
||||
bottom: Int = 0,
|
||||
): Modifier = this.then(
|
||||
PaddingModifier(
|
||||
left = left,
|
||||
top = top,
|
||||
right = right,
|
||||
bottom = bottom,
|
||||
)
|
||||
)
|
||||
|
||||
@Stable
|
||||
public fun Modifier.padding(
|
||||
horizontal: Int = 0,
|
||||
vertical: Int = 0,
|
||||
): Modifier = this.then(
|
||||
PaddingModifier(
|
||||
left = horizontal,
|
||||
top = vertical,
|
||||
right = horizontal,
|
||||
bottom = vertical,
|
||||
)
|
||||
)
|
||||
|
||||
@Stable
|
||||
public fun Modifier.padding(all: Int): Modifier =
|
||||
this.then(
|
||||
PaddingModifier(
|
||||
left = all,
|
||||
top = all,
|
||||
right = all,
|
||||
bottom = all,
|
||||
)
|
||||
)
|
||||
|
||||
private class PaddingModifier(
|
||||
val left: Int = 0,
|
||||
val top: Int = 0,
|
||||
val right: Int = 0,
|
||||
val bottom: Int = 0,
|
||||
) : LayoutModifier {
|
||||
init {
|
||||
require(left >= 0 && top >= 0f && right >= 0f && bottom >= 0f) {
|
||||
"Padding must be non-negative"
|
||||
}
|
||||
}
|
||||
|
||||
override fun MeasureScope.measure(measurable: Measurable): MeasureResult {
|
||||
val horizontal = left + right
|
||||
val vertical = top + bottom
|
||||
|
||||
val placeable = measurable.measure()
|
||||
|
||||
val width = placeable.width + horizontal
|
||||
val height = placeable.height + vertical
|
||||
return layout(width, height) {
|
||||
placeable.place(left, top)
|
||||
}
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = left
|
||||
result = 31 * result + top
|
||||
result = 31 * result + right
|
||||
result = 31 * result + bottom
|
||||
return result
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return other is PaddingModifier &&
|
||||
left == other.left &&
|
||||
top == other.top &&
|
||||
right == other.right &&
|
||||
bottom == other.bottom
|
||||
}
|
||||
|
||||
override fun toString() = when {
|
||||
left == right && left == top && left == bottom -> "Padding($left)"
|
||||
left == right && top == bottom -> "Padding(h=$left, v=$top)"
|
||||
else -> "Padding(l=$left, t=$top, r=$right, b=$bottom)"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,152 @@
|
||||
/*
|
||||
* 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 com.jakewharton.mosaic.modifier
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
|
||||
/**
|
||||
* An ordered, immutable collection of [modifier elements][Modifier.Element] that decorate or add
|
||||
* behavior to Mosaic elements. For example, backgrounds, padding, and click event listeners
|
||||
* decorate or add behavior to rows, text, or buttons.
|
||||
*
|
||||
* Modifier implementations should offer a fluent factory extension function on [Modifier] for
|
||||
* creating combined modifiers by starting from existing modifiers:
|
||||
*
|
||||
* Modifier elements may be combined using [then]. Order is significant; modifier elements that
|
||||
* appear first will be applied first.
|
||||
*
|
||||
* Composables that accept a [Modifier] as a parameter to be applied to the whole component
|
||||
* represented by the composable function should name the parameter `modifier` and
|
||||
* assign the parameter a default value of [Modifier]. It should appear as the first
|
||||
* optional parameter in the parameter list; after all required parameters (except for trailing
|
||||
* lambda parameters) but before any other parameters with default values. Any default modifiers
|
||||
* desired by a composable function should come after the `modifier` parameter's value in the
|
||||
* composable function's implementation, keeping [Modifier] as the default parameter value.
|
||||
*
|
||||
* The pattern above allows default modifiers to still be applied as part of the chain
|
||||
* if a caller also supplies unrelated modifiers.
|
||||
*
|
||||
* Composables that accept modifiers to be applied to a specific subcomponent `foo`
|
||||
* should name the parameter `fooModifier` and follow the same guidelines above for default values
|
||||
* and behavior. Subcomponent modifiers should be grouped together and follow the parent
|
||||
* composable's modifier.
|
||||
*/
|
||||
@Stable
|
||||
public interface Modifier {
|
||||
|
||||
/**
|
||||
* Accumulates a value starting with [initial] and applying [operation] to the current value
|
||||
* and each element from outside in.
|
||||
*
|
||||
* Elements wrap one another in a chain from left to right; an [Element] that appears to the
|
||||
* left of another in a `+` expression or in [operation]'s parameter order affects all
|
||||
* of the elements that appear after it. [foldIn] may be used to accumulate a value starting
|
||||
* from the parent or head of the modifier chain to the final wrapped child.
|
||||
*/
|
||||
public fun <R> foldIn(initial: R, operation: (R, Element) -> R): R
|
||||
|
||||
/**
|
||||
* Accumulates a value starting with [initial] and applying [operation] to the current value
|
||||
* and each element from inside out.
|
||||
*
|
||||
* Elements wrap one another in a chain from left to right; an [Element] that appears to the
|
||||
* left of another in a `+` expression or in [operation]'s parameter order affects all
|
||||
* of the elements that appear after it. [foldOut] may be used to accumulate a value starting
|
||||
* from the child or tail of the modifier chain up to the parent or head of the chain.
|
||||
*/
|
||||
public fun <R> foldOut(initial: R, operation: (Element, R) -> R): R
|
||||
|
||||
/**
|
||||
* Returns `true` if [predicate] returns true for any [Element] in this [Modifier].
|
||||
*/
|
||||
public fun any(predicate: (Element) -> Boolean): Boolean
|
||||
|
||||
/**
|
||||
* Returns `true` if [predicate] returns true for all [Element]s in this [Modifier] or if
|
||||
* this [Modifier] contains no [Element]s.
|
||||
*/
|
||||
public fun all(predicate: (Element) -> Boolean): Boolean
|
||||
|
||||
/**
|
||||
* Concatenates this modifier with another.
|
||||
*
|
||||
* Returns a [Modifier] representing this modifier followed by [other] in sequence.
|
||||
*/
|
||||
public infix fun then(other: Modifier): Modifier =
|
||||
if (other === Modifier) this else CombinedModifier(this, other)
|
||||
|
||||
/**
|
||||
* A single element contained within a [Modifier] chain.
|
||||
*/
|
||||
public interface Element : Modifier {
|
||||
override fun <R> foldIn(initial: R, operation: (R, Element) -> R): R =
|
||||
operation(initial, this)
|
||||
|
||||
override fun <R> foldOut(initial: R, operation: (Element, R) -> R): R =
|
||||
operation(this, initial)
|
||||
|
||||
override fun any(predicate: (Element) -> Boolean): Boolean = predicate(this)
|
||||
|
||||
override fun all(predicate: (Element) -> Boolean): Boolean = predicate(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* The companion object `Modifier` is the empty, default, or starter [Modifier]
|
||||
* that contains no [elements][Element]. Use it to create a new [Modifier] using
|
||||
* modifier extension factory functions or as the default value for [Modifier] parameters.
|
||||
*/
|
||||
// The companion object implements `Modifier` so that it may be used as the start of a
|
||||
// modifier extension factory expression.
|
||||
public companion object : Modifier {
|
||||
override fun <R> foldIn(initial: R, operation: (R, Element) -> R): R = initial
|
||||
override fun <R> foldOut(initial: R, operation: (Element, R) -> R): R = initial
|
||||
override fun any(predicate: (Element) -> Boolean): Boolean = false
|
||||
override fun all(predicate: (Element) -> Boolean): Boolean = true
|
||||
override infix fun then(other: Modifier): Modifier = other
|
||||
override fun toString(): String = "Modifier"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A node in a [Modifier] chain. A CombinedModifier always contains at least two elements;
|
||||
* a Modifier [outer] that wraps around the Modifier [inner].
|
||||
*/
|
||||
public class CombinedModifier(
|
||||
internal val outer: Modifier,
|
||||
internal val inner: Modifier
|
||||
) : Modifier {
|
||||
override fun <R> foldIn(initial: R, operation: (R, Modifier.Element) -> R): R =
|
||||
inner.foldIn(outer.foldIn(initial, operation), operation)
|
||||
|
||||
override fun <R> foldOut(initial: R, operation: (Modifier.Element, R) -> R): R =
|
||||
outer.foldOut(inner.foldOut(initial, operation), operation)
|
||||
|
||||
override fun any(predicate: (Modifier.Element) -> Boolean): Boolean =
|
||||
outer.any(predicate) || inner.any(predicate)
|
||||
|
||||
override fun all(predicate: (Modifier.Element) -> Boolean): Boolean =
|
||||
outer.all(predicate) && inner.all(predicate)
|
||||
|
||||
override fun equals(other: Any?): Boolean =
|
||||
other is CombinedModifier && outer == other.outer && inner == other.inner
|
||||
|
||||
override fun hashCode(): Int = outer.hashCode() + 31 * inner.hashCode()
|
||||
|
||||
override fun toString(): String = "[" + foldIn("") { acc, element ->
|
||||
if (acc.isEmpty()) element.toString() else "$acc, $element"
|
||||
} + "]"
|
||||
}
|
||||
@ -157,11 +157,11 @@ internal fun createRootNode(): MosaicNode {
|
||||
}
|
||||
}
|
||||
},
|
||||
drawPolicy = null,
|
||||
staticPaintPolicy = StaticPaintPolicy.Children,
|
||||
debugPolicy = {
|
||||
children.joinToString(separator = "\n")
|
||||
}
|
||||
},
|
||||
isStatic = false,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ import kotlin.jvm.JvmName
|
||||
|
||||
@Composable
|
||||
public fun Column(content: @Composable () -> Unit) {
|
||||
Layout(content, { "Column()" }) { measurables ->
|
||||
Layout(content, debugInfo = { "Column()" }) { measurables ->
|
||||
var width = 0
|
||||
var height = 0
|
||||
val placeables = measurables.map { measurable ->
|
||||
|
||||
@ -3,12 +3,12 @@
|
||||
package com.jakewharton.mosaic.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.jakewharton.mosaic.layout.DrawPolicy
|
||||
import com.jakewharton.mosaic.layout.Measurable
|
||||
import com.jakewharton.mosaic.layout.MeasurePolicy
|
||||
import com.jakewharton.mosaic.layout.MeasureResult
|
||||
import com.jakewharton.mosaic.layout.MeasureScope
|
||||
import com.jakewharton.mosaic.layout.StaticPaintPolicy
|
||||
import com.jakewharton.mosaic.modifier.Modifier
|
||||
import kotlin.jvm.JvmName
|
||||
|
||||
internal fun interface NoContentMeasurePolicy {
|
||||
@ -35,15 +35,16 @@ internal sealed class NoContentMeasureScope {
|
||||
|
||||
@Composable
|
||||
internal fun Layout(
|
||||
modifiers: Modifier = Modifier,
|
||||
debugInfo: () -> String = { "Layout()" },
|
||||
drawPolicy: DrawPolicy,
|
||||
measurePolicy: NoContentMeasurePolicy,
|
||||
) {
|
||||
Node(
|
||||
measurePolicy = NoContentMeasurePolicyMeasurePolicy(measurePolicy),
|
||||
drawPolicy = drawPolicy,
|
||||
modifiers = modifiers,
|
||||
staticPaintPolicy = null,
|
||||
debugPolicy = { debugInfo() + " x=$x y=$y w=$width h=$height" },
|
||||
debugPolicy = { debugInfo() + " x=$x y=$y w=$width h=$height${modifiers.toDebugString()}" },
|
||||
factory = NodeFactory,
|
||||
)
|
||||
}
|
||||
|
||||
@ -59,22 +60,32 @@ private class NoContentMeasurePolicyMeasurePolicy(
|
||||
@Composable
|
||||
public fun Layout(
|
||||
content: @Composable () -> Unit,
|
||||
modifiers: Modifier = Modifier,
|
||||
debugInfo: () -> String = { "Layout()" },
|
||||
measurePolicy: MeasurePolicy,
|
||||
) {
|
||||
Node(
|
||||
content = content,
|
||||
measurePolicy = measurePolicy,
|
||||
drawPolicy = null,
|
||||
modifiers = modifiers,
|
||||
staticPaintPolicy = StaticPaintPolicy.Children,
|
||||
debugPolicy = {
|
||||
buildString {
|
||||
append(debugInfo())
|
||||
append(" x=$x y=$y w=$width h=$height")
|
||||
append(" x=$x y=$y w=$width h=$height${modifiers.toDebugString()}")
|
||||
children.joinTo(this, separator = "") {
|
||||
"\n" + it.toString().prependIndent(" ")
|
||||
}
|
||||
}
|
||||
},
|
||||
factory = NodeFactory,
|
||||
)
|
||||
}
|
||||
|
||||
private fun Modifier.toDebugString(): String {
|
||||
return if (this == Modifier) {
|
||||
""
|
||||
} else {
|
||||
" " + toString()
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,30 +2,30 @@ package com.jakewharton.mosaic.ui
|
||||
|
||||
import androidx.compose.runtime.Applier
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReusableComposeNode
|
||||
import com.jakewharton.mosaic.TextCanvas
|
||||
import androidx.compose.runtime.ComposeNode
|
||||
import com.jakewharton.mosaic.TextSurface
|
||||
import com.jakewharton.mosaic.layout.DebugPolicy
|
||||
import com.jakewharton.mosaic.layout.DrawPolicy
|
||||
import com.jakewharton.mosaic.layout.Measurable
|
||||
import com.jakewharton.mosaic.layout.MeasurePolicy
|
||||
import com.jakewharton.mosaic.layout.MeasureScope
|
||||
import com.jakewharton.mosaic.layout.MosaicNode
|
||||
import com.jakewharton.mosaic.layout.StaticPaintPolicy
|
||||
import com.jakewharton.mosaic.modifier.Modifier
|
||||
|
||||
@Composable
|
||||
internal inline fun Node(
|
||||
content: @Composable () -> Unit = {},
|
||||
modifiers: Modifier = Modifier,
|
||||
measurePolicy: MeasurePolicy,
|
||||
drawPolicy: DrawPolicy?,
|
||||
staticPaintPolicy: StaticPaintPolicy?,
|
||||
debugPolicy: DebugPolicy,
|
||||
noinline factory: () -> MosaicNode,
|
||||
) {
|
||||
ReusableComposeNode<MosaicNode, Applier<Any>>(
|
||||
factory = NodeFactory,
|
||||
ComposeNode<MosaicNode, Applier<Any>>(
|
||||
factory = factory,
|
||||
update = {
|
||||
set(measurePolicy) { this.measurePolicy = measurePolicy }
|
||||
set(drawPolicy) { this.drawPolicy = drawPolicy }
|
||||
set(modifiers) { this.modifiers = modifiers }
|
||||
set(staticPaintPolicy) { this.staticPaintPolicy = staticPaintPolicy }
|
||||
set(debugPolicy) { this.debugPolicy = debugPolicy }
|
||||
},
|
||||
@ -36,15 +36,23 @@ internal inline fun Node(
|
||||
internal val NodeFactory: () -> MosaicNode = {
|
||||
MosaicNode(
|
||||
measurePolicy = ThrowingPolicy,
|
||||
drawPolicy = ThrowingPolicy,
|
||||
staticPaintPolicy = ThrowingPolicy,
|
||||
debugPolicy = ThrowingPolicy,
|
||||
isStatic = false,
|
||||
)
|
||||
}
|
||||
|
||||
private val ThrowingPolicy = object : MeasurePolicy, DrawPolicy, StaticPaintPolicy, DebugPolicy {
|
||||
internal val StaticNodeFactory: () -> MosaicNode = {
|
||||
MosaicNode(
|
||||
measurePolicy = ThrowingPolicy,
|
||||
staticPaintPolicy = ThrowingPolicy,
|
||||
debugPolicy = ThrowingPolicy,
|
||||
isStatic = true,
|
||||
)
|
||||
}
|
||||
|
||||
private val ThrowingPolicy = object : MeasurePolicy, StaticPaintPolicy, DebugPolicy {
|
||||
override fun MeasureScope.measure(measurables: List<Measurable>) = throw AssertionError()
|
||||
override fun performDraw(canvas: TextCanvas) = throw AssertionError()
|
||||
override fun MosaicNode.performPaintStatics(statics: MutableList<TextSurface>) = throw AssertionError()
|
||||
override fun MosaicNode.renderDebug() = throw AssertionError()
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ import kotlin.jvm.JvmName
|
||||
|
||||
@Composable
|
||||
public fun Row(content: @Composable () -> Unit) {
|
||||
Layout(content, { "Row()" }) { measurables ->
|
||||
Layout(content, debugInfo = { "Row()" }) { measurables ->
|
||||
var width = 0
|
||||
var height = 0
|
||||
val placeables = measurables.map { measurable ->
|
||||
|
||||
@ -8,6 +8,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import com.jakewharton.mosaic.modifier.Modifier
|
||||
import kotlin.jvm.JvmName
|
||||
|
||||
/**
|
||||
@ -43,9 +44,7 @@ public fun <T> Static(
|
||||
}
|
||||
}
|
||||
},
|
||||
drawPolicy = {
|
||||
// Nothing to do. Children rendered separately.
|
||||
},
|
||||
modifiers = Modifier,
|
||||
staticPaintPolicy = { statics ->
|
||||
if (children.isNotEmpty()) {
|
||||
for (child in children) {
|
||||
@ -58,5 +57,6 @@ public fun <T> Static(
|
||||
debugPolicy = {
|
||||
children.joinToString(prefix = "Static()") { "\n" + it.toString().prependIndent(" ") }
|
||||
},
|
||||
factory = StaticNodeFactory,
|
||||
)
|
||||
}
|
||||
|
||||
@ -4,6 +4,8 @@ package com.jakewharton.mosaic.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.jakewharton.mosaic.layout.drawBehind
|
||||
import com.jakewharton.mosaic.modifier.Modifier
|
||||
import com.jakewharton.mosaic.text.TextLayout
|
||||
import kotlin.jvm.JvmName
|
||||
|
||||
@ -25,9 +27,9 @@ public fun Text(
|
||||
layout.measure()
|
||||
layout(layout.width, layout.height)
|
||||
},
|
||||
drawPolicy = { canvas ->
|
||||
modifiers = Modifier.drawBehind {
|
||||
layout.lines.forEachIndexed { row, line ->
|
||||
canvas.write(row, 0, line, color, background, style)
|
||||
write(row, 0, line, color, background, style)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
package com.jakewharton.mosaic
|
||||
|
||||
import com.jakewharton.mosaic.layout.drawBehind
|
||||
import com.jakewharton.mosaic.modifier.Modifier
|
||||
import com.jakewharton.mosaic.ui.Layout
|
||||
import com.jakewharton.mosaic.ui.Row
|
||||
import com.jakewharton.mosaic.ui.Static
|
||||
@ -21,7 +23,7 @@ class DebugRenderingTest {
|
||||
val nodes = mosaicNodes {
|
||||
Row {
|
||||
Text("Hello ")
|
||||
Layout(drawPolicy = { throw UnsupportedOperationException() }) {
|
||||
Layout(modifiers = Modifier.drawBehind { throw UnsupportedOperationException() }) {
|
||||
layout(5, 1)
|
||||
}
|
||||
}
|
||||
@ -37,8 +39,8 @@ class DebugRenderingTest {
|
||||
|
|
||||
|NODES:
|
||||
|Row\(\) x=0 y=0 w=11 h=1
|
||||
| Text\("Hello "\) x=0 y=0 w=6 h=1
|
||||
| Layout\(\) x=6 y=0 w=5 h=1
|
||||
| Text\("Hello "\) x=0 y=0 w=6 h=1 DrawBehind
|
||||
| Layout\(\) x=6 y=0 w=5 h=1 DrawBehind
|
||||
|
|
||||
|OUTPUT:
|
||||
|(kotlin\.|java\.lang\.)?UnsupportedOperationException:?
|
||||
@ -56,9 +58,9 @@ class DebugRenderingTest {
|
||||
assertEquals(
|
||||
"""
|
||||
|NODES:
|
||||
|Text("Hello") x=0 y=0 w=5 h=1
|
||||
|Text("Hello") x=0 y=0 w=5 h=1 DrawBehind
|
||||
|Static()
|
||||
| Text("Static") x=0 y=0 w=6 h=1
|
||||
| Text("Static") x=0 y=0 w=6 h=1 DrawBehind
|
||||
|
|
||||
|STATIC:
|
||||
|Static
|
||||
@ -78,7 +80,7 @@ class DebugRenderingTest {
|
||||
assertEquals(
|
||||
"""
|
||||
|NODES:
|
||||
|Text("Hello") x=0 y=0 w=5 h=1
|
||||
|Text("Hello") x=0 y=0 w=5 h=1 DrawBehind
|
||||
|
|
||||
|OUTPUT:
|
||||
|Hello
|
||||
@ -91,7 +93,7 @@ class DebugRenderingTest {
|
||||
"""
|
||||
|~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +100ms
|
||||
|NODES:
|
||||
|Text("Hello") x=0 y=0 w=5 h=1
|
||||
|Text("Hello") x=0 y=0 w=5 h=1 DrawBehind
|
||||
|
|
||||
|OUTPUT:
|
||||
|Hello
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package com.jakewharton.mosaic
|
||||
|
||||
import com.jakewharton.mosaic.layout.Measurable
|
||||
import com.jakewharton.mosaic.layout.drawBehind
|
||||
import com.jakewharton.mosaic.modifier.Modifier
|
||||
import com.jakewharton.mosaic.ui.Column
|
||||
import com.jakewharton.mosaic.ui.Layout
|
||||
import com.jakewharton.mosaic.ui.Row
|
||||
@ -24,8 +26,8 @@ class LayoutTest {
|
||||
}
|
||||
val expected = """
|
||||
|Custom() x=0 y=0 w=0 h=0
|
||||
| Text("Hi!") x=0 y=0 w=0 h=0
|
||||
| Text("Hey!") x=0 y=0 w=0 h=0
|
||||
| Text("Hi!") x=0 y=0 w=0 h=0 DrawBehind
|
||||
| Text("Hey!") x=0 y=0 w=0 h=0 DrawBehind
|
||||
""".trimMargin()
|
||||
assertEquals(expected, node.toString())
|
||||
}
|
||||
@ -99,9 +101,9 @@ class LayoutTest {
|
||||
}
|
||||
Row {
|
||||
Text("..")
|
||||
Layout(drawPolicy = {
|
||||
Layout(modifiers = Modifier.drawBehind {
|
||||
repeat(4) { row ->
|
||||
it.write(row, 0, "XXXX")
|
||||
write(row, 0, "XXXX")
|
||||
}
|
||||
}) {
|
||||
layout(2, 2)
|
||||
|
||||
Reference in New Issue
Block a user