diff --git a/haze/api/api.txt b/haze/api/api.txt index ac339a7f..c7aa2aa1 100644 --- a/haze/api/api.txt +++ b/haze/api/api.txt @@ -35,9 +35,9 @@ package dev.chrisbanes.haze { @androidx.compose.runtime.Stable public final class HazeState { ctor public HazeState(); - method public dev.chrisbanes.haze.HazeArea getContent(); + method public dev.chrisbanes.haze.HazeArea getContentArea(); method public androidx.compose.ui.graphics.layer.GraphicsLayer? getContentLayer(); - property public final dev.chrisbanes.haze.HazeArea content; + property public final dev.chrisbanes.haze.HazeArea contentArea; property public final androidx.compose.ui.graphics.layer.GraphicsLayer? contentLayer; } diff --git a/haze/src/commonMain/kotlin/dev/chrisbanes/haze/Haze.kt b/haze/src/commonMain/kotlin/dev/chrisbanes/haze/Haze.kt index 4092982c..67b082e8 100644 --- a/haze/src/commonMain/kotlin/dev/chrisbanes/haze/Haze.kt +++ b/haze/src/commonMain/kotlin/dev/chrisbanes/haze/Haze.kt @@ -28,8 +28,14 @@ import dev.drewhamilton.poko.Poko @Stable class HazeState { - val content: HazeArea by lazy { HazeArea() } + val contentArea: HazeArea by lazy { HazeArea() } + /** + * The content [GraphicsLayer]. This is used by [hazeChild] draw nodes when drawing their + * blurred areas. + * + * This is explicitly NOT snapshot or state backed, as doing so would cause draw loops. + */ var contentLayer: GraphicsLayer? = null internal set } diff --git a/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeChild.kt b/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeChild.kt index a4eb9c20..e61dd29f 100644 --- a/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeChild.kt +++ b/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeChild.kt @@ -9,6 +9,7 @@ import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.drawscope.ContentDrawScope import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.node.invalidateDraw import androidx.compose.ui.platform.InspectorInfo import androidx.compose.ui.unit.toSize @@ -61,6 +62,8 @@ private class HazeChildNode( HazeArea(shape = shape, style = style) } + private var drawWithContentLayerCount = 0 + override fun update() { // Propagate any shape changes to the HazeArea area.shape = shape @@ -94,7 +97,18 @@ private class HazeChildNode( } if (USE_GRAPHICS_LAYERS) { - drawEffectsWithGraphicsLayer(requireNotNull(state.contentLayer)) + val contentLayer = state.contentLayer + if (contentLayer != null) { + drawWithContentLayerCount = 0 + drawEffectsWithGraphicsLayer(contentLayer) + } else { + // The content layer has not have been drawn yet (draw order matters here). If it hasn't + // there's not much we do other than invalidate and wait for the next frame. + // We only want to force a few frames, otherwise we're causing a draw loop. + if (++drawWithContentLayerCount <= 2) { + invalidateDraw() + } + } } else { drawEffectsWithScrim() } diff --git a/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeEffect.kt b/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeEffect.kt index 434147d9..c5501722 100644 --- a/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeEffect.kt +++ b/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeEffect.kt @@ -97,7 +97,7 @@ internal abstract class HazeEffectNode : currentEffects.remove(area) ?: HazeEffect(area = area) } .onEach { effect -> - val resolvedStyle = resolveStyle(state.content.style, effect.area.style) + val resolvedStyle = resolveStyle(state.contentArea.style, effect.area.style) effect.size = effect.area.size effect.positionOnScreen = effect.area.positionOnScreen @@ -127,7 +127,7 @@ internal abstract class HazeEffectNode : for (effect in effects) { // Now we need to draw `contentNode` into each of an 'effect' graphic layers. // The RenderEffect applied will provide the blurring effect. - val contentArea = state.content + val contentArea = state.contentArea val boundsInContent = effect.calculateBounds(-contentArea.positionOnScreen) graphicsContext.useGraphicsLayer { layer -> diff --git a/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeNode.kt b/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeNode.kt index a973e57d..3608c41f 100644 --- a/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeNode.kt +++ b/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeNode.kt @@ -3,6 +3,7 @@ package dev.chrisbanes.haze +import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.drawscope.ContentDrawScope import androidx.compose.ui.graphics.layer.drawLayer @@ -14,7 +15,6 @@ import androidx.compose.ui.node.GlobalPositionAwareModifierNode import androidx.compose.ui.node.LayoutAwareModifierNode import androidx.compose.ui.node.currentValueOf import androidx.compose.ui.platform.LocalGraphicsContext -import androidx.compose.ui.unit.roundToIntSize import androidx.compose.ui.unit.toSize internal class HazeNode( @@ -27,7 +27,7 @@ internal class HazeNode( DrawModifierNode { fun update() { - state.content.style = defaultStyle + state.contentArea.style = defaultStyle } override fun onAttach() { @@ -37,45 +37,42 @@ internal class HazeNode( override fun onGloballyPositioned(coordinates: LayoutCoordinates) = onPlaced(coordinates) override fun onPlaced(coordinates: LayoutCoordinates) { - state.content.apply { - size = coordinates.size.toSize() - positionOnScreen = coordinates.positionInWindow() + calculateWindowOffset() + Snapshot.withMutableSnapshot { + state.contentArea.apply { + size = coordinates.size.toSize() + positionOnScreen = coordinates.positionInWindow() + calculateWindowOffset() + } } } override fun ContentDrawScope.draw() { - val graphicsContext = currentValueOf(LocalGraphicsContext) - - state.contentLayer?.let { graphicsContext.releaseGraphicsLayer(it) } - state.contentLayer = null - if (!USE_GRAPHICS_LAYERS) { // If we're not using graphics layers, just call drawContent and return early drawContent() return } - val contentLayer = graphicsContext.createGraphicsLayer() + val graphicsContext = currentValueOf(LocalGraphicsContext) + + val contentLayer = state.contentLayer ?: graphicsContext.createGraphicsLayer() + state.contentLayer = contentLayer // First we draw the composable content into a graphics layer - contentLayer.record(size = size.roundToIntSize()) { + contentLayer.record { this@draw.drawContent() } // Now we draw `content` into the window canvas drawLayer(contentLayer) - - // Otherwise we need to stuff the content graphics layer into the HazeState - state.contentLayer = contentLayer } override fun onDetach() { super.onDetach() - state.contentLayer?.let { old -> - currentValueOf(LocalGraphicsContext).releaseGraphicsLayer(old) - state.contentLayer = null + state.contentLayer?.let { layer -> + currentValueOf(LocalGraphicsContext).releaseGraphicsLayer(layer) } + state.contentLayer = null } }