diff --git a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/canvas.kt b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/canvas.kt index 94ddcfba..d9a0fca1 100644 --- a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/canvas.kt +++ b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/canvas.kt @@ -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(' ') diff --git a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/ContentDrawScope.kt b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/ContentDrawScope.kt new file mode 100644 index 00000000..3aeb03ff --- /dev/null +++ b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/ContentDrawScope.kt @@ -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() +} diff --git a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/DrawModifier.kt b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/DrawModifier.kt new file mode 100644 index 00000000..e0924518 --- /dev/null +++ b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/DrawModifier.kt @@ -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" +} diff --git a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/DrawScope.kt b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/DrawScope.kt new file mode 100644 index 00000000..301ce2fa --- /dev/null +++ b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/DrawScope.kt @@ -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 + } + } + } +} diff --git a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/LayoutModifier.kt b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/LayoutModifier.kt new file mode 100644 index 00000000..84ddfead --- /dev/null +++ b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/LayoutModifier.kt @@ -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)" + } +} diff --git a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/Node.kt b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/Node.kt index 1d67298a..cd00e47a 100644 --- a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/Node.kt +++ b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/Node.kt @@ -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) @@ -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() - 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 diff --git a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/Padding.kt b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/Padding.kt new file mode 100644 index 00000000..8c35c722 --- /dev/null +++ b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/Padding.kt @@ -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)" + } +} diff --git a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/modifier/modifier.kt b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/modifier/modifier.kt new file mode 100644 index 00000000..89401df6 --- /dev/null +++ b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/modifier/modifier.kt @@ -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 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 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 foldIn(initial: R, operation: (R, Element) -> R): R = + operation(initial, this) + + override fun 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 foldIn(initial: R, operation: (R, Element) -> R): R = initial + override fun 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 foldIn(initial: R, operation: (R, Modifier.Element) -> R): R = + inner.foldIn(outer.foldIn(initial, operation), operation) + + override fun 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" + } + "]" +} diff --git a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/mosaic.kt b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/mosaic.kt index 00dea670..ac91ce8c 100644 --- a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/mosaic.kt +++ b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/mosaic.kt @@ -157,11 +157,11 @@ internal fun createRootNode(): MosaicNode { } } }, - drawPolicy = null, staticPaintPolicy = StaticPaintPolicy.Children, debugPolicy = { children.joinToString(separator = "\n") - } + }, + isStatic = false, ) } diff --git a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/Column.kt b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/Column.kt index dcacd65f..4b2a1302 100644 --- a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/Column.kt +++ b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/Column.kt @@ -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 -> diff --git a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/Layout.kt b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/Layout.kt index 55ca8a98..38f6f159 100644 --- a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/Layout.kt +++ b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/Layout.kt @@ -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() + } +} diff --git a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/Node.kt b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/Node.kt index 4d6ab9d1..8e882dc3 100644 --- a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/Node.kt +++ b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/Node.kt @@ -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>( - factory = NodeFactory, + ComposeNode>( + 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) = throw AssertionError() - override fun performDraw(canvas: TextCanvas) = throw AssertionError() override fun MosaicNode.performPaintStatics(statics: MutableList) = throw AssertionError() override fun MosaicNode.renderDebug() = throw AssertionError() } diff --git a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/Row.kt b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/Row.kt index 80c99577..2381fe94 100644 --- a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/Row.kt +++ b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/Row.kt @@ -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 -> diff --git a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/Static.kt b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/Static.kt index 133e2625..d76ba57e 100644 --- a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/Static.kt +++ b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/Static.kt @@ -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 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 Static( debugPolicy = { children.joinToString(prefix = "Static()") { "\n" + it.toString().prependIndent(" ") } }, + factory = StaticNodeFactory, ) } diff --git a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/Text.kt b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/Text.kt index d9b9a4c0..dfe58998 100644 --- a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/Text.kt +++ b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/Text.kt @@ -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) } }, ) diff --git a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/DebugRenderingTest.kt b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/DebugRenderingTest.kt index f7ce7858..ce41d1fc 100644 --- a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/DebugRenderingTest.kt +++ b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/DebugRenderingTest.kt @@ -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 diff --git a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/LayoutTest.kt b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/LayoutTest.kt index 99a768b2..76c47479 100644 --- a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/LayoutTest.kt +++ b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/LayoutTest.kt @@ -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)