Skip to content

Commit

Permalink
Add support for getting the device's safe area insets.
Browse files Browse the repository at this point in the history
  • Loading branch information
colinrtwhite committed Apr 16, 2023
1 parent 7e441f4 commit c35c6b1
Show file tree
Hide file tree
Showing 22 changed files with 395 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,17 @@ public fun Margin(
public fun Margin(
all: Int = 0,
): Margin = Margin(all, all, all, all)

// Temporary constructor before migrating everything to doubles.
@Stable
public fun Margin(
left: Double = 0.0,
right: Double = 0.0,
top: Double = 0.0,
bottom: Double = 0.0,
): Margin = Margin(
left = left.toInt(),
right = right.toInt(),
top = top.toInt(),
bottom = bottom.toInt(),
)
35 changes: 35 additions & 0 deletions redwood-treehouse-composeui-insets/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import app.cash.redwood.buildsupport.KmpTargets

apply plugin: 'com.android.library'
apply plugin: 'org.jetbrains.kotlin.multiplatform'
apply plugin: 'app.cash.redwood.build.compose'
apply plugin: 'com.vanniktech.maven.publish'
apply plugin: 'org.jetbrains.dokka' // Must be applied here for publish plugin.

kotlin {
android {
publishLibraryVariants('release')
}

KmpTargets.addAllTargets(project, true /* skipJs */)

sourceSets {
commonMain {
dependencies {
api projects.redwoodTreehouse
api projects.redwoodTreehouseHost
implementation libs.jetbrains.compose.foundation
implementation projects.redwoodWidgetCompose
}
}
androidMain {
dependencies {
implementation libs.androidx.core
}
}
}
}

android {
namespace 'app.cash.treehouse.composeui'
}
2 changes: 2 additions & 0 deletions redwood-treehouse-composeui-insets/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
POM_ARTIFACT_ID=redwood-treehouse-composeui-insets
POM_NAME=Redwood Treehouse Compose UI Insets
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright (C) 2023 Square, Inc.
*
* 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.
*/
@file:JvmName("insetsAndroid")

package app.cash.redwood.treehouse.composeui

import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.systemBars
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection
import app.cash.redwood.layout.api.Margin

@Composable
public actual fun safeAreaInsets(): Margin {
val layoutDirection = LocalLayoutDirection.current
return WindowInsets.systemBars.asPaddingValues().toMargin(layoutDirection)
}

@Composable
private fun PaddingValues.toMargin(layoutDirection: LayoutDirection) = Margin(
left = calculateLeftPadding(layoutDirection).value.toDouble(),
right = calculateRightPadding(layoutDirection).value.toDouble(),
top = calculateTopPadding().value.toDouble(),
bottom = calculateBottomPadding().value.toDouble(),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright (C) 2023 Square, Inc.
*
* 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 app.cash.redwood.treehouse.composeui

import androidx.compose.runtime.Composable
import app.cash.redwood.layout.api.Margin

/** Return the device's insets in screen density-independent pixels. */
@Composable
public expect fun safeAreaInsets(): Margin

internal operator fun Margin.div(density: Double) = Margin(
left = left / density,
right = right / density,
top = top / density,
bottom = bottom / density,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright (C) 2023 Square, Inc.
*
* 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 app.cash.redwood.treehouse.composeui

import app.cash.redwood.layout.api.Margin
import kotlinx.cinterop.useContents
import platform.UIKit.UIApplication
import platform.UIKit.safeAreaInsets

public actual fun safeAreaInsets(): Margin {
val keyWindow = UIApplication.sharedApplication.keyWindow ?: return Margin.Zero
return keyWindow.safeAreaInsets.useContents { Margin(left, right, top, bottom) }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright (C) 2023 Square, Inc.
*
* 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.
*/
@file:JvmName("insetsJvm")

package app.cash.redwood.treehouse.composeui

import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalDensity
import app.cash.redwood.layout.api.Margin
import java.awt.GraphicsEnvironment
import java.awt.Insets
import java.awt.Toolkit

@Composable
public actual fun safeAreaInsets(): Margin {
val configuration = GraphicsEnvironment.getLocalGraphicsEnvironment()
.defaultScreenDevice.defaultConfiguration
val rawSpacing = Toolkit.getDefaultToolkit().getScreenInsets(configuration).toMargin()
return rawSpacing / LocalDensity.current.density.toDouble()
}

private fun Insets.toMargin() = Margin(
left = left.toDouble(),
right = right.toDouble(),
top = top.toDouble(),
bottom = bottom.toDouble(),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright (C) 2023 Square, Inc.
*
* 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 app.cash.redwood.treehouse.composeui

import app.cash.redwood.layout.api.Margin
import kotlinx.cinterop.useContents
import platform.AppKit.NSScreen

public actual fun safeAreaInsets(): Margin {
val mainScreen = NSScreen.mainScreen ?: return Margin.Zero
return mainScreen.safeAreaInsets.useContents { Margin(left, right, top, bottom) }
}
12 changes: 4 additions & 8 deletions redwood-treehouse-composeui/build.gradle
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import app.cash.redwood.buildsupport.KmpTargets

apply plugin: 'com.android.library'
apply plugin: 'org.jetbrains.kotlin.multiplatform'
apply plugin: 'app.cash.redwood.build.compose'
Expand All @@ -9,14 +11,7 @@ kotlin {
publishLibraryVariants('release')
}

iosArm64()
iosX64()
iosSimulatorArm64()

jvm()

macosArm64()
macosX64()
KmpTargets.addAllTargets(project, true /* skipJs */)

sourceSets {
commonMain {
Expand All @@ -25,6 +20,7 @@ kotlin {
api projects.redwoodTreehouseHost
implementation libs.jetbrains.compose.foundation
implementation projects.redwoodWidgetCompose
implementation projects.redwoodTreehouseComposeuiInsets
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public fun <A : AppService> TreehouseContent(
) {
val hostConfiguration = HostConfiguration(
darkMode = isSystemInDarkTheme(),
safeAreaInsets = safeAreaInsets(),
)

val treehouseView = remember(widgetSystem) {
Expand Down
1 change: 1 addition & 0 deletions redwood-treehouse-configuration/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ kotlin {
sourceSets {
commonMain {
dependencies {
api projects.redwoodLayoutApi
api libs.kotlinx.serialization.core
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@
*/
package app.cash.redwood.treehouse

import app.cash.redwood.layout.api.Margin
import kotlinx.serialization.Serializable

@Serializable
public data class HostConfiguration(
val darkMode: Boolean = false,
val safeAreaInsets: Margin = Margin.Zero,
) {
public companion object
}
1 change: 1 addition & 0 deletions redwood-treehouse-host/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ kotlin {
androidMain {
dependencies {
api libs.okHttp
implementation libs.androidx.core
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ import android.content.res.Configuration.UI_MODE_NIGHT_MASK
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.view.View
import android.view.ViewGroup
import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import app.cash.redwood.LayoutModifier
import app.cash.redwood.layout.api.Margin
import app.cash.redwood.treehouse.TreehouseView.WidgetSystem
import app.cash.redwood.widget.ViewGroupChildren
import app.cash.redwood.widget.Widget
Expand Down Expand Up @@ -113,6 +117,26 @@ class TreehouseWidgetViewTest {
}
}

@Test fun hostConfigurationEmitsSafeAreaInsetsChanges() = runTest {
val layout = TreehouseWidgetView(context, throwingWidgetSystem)
layout.hostConfiguration.test {
assertEquals(HostConfiguration(safeAreaInsets = Margin.Zero), awaitItem())
val insets = Insets.of(10, 20, 30, 40)
val windowInsets = WindowInsetsCompat.Builder()
.setInsets(WindowInsetsCompat.Type.systemBars(), insets)
.build()
ViewCompat.dispatchApplyWindowInsets(layout, windowInsets)
val density = context.resources.displayMetrics.density.toDouble()
val expectedInsets = Margin(
left = density * insets.left,
right = density * insets.right,
top = density * insets.top,
bottom = density * insets.bottom,
)
assertEquals(HostConfiguration(safeAreaInsets = expectedInsets), awaitItem())
}
}

private fun viewWidget(view: View) = object : Widget<View> {
override val value: View get() = view
override var layoutModifiers: LayoutModifier = LayoutModifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.view.View
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.FrameLayout
import androidx.core.graphics.Insets
import app.cash.redwood.treehouse.TreehouseView.ReadyForContentChangeListener
import app.cash.redwood.treehouse.TreehouseView.WidgetSystem
import app.cash.redwood.widget.ViewGroupChildren
Expand Down Expand Up @@ -51,12 +52,17 @@ public class TreehouseWidgetView(
private val _children = ViewGroupChildren(this)
override val children: Widget.Children<View> get() = _children

private val mutableHostConfiguration =
MutableStateFlow(computeHostConfiguration(context.resources.configuration))
private val mutableHostConfiguration = MutableStateFlow(computeHostConfiguration())

override val hostConfiguration: StateFlow<HostConfiguration>
get() = mutableHostConfiguration

init {
setOnWindowInsetsChangeListener { insets ->
mutableHostConfiguration.value = computeHostConfiguration(insets = insets.systemBars)
}
}

override fun reset() {
_children.remove(0, _children.widgets.size)

Expand All @@ -78,17 +84,19 @@ public class TreehouseWidgetView(

override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
mutableHostConfiguration.value = computeHostConfiguration(newConfig)
mutableHostConfiguration.value = computeHostConfiguration(config = newConfig)
}

override fun generateDefaultLayoutParams(): LayoutParams =
LayoutParams(MATCH_PARENT, MATCH_PARENT)
}

private fun computeHostConfiguration(
config: Configuration,
): HostConfiguration {
return HostConfiguration(
darkMode = (config.uiMode and UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES,
)
private fun computeHostConfiguration(
config: Configuration = context.resources.configuration,
insets: Insets = rootWindowInsetsCompat.systemBars,
): HostConfiguration {
return HostConfiguration(
darkMode = (config.uiMode and UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES,
safeAreaInsets = insets.toMargin(resources.displayMetrics.density.toDouble()),
)
}
}
Loading

0 comments on commit c35c6b1

Please sign in to comment.