Skip to content
Binary file modified docs/screenshots/ios/ios-ble-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/ios/ios-detail-error.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/ios/ios-home-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/ios/ios-home-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/ios/ios-mqtt-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/ios/ios-share-sheet.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
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(17, stripped.getRight(density, LayoutDirection.Ltr))
assertEquals(23, stripped.getBottom(density))
assertEquals(13, sharinganContentInsets(source, stripTop = false).getTop(density))
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -13,21 +15,31 @@ 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
* check otherwise); the key remains a host-side opt-in for ProMotion/120Hz.
*/
@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
Expand Down Expand Up @@ -58,6 +70,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)
}
}
Binary file modified site/assets/screenshots/ios/ios-ble-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified site/assets/screenshots/ios/ios-detail-error.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified site/assets/screenshots/ios/ios-home-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified site/assets/screenshots/ios/ios-home-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified site/assets/screenshots/ios/ios-mqtt-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified site/assets/screenshots/ios/ios-share-sheet.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading