Rewrite node to use modifiers (#172)

This commit is contained in:
Jake Wharton
2023-05-15 22:49:51 -04:00
committed by GitHub
parent 454e171060
commit caeacc14a0
17 changed files with 576 additions and 99 deletions

View File

@ -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(' ')

View File

@ -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()
}

View File

@ -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"
}

View File

@ -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
}
}
}
}

View File

@ -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)"
}
}

View File

@ -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

View File

@ -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)"
}
}

View File

@ -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"
} + "]"
}

View File

@ -157,11 +157,11 @@ internal fun createRootNode(): MosaicNode {
}
}
},
drawPolicy = null,
staticPaintPolicy = StaticPaintPolicy.Children,
debugPolicy = {
children.joinToString(separator = "\n")
}
},
isStatic = false,
)
}

View File

@ -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 ->

View File

@ -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()
}
}

View File

@ -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()
}

View File

@ -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 ->

View File

@ -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,
)
}

View File

@ -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)
}
},
)

View File

@ -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

View File

@ -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)