Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add WindowInfo.containerSize #785

Merged
merged 8 commits into from
Sep 5, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright 2023 The Android Open Source Project
*
* 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 androidx.compose.ui.platform

import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.input.pointer.EmptyPointerKeyboardModifiers
import androidx.compose.ui.input.pointer.PointerKeyboardModifiers

/**
* Provides information about the Window that is hosting this compose hierarchy.
*/
@Stable
actual interface WindowInfo {
/**
* Indicates whether the window hosting this compose hierarchy is in focus.
*
* When there are multiple windows visible, either in a multi-window environment or if a
* popup or dialog is visible, this property can be used to determine if the current window
* is in focus.
*/
actual val isWindowFocused: Boolean

/**
* Indicates the state of keyboard modifiers (pressed or not).
*/
@ExperimentalComposeUiApi
actual val keyboardModifiers: PointerKeyboardModifiers
}

internal class WindowInfoImpl : WindowInfo {
override var isWindowFocused: Boolean by mutableStateOf(false)

@ExperimentalComposeUiApi
override var keyboardModifiers: PointerKeyboardModifiers by GlobalKeyboardModifiers

companion object {
// One instance across all windows makes sense, since the state of KeyboardModifiers is
// common for all windows.
internal val GlobalKeyboardModifiers = mutableStateOf(EmptyPointerKeyboardModifiers())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,16 @@ package androidx.compose.ui.platform
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.input.pointer.EmptyPointerKeyboardModifiers
import androidx.compose.ui.input.pointer.PointerKeyboardModifiers
import kotlinx.coroutines.flow.collect

/**
* Provides information about the Window that is hosting this compose hierarchy.
*/
@Stable
interface WindowInfo {
expect interface WindowInfo {
/**
* Indicates whether the window hosting this compose hierarchy is in focus.
*
Expand All @@ -44,10 +41,8 @@ interface WindowInfo {
/**
* Indicates the state of keyboard modifiers (pressed or not).
*/
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
@get:ExperimentalComposeUiApi
@ExperimentalComposeUiApi
val keyboardModifiers: PointerKeyboardModifiers
get() = WindowInfoImpl.GlobalKeyboardModifiers.value
}

@Composable
Expand All @@ -58,23 +53,3 @@ internal fun WindowFocusObserver(onWindowFocusChanged: (isWindowFocused: Boolean
snapshotFlow { windowInfo.isWindowFocused }.collect { callback.value(it) }
}
}

internal class WindowInfoImpl : WindowInfo {
private val _isWindowFocused = mutableStateOf(false)

override var isWindowFocused: Boolean
set(value) { _isWindowFocused.value = value }
get() = _isWindowFocused.value

@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
@get:ExperimentalComposeUiApi
override var keyboardModifiers: PointerKeyboardModifiers
get() = GlobalKeyboardModifiers.value
set(value) { GlobalKeyboardModifiers.value = value }

companion object {
// One instance across all windows makes sense, since the state of KeyboardModifiers is
// common for all windows.
internal val GlobalKeyboardModifiers = mutableStateOf(EmptyPointerKeyboardModifiers())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import androidx.compose.ui.semantics.SemanticsOwner
import androidx.compose.ui.toPointerKeyboardModifiers
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.WindowExceptionHandler
Expand Down Expand Up @@ -301,9 +302,15 @@ internal abstract class ComposeBridge {
}

protected fun updateSceneSize() {
val scale = scene.density.density
MatkovIvan marked this conversation as resolved.
Show resolved Hide resolved
val size = IntSize(
width = (component.width * scale).toInt(),
height = (component.height * scale).toInt()
)
platform.windowInfo.containerSize = size
scene.constraints = Constraints(
maxWidth = (component.width * scene.density.density).toInt(),
maxHeight = (component.height * scene.density.density).toInt()
maxWidth = size.width,
maxHeight = size.height
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import androidx.compose.ui.layout.layout
import androidx.compose.ui.sendMouseEvent
import androidx.compose.ui.window.WindowExceptionHandler
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.density
import androidx.compose.ui.window.runApplicationTest
Expand Down Expand Up @@ -151,6 +152,12 @@ class ComposeWindowTest {
assertThat(window.preferredSize).isEqualTo(Dimension(234, 345))
window.pack()
assertThat(window.size).isEqualTo(Dimension(234, 345))

assertThat(window.scene.platform.windowInfo.containerSize)
.isEqualTo(IntSize(
width = (234 * window.density.density).toInt(),
height = (345 * window.density.density).toInt(),
))
Comment on lines +155 to +160
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better to test WindowInfo separately from other things. A good test should validate one thing.

Also, don't you want to test it inside the composition (i.e. with LocalWindow.current.containerSize?

} finally {
window.dispose()
}
Expand All @@ -176,6 +183,12 @@ class ComposeWindowTest {
window.isVisible = true
assertThat(window.preferredSize).isEqualTo(Dimension(300, 400))
assertThat(window.size).isEqualTo(Dimension(300, 400))

assertThat(window.scene.platform.windowInfo.containerSize)
.isEqualTo(IntSize(
width = (300 * window.density.density).toInt(),
height = (400 * window.density.density).toInt(),
))
} finally {
window.dispose()
}
Expand Down Expand Up @@ -210,6 +223,12 @@ class ComposeWindowTest {
)
)
)

assertThat(window.scene.platform.windowInfo.containerSize)
.isEqualTo(IntSize(
width = (300 * window.density.density).toInt(),
height = (400 * window.density.density).toInt(),
))
} finally {
window.dispose()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import androidx.compose.ui.native.ComposeLayer
import androidx.compose.ui.platform.JSTextInputService
import androidx.compose.ui.platform.Platform
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.WindowInfoImpl
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
Expand All @@ -51,8 +52,12 @@ internal actual class ComposeWindow(val canvasId: String) {
fontScale = 1f
)

private val _windowInfo = WindowInfoImpl().apply {
isWindowFocused = true
}
private val jsTextInputService = JSTextInputService()
val platform = object : Platform by Platform.Empty {
override val windowInfo get() = _windowInfo
override val textInputService = jsTextInputService
override val viewConfiguration = object : ViewConfiguration {
override val longPressTimeoutMillis: Long = 500
Expand Down Expand Up @@ -82,6 +87,7 @@ internal actual class ComposeWindow(val canvasId: String) {
canvas.setAttribute("tabindex", "0")
layer.layer.needRedraw()

_windowInfo.containerSize = IntSize(canvas.width, canvas.height)
layer.setSize(canvas.width, canvas.height)
}

Expand All @@ -94,6 +100,7 @@ internal actual class ComposeWindow(val canvasId: String) {

canvas.width = newSize.width
canvas.height = newSize.height
_windowInfo.containerSize = IntSize(canvas.width, canvas.height)
layer.layer.attachTo(canvas)
layer.setSize(canvas.width, canvas.height)
layer.layer.needRedraw()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,23 @@ package androidx.compose.ui.window

import androidx.compose.runtime.Composable
import androidx.compose.ui.createSkiaLayer
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.native.ComposeLayer
import androidx.compose.ui.platform.MacosTextInputService
import androidx.compose.ui.platform.Platform
import androidx.compose.ui.platform.WindowInfoImpl
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import platform.AppKit.*
import platform.Foundation.*
import kotlinx.cinterop.*

internal actual class ComposeWindow actual constructor() {
private val macosTextInputService = MacosTextInputService()
private val _windowInfo = WindowInfoImpl().apply {
isWindowFocused = true
}
val platform: Platform = object : Platform by Platform.Empty {
override val windowInfo get() = _windowInfo
override val textInputService = macosTextInputService
}
val layer = ComposeLayer(
Expand All @@ -56,11 +61,16 @@ internal actual class ComposeWindow actual constructor() {
init {
layer.layer.attachTo(nsWindow)
nsWindow.orderFrontRegardless()
contentRect.useContents {
val scale = nsWindow.backingScaleFactor.toFloat()
layer.setDensity(Density(scale))
layer.setSize((size.width * scale).toInt(), (size.height * scale).toInt())
val scale = nsWindow.backingScaleFactor.toFloat()
val size = contentRect.useContents {
IntSize(
width = (size.width * scale).toInt(),
height = (size.height * scale).toInt()
)
}
_windowInfo.containerSize = size
layer.setDensity(Density(scale))
layer.setSize(size.width, size.height)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ internal interface Platform {
val textToolbar: TextToolbar

companion object {
@OptIn(ExperimentalComposeUiApi::class)
val Empty = object : Platform {
override val windowInfo = WindowInfoImpl().apply {
// true is a better default if platform doesn't provide WindowInfo.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright 2023 The Android Open Source Project
*
* 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 androidx.compose.ui.platform

import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.input.pointer.EmptyPointerKeyboardModifiers
import androidx.compose.ui.input.pointer.PointerKeyboardModifiers
import androidx.compose.ui.unit.IntSize

/**
* Provides information about the Window that is hosting this compose hierarchy.
*/
@Stable
actual interface WindowInfo {
/**
* Indicates whether the window hosting this compose hierarchy is in focus.
*
* When there are multiple windows visible, either in a multi-window environment or if a
* popup or dialog is visible, this property can be used to determine if the current window
* is in focus.
*/
actual val isWindowFocused: Boolean

/**
* Indicates the state of keyboard modifiers (pressed or not).
*/
@ExperimentalComposeUiApi
actual val keyboardModifiers: PointerKeyboardModifiers

/**
* Size of the window's content container in pixels.
*/
@ExperimentalComposeUiApi
MatkovIvan marked this conversation as resolved.
Show resolved Hide resolved
val containerSize: IntSize
}

internal class WindowInfoImpl : WindowInfo {
override var isWindowFocused: Boolean by mutableStateOf(false)

@ExperimentalComposeUiApi
override var keyboardModifiers: PointerKeyboardModifiers by GlobalKeyboardModifiers

@ExperimentalComposeUiApi
override var containerSize: IntSize by mutableStateOf(IntSize.Zero)

companion object {
// One instance across all windows makes sense, since the state of KeyboardModifiers is
// common for all windows.
internal val GlobalKeyboardModifiers = mutableStateOf(EmptyPointerKeyboardModifiers())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,10 @@ internal actual class ComposeWindow : UIViewController {
}
}

private val _windowInfo = WindowInfoImpl().apply {
isWindowFocused = true
}

@OverrideInit
actual constructor() : super(nibName = null, bundle = null)

Expand Down Expand Up @@ -341,15 +345,19 @@ internal actual class ComposeWindow : UIViewController {
}

private fun updateLayout(context: AttachedComposeContext) {
context.scene.density = density
context.scene.constraints = view.frame.useContents {
val scale = density.density

Constraints(
maxWidth = (size.width * scale).roundToInt(),
maxHeight = (size.height * scale).roundToInt()
val scale = density.density
val size = view.frame.useContents {
IntSize(
width = (size.width * scale).roundToInt(),
height = (size.height * scale).roundToInt()
)
}
_windowInfo.containerSize = size
context.scene.density = density
context.scene.constraints = Constraints(
maxWidth = size.width,
maxHeight = size.height
)

context.view.needRedraw()
}
Expand Down Expand Up @@ -523,6 +531,8 @@ internal actual class ComposeWindow : UIViewController {
val inputTraits = inputServices.skikoUITextInputTraits

val platform = object : Platform by Platform.Empty {
override val windowInfo: WindowInfo
get() = _windowInfo
override val textInputService: PlatformTextInputService = inputServices
override val viewConfiguration =
object : ViewConfiguration {
Expand Down