diff --git a/gradle.properties b/gradle.properties index 0ae109cf..fc34dd8d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,9 +16,10 @@ android.useAndroidX=true android.enableJetifier=true org.gradle.jvmargs=-Xmx4g org.jetbrains.compose.experimental.jscanvas.enabled=true +org.jetbrains.compose.experimental.wasm.enabled=true org.jetbrains.compose.experimental.macos.enabled=true org.jetbrains.compose.experimental.uikit.enabled=true kotlin.mpp.androidSourceSetLayoutVersion=2 android.defaults.buildfeatures.buildconfig=true android.nonTransitiveRClass=false -android.nonFinalResIds=false \ No newline at end of file +android.nonFinalResIds=false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 601e7ab5..32c974b6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,13 +9,13 @@ junit = "4.13.2" junitJupiterEngine = "5.10.1" junitJupiterApi = "5.10.1" kotlin = "1.9.21" -kotlinxCoroutinesCore = "1.7.3" +kotlinxCoroutinesCore = "1.8.0" lifecycleRuntimeKtx = "2.6.2" material = "1.5.0" moleculeRuntime = "1.3.2" savedstateKtx = "1.2.1" spotless = "6.25.0" -jetbrainsComposePlugin = "1.5.11" +jetbrainsComposePlugin = "1.6.0" skiko = "0.7.90" koin = "3.5.0" koin-compose = "1.1.2" diff --git a/precompose/build.gradle.kts b/precompose/build.gradle.kts index e5f706db..0bbbc9fa 100644 --- a/precompose/build.gradle.kts +++ b/precompose/build.gradle.kts @@ -1,3 +1,4 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl import java.util.Properties plugins { @@ -11,6 +12,7 @@ plugins { group = "moe.tlaster" version = rootProject.extra.get("precomposeVersion") as String +@OptIn(ExperimentalWasmDsl::class) kotlin { applyDefaultHierarchyTemplate() macosArm64() @@ -32,6 +34,9 @@ kotlin { js(IR) { browser() } + wasmJs { + browser() + } sourceSets { val commonMain by getting { dependencies { diff --git a/precompose/src/commonMain/kotlin/moe/tlaster/precompose/navigation/NavHost.kt b/precompose/src/commonMain/kotlin/moe/tlaster/precompose/navigation/NavHost.kt index cd874359..a83413dc 100644 --- a/precompose/src/commonMain/kotlin/moe/tlaster/precompose/navigation/NavHost.kt +++ b/precompose/src/commonMain/kotlin/moe/tlaster/precompose/navigation/NavHost.kt @@ -1,3 +1,6 @@ +// TODO Migrate Material's Swipeable to Foundation's AnchoredDraggable APIs. +@file:Suppress("DEPRECATION") + package moe.tlaster.precompose.navigation import androidx.compose.animation.AnimatedContent diff --git a/precompose/src/commonMain/kotlin/moe/tlaster/precompose/navigation/SwipeProperties.kt b/precompose/src/commonMain/kotlin/moe/tlaster/precompose/navigation/SwipeProperties.kt index ed62499b..93923e35 100644 --- a/precompose/src/commonMain/kotlin/moe/tlaster/precompose/navigation/SwipeProperties.kt +++ b/precompose/src/commonMain/kotlin/moe/tlaster/precompose/navigation/SwipeProperties.kt @@ -1,4 +1,6 @@ @file:OptIn(ExperimentalMaterialApi::class) +// TODO Migrate Material's Swipeable to Foundation's AnchoredDraggable APIs. +@file:Suppress("DEPRECATION") package moe.tlaster.precompose.navigation diff --git a/precompose/src/macosMain/kotlin/moe/tlaster/precompose/ComposeWindow.kt b/precompose/src/macosMain/kotlin/moe/tlaster/precompose/ComposeWindow.kt index 23458bd4..c75c8f96 100644 --- a/precompose/src/macosMain/kotlin/moe/tlaster/precompose/ComposeWindow.kt +++ b/precompose/src/macosMain/kotlin/moe/tlaster/precompose/ComposeWindow.kt @@ -3,31 +3,18 @@ package moe.tlaster.precompose import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.compose.ui.createSkiaLayer -import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.graphics.BlendMode -import androidx.compose.ui.input.pointer.PointerIcon +import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.native.ComposeLayer -import androidx.compose.ui.node.LayoutNode -import androidx.compose.ui.platform.AccessibilityController -import androidx.compose.ui.platform.DefaultInputModeManager -import androidx.compose.ui.platform.EmptyFocusManager import androidx.compose.ui.platform.MacosTextInputService -import androidx.compose.ui.platform.Platform -import androidx.compose.ui.platform.TextToolbar -import androidx.compose.ui.platform.TextToolbarStatus -import androidx.compose.ui.platform.ViewConfiguration +import androidx.compose.ui.platform.PlatformContext import androidx.compose.ui.platform.WindowInfoImpl -import androidx.compose.ui.semantics.SemanticsOwner import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.dp import kotlinx.cinterop.BetaInteropApi import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.ObjCAction import kotlinx.cinterop.useContents +import org.jetbrains.skiko.SkiaLayer +import org.jetbrains.skiko.SkikoInput import platform.AppKit.NSBackingStoreBuffered import platform.AppKit.NSWindow import platform.AppKit.NSWindowDelegateProtocol @@ -62,53 +49,22 @@ internal class ComposeWindow( ) } private val macosTextInputService = MacosTextInputService() - private val platform: Platform = object : Platform { - override val windowInfo = WindowInfoImpl().apply { - // true is a better default if platform doesn't provide WindowInfo. - // otherwise UI will be rendered always in unfocused mode - // (hidden textfield cursor, gray titlebar, etc) - isWindowFocused = true - } - - override var dialogScrimBlendMode by mutableStateOf(BlendMode.SrcOver) - - override val inputModeManager = DefaultInputModeManager() - override val focusManager = EmptyFocusManager - - override fun requestFocusForOwner() = false - - override fun accessibilityController(owner: SemanticsOwner) = object : AccessibilityController { - override fun onSemanticsChange() = Unit - override fun onLayoutChange(layoutNode: LayoutNode) = Unit - override suspend fun syncLoop() = Unit - } + private val _windowInfo = WindowInfoImpl().apply { + isWindowFocused = true + } - override fun setPointerIcon(pointerIcon: PointerIcon) = Unit - override val viewConfiguration = object : ViewConfiguration { - override val longPressTimeoutMillis: Long = 500 - override val doubleTapTimeoutMillis: Long = 300 - override val doubleTapMinTimeMillis: Long = 40 - override val touchSlop: Float get() = with(density) { 18.dp.toPx() } - } - override val textToolbar: TextToolbar = object : TextToolbar { - override fun hide() = Unit - override val status: TextToolbarStatus = TextToolbarStatus.Hidden - override fun showMenu( - rect: Rect, - onCopyRequested: (() -> Unit)?, - onPasteRequested: (() -> Unit)?, - onCutRequested: (() -> Unit)?, - onSelectAllRequested: (() -> Unit)?, - ) = Unit + @OptIn(InternalComposeUiApi::class) + private val platformContext: PlatformContext = + object : PlatformContext by PlatformContext.Empty { + override val windowInfo get() = _windowInfo + override val textInputService get() = macosTextInputService } - override val textInputService = macosTextInputService - } - - val layer = ComposeLayer( - layer = createSkiaLayer(), - platform = platform, - input = macosTextInputService.input, + @OptIn(InternalComposeUiApi::class) + private val layer = ComposeLayer( + layer = SkiaLayer(), + platformContext = platformContext, + input = SkikoInput.Empty, ) val title: String get() = nsWindow.title() diff --git a/precompose/src/wasmJsMain/kotlin/moe/tlaster/precompose/PreComposeWindow.kt b/precompose/src/wasmJsMain/kotlin/moe/tlaster/precompose/PreComposeWindow.kt new file mode 100644 index 00000000..48c65170 --- /dev/null +++ b/precompose/src/wasmJsMain/kotlin/moe/tlaster/precompose/PreComposeWindow.kt @@ -0,0 +1,77 @@ +package moe.tlaster.precompose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.window.CanvasBasedWindow +import moe.tlaster.precompose.lifecycle.LifecycleOwner +import moe.tlaster.precompose.lifecycle.LifecycleRegistry +import moe.tlaster.precompose.lifecycle.LocalLifecycleOwner +import moe.tlaster.precompose.stateholder.LocalStateHolder +import moe.tlaster.precompose.stateholder.StateHolder +import moe.tlaster.precompose.ui.BackDispatcher +import moe.tlaster.precompose.ui.BackDispatcherOwner +import moe.tlaster.precompose.ui.LocalBackDispatcherOwner + +/** + * Creates a new [CanvasBasedWindow] with the given [title] and [content]. + */ +@OptIn(ExperimentalComposeUiApi::class) +fun preComposeWindow( + title: String = "Untitled", + canvasElementId: String = "ComposeTarget", + requestResize: (suspend () -> IntSize)? = null, + applyDefaultStyles: Boolean = true, + content: @Composable () -> Unit, +) { + CanvasBasedWindow( + title = title, + canvasElementId = canvasElementId, + requestResize = requestResize, + applyDefaultStyles = applyDefaultStyles, + content = { + PreComposeApp { + content.invoke() + } + }, + ) +} + +@Composable +actual fun PreComposeApp( + content: @Composable () -> Unit, +) { + ProvidePreComposeCompositionLocals { + content.invoke() + } +} + +@Composable +fun ProvidePreComposeCompositionLocals( + holder: PreComposeWindowHolder = remember { + PreComposeWindowHolder() + }, + content: @Composable () -> Unit, +) { + CompositionLocalProvider( + LocalLifecycleOwner provides holder, + LocalStateHolder provides holder.stateHolder, + LocalBackDispatcherOwner provides holder, + ) { + content.invoke() + } +} + +class PreComposeWindowHolder : LifecycleOwner, BackDispatcherOwner { + override val lifecycle by lazy { + LifecycleRegistry() + } + val stateHolder by lazy { + StateHolder() + } + override val backDispatcher by lazy { + BackDispatcher() + } +} diff --git a/precompose/src/wasmJsMain/kotlin/moe/tlaster/precompose/reflect/KClass.js.kt b/precompose/src/wasmJsMain/kotlin/moe/tlaster/precompose/reflect/KClass.js.kt new file mode 100644 index 00000000..14a065d1 --- /dev/null +++ b/precompose/src/wasmJsMain/kotlin/moe/tlaster/precompose/reflect/KClass.js.kt @@ -0,0 +1,7 @@ +package moe.tlaster.precompose.reflect + +import kotlin.reflect.KClass + +actual val KClass.canonicalName: String? + // qualifiedName is unsupported [This reflection API is not supported yet in JavaScript] + get() = this.simpleName