From bc0a8e8ce57f6cfbf6182435569ee59fa4b16ee1 Mon Sep 17 00:00:00 2001 From: Mohamed Ibrahim Date: Fri, 3 Jul 2026 17:00:41 +0100 Subject: [PATCH 1/3] fix(ios): strip phantom top inset when presented as a page sheet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A page sheet already sits below the status bar, but CMP 1.11 sources safeDrawing from the UIWindow, so the sheet paid the status-bar inset twice — a ~62pt dead band above the header (iPhone 16 Pro). presentSharingan() now presents an internal sheet variant that provides LocalStripTopInset, and the Scaffold drops the top side of safeDrawing when it is set. The public SharinganViewController() factory is unchanged, so full-screen/embedded hosts keep their top inset. No public API changes (apiCheck + checkApiParity green). Fixes #42 Co-Authored-By: Claude Fable 5 --- .../dev/sharingan/ui/SharinganScreen.kt | 14 ++++++++++- .../dev/sharingan/ui/ScreenInsetsTest.kt | 23 +++++++++++++++++++ .../dev/sharingan/SharinganViewController.kt | 22 +++++++++++++----- 3 files changed, 52 insertions(+), 7 deletions(-) create mode 100644 sharingan/src/commonTest/kotlin/dev/sharingan/ui/ScreenInsetsTest.kt diff --git a/sharingan/src/commonMain/kotlin/dev/sharingan/ui/SharinganScreen.kt b/sharingan/src/commonMain/kotlin/dev/sharingan/ui/SharinganScreen.kt index a581884..f615b43 100644 --- a/sharingan/src/commonMain/kotlin/dev/sharingan/ui/SharinganScreen.kt +++ b/sharingan/src/commonMain/kotlin/dev/sharingan/ui/SharinganScreen.kt @@ -3,8 +3,10 @@ package dev.sharingan.ui import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.material3.Scaffold @@ -17,6 +19,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLayoutDirection @@ -27,6 +30,15 @@ import dev.sharingan.SharinganEvent import dev.sharingan.SharinganStore import kotlinx.coroutines.delay +// Sheet hosts (iOS page sheet) already sit below the status bar, so the top +// safe-area inset would be paid twice (#42). Full-screen/embedded hosts still +// need it. internal -> not part of the checked public API surface. +internal val LocalStripTopInset = staticCompositionLocalOf { false } + +internal fun sharinganContentInsets(safeDrawing: WindowInsets, stripTop: Boolean): WindowInsets = + if (stripTop) safeDrawing.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom) + else safeDrawing + /** * The Sharingan log browser: home (three protocol tabs), event detail, and * the share sheet — the whole flow from the design, in one composable. @@ -147,7 +159,7 @@ internal fun SharinganScreenContent( Scaffold( modifier = modifier, containerColor = colors.bg, - contentWindowInsets = WindowInsets.safeDrawing, + contentWindowInsets = sharinganContentInsets(WindowInsets.safeDrawing, LocalStripTopInset.current), ) { innerPadding -> Box( Modifier diff --git a/sharingan/src/commonTest/kotlin/dev/sharingan/ui/ScreenInsetsTest.kt b/sharingan/src/commonTest/kotlin/dev/sharingan/ui/ScreenInsetsTest.kt new file mode 100644 index 0000000..e7275d1 --- /dev/null +++ b/sharingan/src/commonTest/kotlin/dev/sharingan/ui/ScreenInsetsTest.kt @@ -0,0 +1,23 @@ +package dev.sharingan.ui + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection +import kotlin.test.Test +import kotlin.test.assertEquals + +internal class ScreenInsetsTest { + + @Test + fun `Given strip=true, When content insets built, Then top side absent`() { + val density = Density(1f) + val source = WindowInsets(left = 7, top = 13, right = 17, bottom = 23) + + val stripped = sharinganContentInsets(source, stripTop = true) + + assertEquals(0, stripped.getTop(density)) + assertEquals(7, stripped.getLeft(density, LayoutDirection.Ltr)) + assertEquals(23, stripped.getBottom(density)) + assertEquals(13, sharinganContentInsets(source, stripTop = false).getTop(density)) + } +} diff --git a/sharingan/src/iosMain/kotlin/dev/sharingan/SharinganViewController.kt b/sharingan/src/iosMain/kotlin/dev/sharingan/SharinganViewController.kt index 99836ac..28516df 100644 --- a/sharingan/src/iosMain/kotlin/dev/sharingan/SharinganViewController.kt +++ b/sharingan/src/iosMain/kotlin/dev/sharingan/SharinganViewController.kt @@ -1,7 +1,9 @@ package dev.sharingan +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.window.ComposeUIViewController import dev.sharingan.internal.topmostViewController +import dev.sharingan.ui.LocalStripTopInset import dev.sharingan.ui.SharinganScreen import kotlin.experimental.ExperimentalObjCName import kotlin.native.ObjCName @@ -23,11 +25,19 @@ import platform.darwin.dispatch_get_main_queue */ @ObjCName("SharinganViewController", swiftName = "SharinganViewController") @OptIn(ExperimentalObjCName::class) -public fun SharinganViewController(): UIViewController = ComposeUIViewController(configure = { - enforceStrictPlistSanityCheck = false -}) { - SharinganScreen() -} +public fun SharinganViewController(): UIViewController = composeVc(stripTopInset = false) + +// internal — presentSharingan() uses this; strips the phantom top inset (#42): +// a page sheet already sits below the status bar, but CMP 1.11 reads insets +// from the UIWindow, so safeDrawing would pay the top inset twice. +internal fun sharinganSheetViewController(): UIViewController = composeVc(stripTopInset = true) + +private fun composeVc(stripTopInset: Boolean): UIViewController = + ComposeUIViewController(configure = { enforceStrictPlistSanityCheck = false }) { + CompositionLocalProvider(LocalStripTopInset provides stripTopInset) { + SharinganScreen() + } + } /** * Presents the log browser over the topmost view controller of the key @@ -58,6 +68,6 @@ public fun presentSharingan(animated: Boolean = true) { // Guard: UIKit silently swallows a present() while another // presentation/dismissal is already in flight; make the no-op explicit. if (top.isBeingDismissed() || top.isBeingPresented()) return@dispatch_async - top.presentViewController(SharinganViewController(), animated = animated, completion = null) + top.presentViewController(sharinganSheetViewController(), animated = animated, completion = null) } } From f8d0504c4b8da789cc056c5b981747aaf2c4a096 Mon Sep 17 00:00:00 2001 From: Mohamed Ibrahim Date: Fri, 3 Jul 2026 17:32:04 +0100 Subject: [PATCH 2/3] test: drop commas from backticked test name for Kotlin/Native Kotlin/Native forbids ',' in identifiers; commonTest also compiles for the iOS targets, so the #42 inset test broke compileTestKotlinIosArm64. Co-Authored-By: Claude Fable 5 --- .../src/commonTest/kotlin/dev/sharingan/ui/ScreenInsetsTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sharingan/src/commonTest/kotlin/dev/sharingan/ui/ScreenInsetsTest.kt b/sharingan/src/commonTest/kotlin/dev/sharingan/ui/ScreenInsetsTest.kt index e7275d1..75c573f 100644 --- a/sharingan/src/commonTest/kotlin/dev/sharingan/ui/ScreenInsetsTest.kt +++ b/sharingan/src/commonTest/kotlin/dev/sharingan/ui/ScreenInsetsTest.kt @@ -9,7 +9,7 @@ import kotlin.test.assertEquals internal class ScreenInsetsTest { @Test - fun `Given strip=true, When content insets built, Then top side absent`() { + fun `Given strip=true When content insets built Then top side absent`() { val density = Density(1f) val source = WindowInsets(left = 7, top = 13, right = 17, bottom = 23) From f22b08dbfceb75c75d7c288d20553c787b64e2dd Mon Sep 17 00:00:00 2001 From: Mohamed Ibrahim Date: Fri, 3 Jul 2026 18:13:41 +0100 Subject: [PATCH 3/3] docs+test: steer sheet hosts to presentSharingan(), assert right inset Review polish for #42: the raw-VC KDoc now warns that a self-presented page sheet shows the phantom top gap and points at presentSharingan(); the inset test also pins the right side surviving the strip. Co-Authored-By: Claude Fable 5 --- .../kotlin/dev/sharingan/ui/ScreenInsetsTest.kt | 1 + .../kotlin/dev/sharingan/SharinganViewController.kt | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/sharingan/src/commonTest/kotlin/dev/sharingan/ui/ScreenInsetsTest.kt b/sharingan/src/commonTest/kotlin/dev/sharingan/ui/ScreenInsetsTest.kt index 75c573f..aa6c272 100644 --- a/sharingan/src/commonTest/kotlin/dev/sharingan/ui/ScreenInsetsTest.kt +++ b/sharingan/src/commonTest/kotlin/dev/sharingan/ui/ScreenInsetsTest.kt @@ -17,6 +17,7 @@ internal class ScreenInsetsTest { assertEquals(0, stripped.getTop(density)) assertEquals(7, stripped.getLeft(density, LayoutDirection.Ltr)) + assertEquals(17, stripped.getRight(density, LayoutDirection.Ltr)) assertEquals(23, stripped.getBottom(density)) assertEquals(13, sharinganContentInsets(source, stripTop = false).getTop(density)) } diff --git a/sharingan/src/iosMain/kotlin/dev/sharingan/SharinganViewController.kt b/sharingan/src/iosMain/kotlin/dev/sharingan/SharinganViewController.kt index 28516df..ffa64da 100644 --- a/sharingan/src/iosMain/kotlin/dev/sharingan/SharinganViewController.kt +++ b/sharingan/src/iosMain/kotlin/dev/sharingan/SharinganViewController.kt @@ -15,9 +15,11 @@ import platform.darwin.dispatch_async import platform.darwin.dispatch_get_main_queue /** - * The Sharingan log browser as a `UIViewController` — embed or present it - * however your app likes (sheet, push, debug menu). For the common case, - * call [presentSharingan] instead. + * The Sharingan log browser as a `UIViewController` — embed or push it + * however your app likes (full-screen, navigation stack, debug menu). For + * sheet presentation use [presentSharingan], which compensates for a + * Compose Multiplatform inset quirk (#42); presenting this raw controller + * as your own page sheet shows a phantom top gap. * * A host app whose Info.plist lacks `CADisableMinimumFrameDurationOnPhone` * must never crash (Compose Multiplatform 1.11 aborts on a strict plist