commit f0632a1b7479e8cc243af0d96697c83658cce12c
parent e62626f7fbdc10490347e5365e04f733f039ea3d
Author: triesap <tyson@radroots.org>
Date: Fri, 14 Aug 2026 17:36:09 +0000
ui: protect lifecycle canvas window chrome
- Keep canvas backgrounds full-bleed beneath host-owned chrome
- Move header foreground outside the physical exclusion region
- Expand the top band when host chrome is taller than design metrics
- Verify content bounds and the complete shared desktop suite
Diffstat:
3 files changed, 58 insertions(+), 9 deletions(-)
diff --git a/app/design_system/src/commonMain/kotlin/org/harvestcircle/designsystem/layout/HarvestCircleCanvasFrame.kt b/app/design_system/src/commonMain/kotlin/org/harvestcircle/designsystem/layout/HarvestCircleCanvasFrame.kt
@@ -2,8 +2,10 @@ package org.harvestcircle.designsystem.layout
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.absolutePadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -14,6 +16,8 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.unit.dp
import org.harvestcircle.designsystem.shell.HarvestCircleShellMetrics
import org.harvestcircle.designsystem.shell.HarvestCircleShellPalette
@@ -30,15 +34,26 @@ public fun HarvestCircleCanvasFrame(
bodyScrollable: Boolean = false,
) {
val colors = HarvestCircleShellPalette
- Box(modifier.fillMaxSize().background(colors.viewportCanvas), contentAlignment = Alignment.Center) {
+ BoxWithConstraints(modifier.fillMaxSize().background(colors.viewportCanvas), contentAlignment = Alignment.Center) {
+ val chromeClearance =
+ resolveHarvestCircleWindowChromeClearance(
+ exclusion = HarvestCircleWindowChrome.exclusion,
+ windowWidth = maxWidth,
+ regionLeft = 0.dp,
+ regionWidth = maxWidth,
+ minimumTopBandHeight = HarvestCircleShellMetrics.topBarHeight,
+ )
Column(Modifier.fillMaxSize().background(colors.pane)) {
Row(
modifier =
Modifier
.fillMaxWidth()
- .height(HarvestCircleShellMetrics.topBarHeight)
+ .height(chromeClearance.topBandHeight)
.background(colors.applicationFrame)
- .padding(horizontal = HarvestCircleShellMetrics.contentPageHorizontalInset),
+ .absolutePadding(
+ left = maxOf(HarvestCircleShellMetrics.contentPageHorizontalInset, chromeClearance.left),
+ right = maxOf(HarvestCircleShellMetrics.contentPageHorizontalInset, chromeClearance.right),
+ ).testTag("harvestcircle-canvas-chrome-content"),
verticalAlignment = Alignment.CenterVertically,
) {
Box { navigation() }
diff --git a/app/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/CanvasScaffoldTest.kt b/app/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/CanvasScaffoldTest.kt
@@ -8,9 +8,13 @@ import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.v2.runComposeUiTest
+import androidx.compose.ui.unit.dp
import org.harvestcircle.appearance.AppearanceState
import org.harvestcircle.appearance.TextSizePreference
+import org.harvestcircle.designsystem.layout.HarvestCircleWindowChromeEdge
+import org.harvestcircle.designsystem.layout.HarvestCircleWindowChromeExclusion
import kotlin.test.Test
+import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
@@ -47,6 +51,29 @@ class CanvasScaffoldTest {
}
assertTrue(onNodeWithTag("canvas-body").fetchSemanticsNode().config.contains(SemanticsActions.ScrollBy))
}
+
+ @Test
+ fun macOsChromeExclusionProtectsHeaderForegroundAndMovesBodyBelowTallChrome() =
+ runComposeUiTest {
+ setHarvestCircleContent(
+ windowChromeExclusion =
+ HarvestCircleWindowChromeExclusion(
+ edge = HarvestCircleWindowChromeEdge.Left,
+ width = 112.dp,
+ height = 56.dp,
+ ),
+ ) {
+ canvas(TextSizePreference.Default)
+ }
+
+ val chromeContent = onNodeWithTag("harvestcircle-canvas-chrome-content").fetchSemanticsNode().boundsInRoot
+ val header = onNodeWithTag("canvas-header").fetchSemanticsNode().boundsInRoot
+ val body = onNodeWithTag("canvas-body").fetchSemanticsNode().boundsInRoot
+
+ assertEquals(112f, chromeContent.left)
+ assertTrue(header.left >= 112f)
+ assertTrue(body.top >= 56f)
+ }
}
@androidx.compose.runtime.Composable
diff --git a/app/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/HarvestCircleTestContent.kt b/app/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/HarvestCircleTestContent.kt
@@ -4,14 +4,21 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.test.ComposeUiTest
import androidx.compose.ui.test.ExperimentalTestApi
import org.harvestcircle.appearance.AppearanceState
+import org.harvestcircle.designsystem.layout.HarvestCircleWindowChromeEnvironment
+import org.harvestcircle.designsystem.layout.HarvestCircleWindowChromeExclusion
@OptIn(ExperimentalTestApi::class)
-internal fun ComposeUiTest.setHarvestCircleContent(content: @Composable () -> Unit) {
+internal fun ComposeUiTest.setHarvestCircleContent(
+ windowChromeExclusion: HarvestCircleWindowChromeExclusion = HarvestCircleWindowChromeExclusion.None,
+ content: @Composable () -> Unit,
+) {
setContent {
- HarvestCircleTheme(
- appearance = AppearanceState(),
- systemDark = false,
- content = content,
- )
+ HarvestCircleWindowChromeEnvironment(windowChromeExclusion) {
+ HarvestCircleTheme(
+ appearance = AppearanceState(),
+ systemDark = false,
+ content = content,
+ )
+ }
}
}