diff --git a/CHANGELOG.md b/CHANGELOG.md index ed6783ded9..20d0d17b6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,55 @@ +# 1.0.0-alpha (Aug 2021) +## Common +- Desktop, Web, and Android artifacts publish at the same time with the same version + +## Desktop + +### Features +- [Context menu support in selectable text](https://android-review.googlesource.com/c/platform/frameworks/support/+/1742314) +- [Cursor change behavior in text and pointer icon API](https://android-review.googlesource.com/c/platform/frameworks/support/+/1736714/12/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.jvm.kt#357) +- [Mouse Clickable modifier](https://github.com/JetBrains/compose-jb/tree/master/tutorials/Mouse_Events#mouse-rightmiddle-clicks-and-keyboard-modifiers) +- Tab navigation between text fields by default +- Resource packing to native distribution +- Support @Preview annotation in desktopMain sourceSet's (when the Compose MPP plugin is installed in IDEA) +- [New features for Composable menu (icons, shortcuts, mnemonics, radiob buttons, checkboxes](https://github.com/JetBrains/compose-jb/tree/master/tutorials/Tray_Notifications_MenuBar_new#menubar) +- [Adaptive window size](https://github.com/JetBrains/compose-jb/blob/master/tutorials/Window_API_new/README.md#adaptive-window-size) +- Support Linux on ARM64 +- [Support hidpi on some Linux distros](https://github.com/JetBrains/compose-jb/issues/188#issuecomment-891614869) +- Support resizing of undecorated resizable windows (`Window(undecorated=true, resizable=true, ...)`) + +### API changes +- new Window API is no longer experimental +- old Window API is deprecated +- classes from `android.compose.desktop.*` moved to `androidx.compose.ui.awt.*` (ComposeWindow, ComposePanel, etc) +- `svgResource`/`vectorXmlResource`/`imageResource` replaced by painterResource + +### API breaking changes: +- Window level keyboard API for the old Window API removed +- Window(icon: BufferedImage) replaced by Window(icon: Painter) +- ContextMenu renamed to CursorDropdownMenu + +## Web + +### API changes +- [classes behave cumulatively](https://github.com/JetBrains/compose-jb/pull/690) +- [removed content builder for empty elements](https://github.com/JetBrains/compose-jb/issues/744) +- [Introduce CSS arithmetic operations](https://github.com/JetBrains/compose-jb/pull/761) +- [Improved the types of Inputs and input events](https://github.com/JetBrains/compose-jb/pull/799) +- [CSS Animations](https://github.com/JetBrains/compose-jb/pull/810) +- [All event types expose native properties](https://github.com/JetBrains/compose-jb/pull/887) +- [Added a complete list of HTML color aliases](https://github.com/JetBrains/compose-jb/issues/890) +- [Introduce support for CSS Grid API](https://github.com/JetBrains/compose-jb/issues/895) +- [Deprecate Color.RGB, Color.HSL etc. functions in favor of top-level rgb, hsl an so on](https://github.com/JetBrains/compose-jb/issues/902) +- [negate CSSNumeric value directly](https://github.com/JetBrains/compose-jb/issues/921) + +### API breaking changes +- [boolean like attributes don't have any parameters anymore](https://github.com/JetBrains/compose-jb/pull/780) +- [removed input type specific event listeners](https://github.com/JetBrains/compose-jb/pull/861) +- [replaced maxWidth/minWidth media queries with prefixed names](https://github.com/JetBrains/compose-jb/issues/886) +- [Remove CSSVariables context and introduce specialized methods for adding String- and Number-valued CSS variables](https://github.com/JetBrains/compose-jb/issues/894) +- [inline style builder was moved into AttributeBuilder scope](https://github.com/JetBrains/compose-jb/pull/699) + + # M4 (Jun 2021) * New experimental [Composable Window API](https://github.com/JetBrains/compose-jb/tree/master/tutorials/Window_API_new) * [Tooltips](https://github.com/JetBrains/compose-jb/tree/master/tutorials/Desktop_Components#tooltips) diff --git a/FEATURES.md b/FEATURES.md index 39ed320215..6f736b1c0a 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -1,25 +1,40 @@ ## Features -Features currently available in Compose for Desktop - * [Scrollbars support](tutorials/Scrollbars/README.md) +### Supported platforms + * macOS (x86-64, arm64) + * Windows (x86-64) + * Linux (x86-64, arm64) + * Web browsers + +### Features currently available in Compose for Desktop + * [Intro](tutorials/Getting_Started) + * [Desktop Components](tutorials/Desktop_Components/README.md) * [Image loading support](tutorials/Image_And_Icons_Manipulations/README.md) * [Keyboard handling](tutorials/Keyboard/README.md) * [Mouse clicks and move](tutorials/Mouse_Events/README.md) * [Packaging to native distributions](tutorials/Native_distributions_and_local_execution/README.md) - * [Tray, menu bar and notifications](tutorials/Tray_Notifications_MenuBar/README.md) - * [Window properties handling](tutorials/Window_API/README.md) + * [Signing and notarization](tutorials/Signing_and_notarization_on_macOS/README.md) + * [Swing interoperability](tutorials/Swing_Integration/README.md) + * [Keyboard navigation](tutorials/Tab_Navigation/README.md) + * [Tray, menu bar and notifications](tutorials/Tray_Notifications_MenuBar_new/README.md) + * [Window properties handling](tutorials/Window_API_new/README.md) + +### Features currently available in Compose for Web + * [Intro](tutorials/Web/Building_UI/README.md) + * [Event handling](tutorials/Web/Events_Handling/README.md) + * [CSS](tutorials/Web/Style_Dsl/README.md) + Follow individual tutorials to understand how to use particular feature. ### Limitations -Following limitations apply to Milestone 3 (M3) release. +Following limitations apply to Alpha release. - * Only 64-bit Windows is supported + * Only 64-bit x86 Windows is supported * Only JDK 11 or later is supported due to the memory management scheme used in Skia bindings - * Some Linux distributions require additional packages to be installed, see [this issue](https://github.com/JetBrains/compose-jb/issues/273) for more information + * Only JDK 15 or later is supported for packaging native distributions due to jpackage limitations - [comment]: <> (__SUPPORTED_GRADLE_VERSIONS__) ### Gradle plugin compatibility @@ -27,3 +42,4 @@ Following limitations apply to Milestone 3 (M3) release. * M1 works only with Gradle 6.4 and 6.5; * M2 works only with Gradle 6.4 or later (6.7 is the latest tested version). * M3 works only with Gradle 6.4 or later (6.8 is the latest tested version). +* Alpha works with Gralde 6.7 or later (7.1 is the latest tested version). diff --git a/README.md b/README.md index f37e30b1a1..419133fe58 100644 --- a/README.md +++ b/README.md @@ -2,50 +2,51 @@ [![Latest release](https://img.shields.io/github/v/release/JetBrains/compose-jb?color=brightgreen&label=latest%20release)](https://github.com/JetBrains/compose-jb/releases/latest) [![Latest build](https://img.shields.io/github/v/release/JetBrains/compose-jb?color=orange&include_prereleases&label=latest%20build)](https://github.com/JetBrains/compose-jb/releases) -# Compose for Desktop and Web, by JetBrains +# Compose Multiplatform, by JetBrains ![](artwork/readme/apps.png) -Compose Kotlin UI framework port for desktop platforms (macOS, Linux, Windows), components outside of the core Compose repository +Compose Kotlin UI framework port for desktop platforms (macOS, Linux, Windows) and Web, components outside of the core Compose repository at https://android.googlesource.com/platform/frameworks/support. +Preview functionality (check your application UI without building/running it) for desktop platforms is available via IDEA plugin (https://plugins.jetbrains.com/plugin/16541-compose-multiplatform-ide-support). + ## Repository organization ## * [artwork](artwork) - design artifacts * [benchmarks](benchmarks) - collection of benchmarks * [compose](compose) - composite build of [Compose-jb sources](https://github.com/JetBrains/androidx) * [ci](ci) - Continuous Integration helpers - * [cef](cef) - CEF integration in Jetpack Compose - * [examples](examples) - examples of multiplatform Compose applications for Desktop and Android + * [cef](cef) - CEF integration in Jetpack Compose (somewhat outdated) + * [examples](examples) - examples of multiplatform Compose applications for Desktop, Android and Web * [codeviewer](examples/codeviewer) - File Browser and Code Viewer application for Android and Desktop * [imageviewer](examples/imageviewer) - Image Viewer application for Android and Desktop * [issues](examples/issues) - GitHub issue tracker with an adaptive UI and ktor-client - * [game](examples/falling_balls) - Simple game - * [game](examples/falling_balls_with_web) - Simple game for web target + * [game](examples/falling-balls) - Simple game + * [game](examples/falling-balls-with-web) - Simple game for web target * [compose-bird](examples/web-compose-bird) - A flappy bird clone using Compose for Web * [notepad](examples/notepad) - Notepad, using the new experimental Composable Window API * [todoapp](examples/todoapp) - TODO items tracker with persistence and multiple screens - * [widgetsgallery](examples/widgetsgallery) - Gallery of standard widgets - * [IDEA plugin](examples/intelliJPlugin) - Plugin for IDEA using Compose for Desktop - * [gradle-plugins](gradle-plugins) - a plugin, simplifying usage of Compose with Gradle + * [widgets gallery](examples/widgetsgallery) - Gallery of standard widgets + * [IDEA plugin](examples/intellij-plugin) - Plugin for IDEA using Compose for Desktop + * [gradle-plugins](gradle-plugins) - a plugin, simplifying usage of Compose Multiplatform with Gradle * [templates](templates) - new application templates (see `desktop-template/build_and_run_from_cli_example.sh` for using without Gradle) - * [tutorials](tutorials) - tutorials on using Compose for Desktop + * [tutorials](tutorials) - tutorials on using Compose Multiplatform * [Getting started](tutorials/Getting_Started) * [Image and icon manipulations](tutorials/Image_And_Icons_Manipulations) * [Mouse events and hover](tutorials/Mouse_Events) * [Scrolling and scrollbars](tutorials/Desktop_Components) * [Tooltips](tutorials/Desktop_Components#tooltips) - * [Top level windows management](tutorials/Window_API) - * [Top level windows management (new Composable API, experimental)](tutorials/Window_API_new) - * [Menu, tray, notifications](tutorials/Tray_Notifications_MenuBar) - * [Menu, tray, notifications (new Composable API, experimental)](tutorials/Tray_Notifications_MenuBar_new) + * [Top level windows management](tutorials/Window_API_new) + * [Menu, tray, notifications](tutorials/Tray_Notifications_MenuBar_new) * [Keyboard support](tutorials/Keyboard) + * [Tab focus navigation](tutorials/Tab_Navigation) * [Building native distribution](tutorials/Native_distributions_and_local_execution) * [Signing and notarization](tutorials/Signing_and_notarization_on_macOS) * [Swing interoperability](tutorials/Swing_Integration) * [Navigation](tutorials/Navigation) - * [components](components) - custom components of Compose for Desktop + * [components](components) - custom components of Compose Multiplatform * [Video Player](components/VideoPlayer) * [Split Pane](components/SplitPane) -## Getting latest version of Multiplatform Compose ## +## Getting latest version of Compose Multiplatform ## See https://github.com/JetBrains/compose-jb/tags for the latest build number. diff --git a/tutorials/Image_And_Icons_Manipulations/compose-logo.xml b/artwork/compose-logo.xml similarity index 100% rename from tutorials/Image_And_Icons_Manipulations/compose-logo.xml rename to artwork/compose-logo.xml diff --git a/cef/build.gradle.kts b/cef/build.gradle.kts index 7631f80c50..38ceaf72f9 100644 --- a/cef/build.gradle.kts +++ b/cef/build.gradle.kts @@ -5,9 +5,9 @@ import kotlin.text.capitalize plugins { // __KOTLIN_COMPOSE_VERSION__ - kotlin("jvm") version "1.4.20" + kotlin("jvm") version "1.5.21" // __LATEST_COMPOSE_RELEASE_VERSION__ - id("org.jetbrains.compose") version "0.3.0-build133" + id("org.jetbrains.compose") version "1.0.0-alpha1" id("de.undercouch.download") version "4.1.1" application } diff --git a/cef/gradle/wrapper/gradle-wrapper.properties b/cef/gradle/wrapper/gradle-wrapper.properties index 622ab64a3c..05679dc3c1 100644 --- a/cef/gradle/wrapper/gradle-wrapper.properties +++ b/cef/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/ci/compose-uber-jar/build.gradle.kts b/ci/compose-uber-jar/build.gradle.kts index 8c5158334b..4694ba8830 100644 --- a/ci/compose-uber-jar/build.gradle.kts +++ b/ci/compose-uber-jar/build.gradle.kts @@ -11,6 +11,7 @@ val properties = ComposeUberJarProperties() repositories { mavenCentral() maven(properties.composeRepoUrl) + google() } val composeVersion: String by lazy { @@ -22,6 +23,7 @@ val composeVersion: String by lazy { } dependencies { + implementation("org.jetbrains.compose.desktop:desktop-jvm-macos-x64:$composeVersion") implementation("org.jetbrains.compose.desktop:desktop-jvm-linux-x64:$composeVersion") } @@ -81,4 +83,4 @@ class ComposeUberJarProperties { inline fun Project.typedProperty(name: String): T? = project.findProperty(name) as? T } -} \ No newline at end of file +} diff --git a/ci/compose-uber-jar/gradle.properties b/ci/compose-uber-jar/gradle.properties index f1668b0fe5..587b780117 100644 --- a/ci/compose-uber-jar/gradle.properties +++ b/ci/compose-uber-jar/gradle.properties @@ -1,3 +1,3 @@ # __LATEST_COMPOSE_RELEASE_VERSION__ -compose.version=0.4.0 +compose.version=1.0.0-alpha1 kotlin.code.style=official diff --git a/ci/compose-uber-jar/gradle/wrapper/gradle-wrapper.properties b/ci/compose-uber-jar/gradle/wrapper/gradle-wrapper.properties index 33682bbbf9..05679dc3c1 100644 --- a/ci/compose-uber-jar/gradle/wrapper/gradle-wrapper.properties +++ b/ci/compose-uber-jar/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/components/SplitPane/demo/src/jvmMain/kotlin/org/jetbrains/compose/splitpane/demo/Main.kt b/components/SplitPane/demo/src/jvmMain/kotlin/org/jetbrains/compose/splitpane/demo/Main.kt index 3bfbc5d128..4054da5b87 100644 --- a/components/SplitPane/demo/src/jvmMain/kotlin/org/jetbrains/compose/splitpane/demo/Main.kt +++ b/components/SplitPane/demo/src/jvmMain/kotlin/org/jetbrains/compose/splitpane/demo/Main.kt @@ -1,49 +1,33 @@ package org.jetbrains.compose.splitpane.demo import androidx.compose.desktop.DesktopTheme -import androidx.compose.desktop.LocalAppWindow -import androidx.compose.desktop.Window import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.width import androidx.compose.material.MaterialTheme -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.composed import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.input.pointer.pointerMoveFilter +import androidx.compose.ui.input.pointer.PointerIcon +import androidx.compose.ui.input.pointer.pointerIcon import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.singleWindowApplication import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi import org.jetbrains.compose.splitpane.HorizontalSplitPane import org.jetbrains.compose.splitpane.VerticalSplitPane import org.jetbrains.compose.splitpane.rememberSplitPaneState import java.awt.Cursor -private fun Modifier.cursorForHorizontalResize( -): Modifier = composed { - var isHover by remember { mutableStateOf(false) } - - if (isHover) { - LocalAppWindow.current.window.cursor = Cursor(Cursor.E_RESIZE_CURSOR) - } else { - LocalAppWindow.current.window.cursor = Cursor.getDefaultCursor() - } - - pointerMoveFilter( - onEnter = { isHover = true; true }, - onExit = { isHover = false; true } - ) -} +@OptIn(ExperimentalComposeUiApi::class) +private fun Modifier.cursorForHorizontalResize(): Modifier = + pointerIcon(PointerIcon(Cursor(Cursor.E_RESIZE_CURSOR))) @OptIn(ExperimentalSplitPaneApi::class) -fun main() = Window( - "SplitPane demo" +fun main() = singleWindowApplication( + title = "SplitPane demo" ) { MaterialTheme { DesktopTheme { diff --git a/components/SplitPane/library/src/commonMain/kotlin/org/jetbrains/compose/splitpane/SplitPaneState.kt b/components/SplitPane/library/src/commonMain/kotlin/org/jetbrains/compose/splitpane/SplitPaneState.kt index 53101cca3c..d0ffc8f887 100644 --- a/components/SplitPane/library/src/commonMain/kotlin/org/jetbrains/compose/splitpane/SplitPaneState.kt +++ b/components/SplitPane/library/src/commonMain/kotlin/org/jetbrains/compose/splitpane/SplitPaneState.kt @@ -1,9 +1,5 @@ package org.jetbrains.compose.splitpane -import androidx.compose.foundation.interaction.Interaction -import androidx.compose.foundation.interaction.InteractionSource -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.collectIsDraggedAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue diff --git a/components/SplitPane/library/src/desktopMain/kotlin/org/jetbrains/compose/splitpane/DesktopSplitter.kt b/components/SplitPane/library/src/desktopMain/kotlin/org/jetbrains/compose/splitpane/DesktopSplitter.kt index 8e7ce35a3f..ff4927c1a0 100644 --- a/components/SplitPane/library/src/desktopMain/kotlin/org/jetbrains/compose/splitpane/DesktopSplitter.kt +++ b/components/SplitPane/library/src/desktopMain/kotlin/org/jetbrains/compose/splitpane/DesktopSplitter.kt @@ -1,45 +1,23 @@ package org.jetbrains.compose.splitpane -import androidx.compose.desktop.LocalAppWindow import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectDragGestures -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.* import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.composed import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.PointerIcon import androidx.compose.ui.input.pointer.consumeAllChanges +import androidx.compose.ui.input.pointer.pointerIcon import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.input.pointer.pointerMoveFilter import androidx.compose.ui.unit.dp import java.awt.Cursor -private fun Modifier.cursorForHorizontalResize( - isHorizontal: Boolean -): Modifier = composed { - var isHover by remember { mutableStateOf(false) } - - if (isHover) { - LocalAppWindow.current.window.cursor = Cursor( - if (isHorizontal) Cursor.E_RESIZE_CURSOR else Cursor.S_RESIZE_CURSOR - ) - } else { - LocalAppWindow.current.window.cursor = Cursor.getDefaultCursor() - } - pointerMoveFilter( - onEnter = { isHover = true; true }, - onExit = { isHover = false; true } - ) -} +@OptIn(ExperimentalComposeUiApi::class) +private fun Modifier.cursorForHorizontalResize(isHorizontal: Boolean): Modifier = + pointerIcon(PointerIcon(Cursor(if (isHorizontal) Cursor.E_RESIZE_CURSOR else Cursor.S_RESIZE_CURSOR))) @Composable private fun DesktopSplitPaneSeparator( diff --git a/components/build.gradle.kts b/components/build.gradle.kts index e1ad7a7bfd..b24228ee1d 100644 --- a/components/build.gradle.kts +++ b/components/build.gradle.kts @@ -10,7 +10,7 @@ buildscript { dependencies { classpath("org.jetbrains.compose:compose-gradle-plugin:$composeVersion") // __KOTLIN_COMPOSE_VERSION__ - classpath(kotlin("gradle-plugin", version = "1.5.10")) + classpath(kotlin("gradle-plugin", version = "1.5.21")) } } diff --git a/components/gradle.properties b/components/gradle.properties index 3e1cd60f44..d61dcdb7a9 100644 --- a/components/gradle.properties +++ b/components/gradle.properties @@ -4,4 +4,4 @@ android.enableJetifier=true kotlin.code.style=official # __LATEST_COMPOSE_RELEASE_VERSION__ -compose.version=0.4.0 \ No newline at end of file +compose.version=1.0.0-alpha1 diff --git a/components/gradle/wrapper/gradle-wrapper.properties b/components/gradle/wrapper/gradle-wrapper.properties index bca17f3656..05679dc3c1 100644 --- a/components/gradle/wrapper/gradle-wrapper.properties +++ b/components/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/compose/README.md b/compose/README.md index 0fb4b4cacc..38c06bbc72 100644 --- a/compose/README.md +++ b/compose/README.md @@ -4,7 +4,7 @@ Composite build of [Compose-jb sources](https://github.com/JetBrains/androidx) ## Download submodules after downloading the main project: ``` -git submodule update --init +git submodule update --init --recursive ``` Set this property to always update submodules on git checkout/pull/reset: ``` @@ -20,7 +20,7 @@ git config --global submodule.recurse true - CMake 3.10.2.4988404 (in folder $androidSdk/cmake, not in $androidSdk/cmake/$version) ## Requirements to develop in IDE -- Android Studio Arctic Fox | 2020.3.1 Canary 15 +- Android Studio Arctic Fox - Custom Gradle 7.1 specified in `Settings -> Build, Execution, Deployment -> Build Tools -> Gradle` (because Android Studio will pick the wrong Gradle in the subproject instead of the Gradle in the root project) - Specified Gradle JDK 11 in `... -> Build Tools -> Gradle` - Environment variables: @@ -35,6 +35,7 @@ export COMPOSE_CUSTOM_GROUP=org.jetbrains.compose androidx.compose.multiplatformEnabled=true androidx.compose.jsCompilerTestsEnabled=true ``` +(note that https://android.googlesource.com/platform/frameworks/support build doesn't work with androidx.compose.jsCompilerTestsEnabled) ## Scripts Publish artifacts to the local directory `out/androidx/build/support_repo/org/jetbrains/compose`: @@ -58,4 +59,4 @@ Run tests for Desktop: Run tests for Web: ``` ./scripts/testWeb -``` \ No newline at end of file +``` diff --git a/compose/build.gradle.kts b/compose/build.gradle.kts index 77c34eac01..1d2de0a52b 100644 --- a/compose/build.gradle.kts +++ b/compose/build.gradle.kts @@ -37,6 +37,7 @@ tasks.register("publishComposeJb") { ":compose:ui:ui-test-junit4", ":compose:ui:ui-text", ":compose:ui:ui-tooling", + ":compose:ui:ui-tooling-preview", ":compose:ui:ui-unit", ":compose:ui:ui-util", ).forEach { @@ -70,6 +71,9 @@ tasks.register("testComposeJbDesktop") { dependsOnComposeTask(":compose:desktop:desktop:jvmTest") dependsOnComposeTask(":compose:animation:animation:desktopTest") dependsOnComposeTask(":compose:animation:animation-core:desktopTest") + dependsOnComposeTask(":compose:ui:ui:desktopTest") + dependsOnComposeTask(":compose:ui:ui-graphics:desktopTest") + dependsOnComposeTask(":compose:ui:ui-text:desktopTest") dependsOnComposeTask(":compose:foundation:foundation:desktopTest") dependsOnComposeTask(":compose:foundation:foundation-layout:desktopTest") dependsOnComposeTask(":compose:material:material:desktopTest") @@ -81,4 +85,12 @@ tasks.register("testComposeJbDesktop") { tasks.register("testComposeJbWeb") { dependsOnComposeTask(":compose:runtime:runtime:jsTest") dependsOnComposeTask(":compose:runtime:runtime:test") -} \ No newline at end of file +} + +tasks.register("buildNativeDemo") { + dependsOnComposeTask(":compose:native:demo:assemble") +} + +tasks.register("testRuntimeNative") { + dependsOnComposeTask(":compose:runtime:runtime:macosX64Test") +} diff --git a/compose/frameworks/support b/compose/frameworks/support index 94cefabe73..aadb6bb998 160000 --- a/compose/frameworks/support +++ b/compose/frameworks/support @@ -1 +1 @@ -Subproject commit 94cefabe7303d41aef797722ee3ab331a21689aa +Subproject commit aadb6bb9988bd5b232b2922fa5a248b823f0d5a5 diff --git a/compose/golden b/compose/golden index cd6860e336..1b20aa5514 160000 --- a/compose/golden +++ b/compose/golden @@ -1 +1 @@ -Subproject commit cd6860e33655776f6533790a27cd37eb04b40e40 +Subproject commit 1b20aa551446123340cb42b4eb21d2f2797e608a diff --git a/compose/prebuilts/androidx/internal b/compose/prebuilts/androidx/internal index f37dc6b42f..818a882ba7 160000 --- a/compose/prebuilts/androidx/internal +++ b/compose/prebuilts/androidx/internal @@ -1 +1 @@ -Subproject commit f37dc6b42fe7838e9e37fbe8a9eb063a1550acd8 +Subproject commit 818a882ba70e8603d6a22b17d421c9049926da4c diff --git a/compose/scripts/buildNativeDemo b/compose/scripts/buildNativeDemo new file mode 100755 index 0000000000..0dec37eab3 --- /dev/null +++ b/compose/scripts/buildNativeDemo @@ -0,0 +1,8 @@ +#!/bin/bash + +cd "$(dirname "$0")" +. ./prepare + +pushd .. +./gradlew buildNativeDemo $COMPOSE_DEFAULT_GRADLE_ARGS "$@" || exit 1 +popd diff --git a/compose/scripts/testRuntimeNative b/compose/scripts/testRuntimeNative new file mode 100755 index 0000000000..94d06d6b81 --- /dev/null +++ b/compose/scripts/testRuntimeNative @@ -0,0 +1,8 @@ +#!/bin/bash + +cd "$(dirname "$0")" +. ./prepare + +pushd .. +./gradlew testRuntimeNative $COMPOSE_DEFAULT_GRADLE_ARGS "$@" || exit 1 +popd diff --git a/examples/codeviewer/build.gradle.kts b/examples/codeviewer/build.gradle.kts index 9cd95bc589..9eb9ff58dd 100644 --- a/examples/codeviewer/build.gradle.kts +++ b/examples/codeviewer/build.gradle.kts @@ -8,10 +8,10 @@ buildscript { dependencies { // __LATEST_COMPOSE_RELEASE_VERSION__ - classpath("org.jetbrains.compose:compose-gradle-plugin:0.4.0") + classpath("org.jetbrains.compose:compose-gradle-plugin:1.0.0-alpha1") classpath("com.android.tools.build:gradle:4.0.1") // __KOTLIN_COMPOSE_VERSION__ - classpath(kotlin("gradle-plugin", version = "1.5.10")) + classpath(kotlin("gradle-plugin", version = "1.5.21")) } } diff --git a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/Editor.kt b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/Editor.kt index c02c21fab9..30676f1ed6 100644 --- a/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/Editor.kt +++ b/examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/Editor.kt @@ -45,10 +45,7 @@ fun Editor(file: File) = Editor( fun content(index: Int): Editor.Content { val text = textLines.get(index) - .trim('\n') // fix for native crash in Skia. - // Workaround for another Skia problem with empty line layout. - // TODO: maybe use another symbols, i.e. \u2800 or \u00a0. - val state = mutableStateOf(if (text.isEmpty()) " " else text) + val state = mutableStateOf(text) return Editor.Content(state, isCode) } diff --git a/examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Mouse.kt b/examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Mouse.kt index 32609e72fb..fbd688bcae 100644 --- a/examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Mouse.kt +++ b/examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Mouse.kt @@ -1,13 +1,15 @@ package org.jetbrains.codeviewer.platform -import androidx.compose.desktop.LocalAppWindow import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.PointerIcon +import androidx.compose.ui.input.pointer.pointerIcon import androidx.compose.ui.input.pointer.pointerMoveFilter import java.awt.Cursor @@ -17,17 +19,20 @@ actual fun Modifier.pointerMoveFilter( onMove: (Offset) -> Boolean ): Modifier = this.pointerMoveFilter(onEnter = onEnter, onExit = onExit, onMove = onMove) +@OptIn(ExperimentalComposeUiApi::class) actual fun Modifier.cursorForHorizontalResize(): Modifier = composed { var isHover by remember { mutableStateOf(false) } - if (isHover) { - LocalAppWindow.current.window.cursor = Cursor(Cursor.E_RESIZE_CURSOR) - } else { - LocalAppWindow.current.window.cursor = Cursor.getDefaultCursor() - } - pointerMoveFilter( onEnter = { isHover = true; true }, onExit = { isHover = false; true } + ).pointerIcon( + PointerIcon( + if (isHover) { + Cursor(Cursor.E_RESIZE_CURSOR) + } else { + Cursor.getDefaultCursor() + } + ) ) -} \ No newline at end of file +} diff --git a/examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Theme.kt b/examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Theme.kt index c3da0e1b4c..2fc250cd76 100644 --- a/examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Theme.kt +++ b/examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Theme.kt @@ -1,7 +1,7 @@ package org.jetbrains.codeviewer.platform -import androidx.compose.desktop.DesktopTheme +import androidx.compose.desktop.DesktopMaterialTheme import androidx.compose.runtime.Composable @Composable -actual fun PlatformTheme(content: @Composable () -> Unit) = DesktopTheme(content = content) \ No newline at end of file +actual fun PlatformTheme(content: @Composable () -> Unit) = DesktopMaterialTheme(content = content) \ No newline at end of file diff --git a/examples/codeviewer/desktop/src/jvmMain/kotlin/org/jetbrains/codeviewer/main.kt b/examples/codeviewer/desktop/src/jvmMain/kotlin/org/jetbrains/codeviewer/main.kt index f30818eb08..1c9c308567 100644 --- a/examples/codeviewer/desktop/src/jvmMain/kotlin/org/jetbrains/codeviewer/main.kt +++ b/examples/codeviewer/desktop/src/jvmMain/kotlin/org/jetbrains/codeviewer/main.kt @@ -1,23 +1,19 @@ package org.jetbrains.codeviewer -import androidx.compose.desktop.Window -import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.res.loadImageBitmap +import androidx.compose.ui.res.useResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.WindowState +import androidx.compose.ui.window.singleWindowApplication import org.jetbrains.codeviewer.ui.MainView -import java.awt.image.BufferedImage -import javax.imageio.ImageIO -fun main() = Window( +@OptIn(ExperimentalComposeUiApi::class) +fun main() = singleWindowApplication( title = "Code Viewer", - size = IntSize(1280, 768), - icon = loadImageResource("ic_launcher.png"), + state = WindowState(width = 1280.dp, height = 768.dp), + icon = BitmapPainter(useResource("ic_launcher.png", ::loadImageBitmap)), ) { MainView() -} - - -@Suppress("SameParameterValue") -private fun loadImageResource(path: String): BufferedImage { - val resource = Thread.currentThread().contextClassLoader.getResource(path) - requireNotNull(resource) { "Resource $path not found" } - return resource.openStream().use(ImageIO::read) -} +} \ No newline at end of file diff --git a/examples/falling_balls_with_web/build.gradle.kts b/examples/falling-balls-web/build.gradle.kts similarity index 94% rename from examples/falling_balls_with_web/build.gradle.kts rename to examples/falling-balls-web/build.gradle.kts index 881efcdea8..9f8930a071 100644 --- a/examples/falling_balls_with_web/build.gradle.kts +++ b/examples/falling-balls-web/build.gradle.kts @@ -2,8 +2,8 @@ import org.jetbrains.compose.compose import org.jetbrains.compose.desktop.application.dsl.TargetFormat plugins { - kotlin("multiplatform") version "1.5.10" - id("org.jetbrains.compose") version "0.5.0-build228" + kotlin("multiplatform") version "1.5.21" + id("org.jetbrains.compose") version "1.0.0-alpha1-rc5" } version = "1.0-SNAPSHOT" @@ -11,6 +11,7 @@ version = "1.0-SNAPSHOT" repositories { mavenCentral() maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + google() } kotlin { @@ -53,7 +54,7 @@ compose.desktop { targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) packageName = "ImageViewer" packageVersion = "1.0.0" - + modules("jdk.crypto.ec") val iconsRoot = project.file("../common/src/desktopMain/resources/images") diff --git a/examples/falling_balls/gradle/wrapper/gradle-wrapper.jar b/examples/falling-balls-web/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from examples/falling_balls/gradle/wrapper/gradle-wrapper.jar rename to examples/falling-balls-web/gradle/wrapper/gradle-wrapper.jar diff --git a/examples/intelliJPlugin/gradle/wrapper/gradle-wrapper.properties b/examples/falling-balls-web/gradle/wrapper/gradle-wrapper.properties similarity index 92% rename from examples/intelliJPlugin/gradle/wrapper/gradle-wrapper.properties rename to examples/falling-balls-web/gradle/wrapper/gradle-wrapper.properties index 7665b0fa93..05679dc3c1 100644 --- a/examples/intelliJPlugin/gradle/wrapper/gradle-wrapper.properties +++ b/examples/falling-balls-web/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/examples/falling_balls/gradlew b/examples/falling-balls-web/gradlew similarity index 100% rename from examples/falling_balls/gradlew rename to examples/falling-balls-web/gradlew diff --git a/examples/falling_balls_with_web/gradlew.bat b/examples/falling-balls-web/gradlew.bat similarity index 100% rename from examples/falling_balls_with_web/gradlew.bat rename to examples/falling-balls-web/gradlew.bat diff --git a/examples/falling_balls_with_web/settings.gradle.kts b/examples/falling-balls-web/settings.gradle.kts similarity index 93% rename from examples/falling_balls_with_web/settings.gradle.kts rename to examples/falling-balls-web/settings.gradle.kts index ad64f11370..401f6cb417 100644 --- a/examples/falling_balls_with_web/settings.gradle.kts +++ b/examples/falling-balls-web/settings.gradle.kts @@ -3,6 +3,7 @@ pluginManagement { gradlePluginPortal() mavenCentral() maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") } + google() } } diff --git a/examples/falling_balls_with_web/src/commonMain/kotlin/fallingBalls/Game.kt b/examples/falling-balls-web/src/commonMain/kotlin/fallingBalls/Game.kt similarity index 100% rename from examples/falling_balls_with_web/src/commonMain/kotlin/fallingBalls/Game.kt rename to examples/falling-balls-web/src/commonMain/kotlin/fallingBalls/Game.kt diff --git a/examples/falling_balls_with_web/src/commonMain/kotlin/fallingBalls/Piece.kt b/examples/falling-balls-web/src/commonMain/kotlin/fallingBalls/Piece.kt similarity index 100% rename from examples/falling_balls_with_web/src/commonMain/kotlin/fallingBalls/Piece.kt rename to examples/falling-balls-web/src/commonMain/kotlin/fallingBalls/Piece.kt diff --git a/examples/falling_balls_with_web/src/commonMain/kotlin/fallingBalls/PieceData.kt b/examples/falling-balls-web/src/commonMain/kotlin/fallingBalls/PieceData.kt similarity index 100% rename from examples/falling_balls_with_web/src/commonMain/kotlin/fallingBalls/PieceData.kt rename to examples/falling-balls-web/src/commonMain/kotlin/fallingBalls/PieceData.kt diff --git a/examples/falling_balls_with_web/src/commonMain/kotlin/fallingBalls/fallingBalls.kt b/examples/falling-balls-web/src/commonMain/kotlin/fallingBalls/fallingBalls.kt similarity index 100% rename from examples/falling_balls_with_web/src/commonMain/kotlin/fallingBalls/fallingBalls.kt rename to examples/falling-balls-web/src/commonMain/kotlin/fallingBalls/fallingBalls.kt diff --git a/examples/falling_balls_with_web/src/commonMain/kotlin/modifiers/position.kt b/examples/falling-balls-web/src/commonMain/kotlin/modifiers/position.kt similarity index 100% rename from examples/falling_balls_with_web/src/commonMain/kotlin/modifiers/position.kt rename to examples/falling-balls-web/src/commonMain/kotlin/modifiers/position.kt diff --git a/examples/falling_balls_with_web/src/jsMain/kotlin/androidx/compose/web/with-web/App.kt b/examples/falling-balls-web/src/jsMain/kotlin/androidx/compose/web/with-web/App.kt similarity index 100% rename from examples/falling_balls_with_web/src/jsMain/kotlin/androidx/compose/web/with-web/App.kt rename to examples/falling-balls-web/src/jsMain/kotlin/androidx/compose/web/with-web/App.kt diff --git a/examples/falling_balls_with_web/src/jsMain/kotlin/modifiers/position.kt b/examples/falling-balls-web/src/jsMain/kotlin/modifiers/position.kt similarity index 100% rename from examples/falling_balls_with_web/src/jsMain/kotlin/modifiers/position.kt rename to examples/falling-balls-web/src/jsMain/kotlin/modifiers/position.kt diff --git a/examples/falling_balls_with_web/src/jsMain/resources/index.html b/examples/falling-balls-web/src/jsMain/resources/index.html similarity index 100% rename from examples/falling_balls_with_web/src/jsMain/resources/index.html rename to examples/falling-balls-web/src/jsMain/resources/index.html diff --git a/examples/falling_balls_with_web/src/jsMain/resources/styles.css b/examples/falling-balls-web/src/jsMain/resources/styles.css similarity index 100% rename from examples/falling_balls_with_web/src/jsMain/resources/styles.css rename to examples/falling-balls-web/src/jsMain/resources/styles.css diff --git a/examples/falling_balls_with_web/src/jvmMain/kotlin/App.kt b/examples/falling-balls-web/src/jvmMain/kotlin/App.kt similarity index 100% rename from examples/falling_balls_with_web/src/jvmMain/kotlin/App.kt rename to examples/falling-balls-web/src/jvmMain/kotlin/App.kt diff --git a/examples/falling_balls_with_web/src/jvmMain/kotlin/modifiers/position.kt b/examples/falling-balls-web/src/jvmMain/kotlin/modifiers/position.kt similarity index 100% rename from examples/falling_balls_with_web/src/jvmMain/kotlin/modifiers/position.kt rename to examples/falling-balls-web/src/jvmMain/kotlin/modifiers/position.kt diff --git a/examples/falling_balls/.gitignore b/examples/falling-balls/.gitignore similarity index 100% rename from examples/falling_balls/.gitignore rename to examples/falling-balls/.gitignore diff --git a/examples/falling_balls/build.gradle.kts b/examples/falling-balls/build.gradle.kts similarity index 79% rename from examples/falling_balls/build.gradle.kts rename to examples/falling-balls/build.gradle.kts index e41dd7f372..d8ab110725 100644 --- a/examples/falling_balls/build.gradle.kts +++ b/examples/falling-balls/build.gradle.kts @@ -4,9 +4,9 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { // __KOTLIN_COMPOSE_VERSION__ - kotlin("jvm") version "1.5.10" + kotlin("jvm") version "1.5.21" // __LATEST_COMPOSE_RELEASE_VERSION__ - id("org.jetbrains.compose") version "0.4.0" + id("org.jetbrains.compose") version "1.0.0-alpha1" } group = "me.user" @@ -14,6 +14,7 @@ version = "1.0" repositories { mavenCentral() + google() maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") } } @@ -23,6 +24,8 @@ dependencies { tasks.withType() { kotlinOptions.jvmTarget = "11" + kotlinOptions.allWarningsAsErrors = true + kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" } compose.desktop { diff --git a/examples/falling_balls/gradle.properties b/examples/falling-balls/gradle.properties similarity index 100% rename from examples/falling_balls/gradle.properties rename to examples/falling-balls/gradle.properties diff --git a/examples/falling_balls_with_web/gradle/wrapper/gradle-wrapper.jar b/examples/falling-balls/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from examples/falling_balls_with_web/gradle/wrapper/gradle-wrapper.jar rename to examples/falling-balls/gradle/wrapper/gradle-wrapper.jar diff --git a/examples/falling_balls/gradle/wrapper/gradle-wrapper.properties b/examples/falling-balls/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from examples/falling_balls/gradle/wrapper/gradle-wrapper.properties rename to examples/falling-balls/gradle/wrapper/gradle-wrapper.properties diff --git a/examples/falling_balls_with_web/gradlew b/examples/falling-balls/gradlew similarity index 100% rename from examples/falling_balls_with_web/gradlew rename to examples/falling-balls/gradlew diff --git a/examples/falling_balls/gradlew.bat b/examples/falling-balls/gradlew.bat similarity index 100% rename from examples/falling_balls/gradlew.bat rename to examples/falling-balls/gradlew.bat diff --git a/examples/falling_balls/settings.gradle.kts b/examples/falling-balls/settings.gradle.kts similarity index 100% rename from examples/falling_balls/settings.gradle.kts rename to examples/falling-balls/settings.gradle.kts diff --git a/examples/falling_balls/src/main/kotlin/Game.kt b/examples/falling-balls/src/main/kotlin/Game.kt similarity index 97% rename from examples/falling_balls/src/main/kotlin/Game.kt rename to examples/falling-balls/src/main/kotlin/Game.kt index ade43f44ef..0cd10d89d1 100644 --- a/examples/falling_balls/src/main/kotlin/Game.kt +++ b/examples/falling-balls/src/main/kotlin/Game.kt @@ -1,5 +1,6 @@ package org.jetbrains.compose.demo.falling +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.layout.* import androidx.compose.material.Button import androidx.compose.material.Slider @@ -27,7 +28,7 @@ class Game { var elapsed by mutableStateOf(0L) var score by mutableStateOf(0) - var clicked by mutableStateOf(0) + private var clicked by mutableStateOf(0) var started by mutableStateOf(false) var paused by mutableStateOf(false) @@ -72,6 +73,7 @@ class Game { } @Composable +@Preview fun FallingBallsGame() { val game = remember { Game() } val density = LocalDensity.current diff --git a/examples/falling_balls/src/main/kotlin/Piece.kt b/examples/falling-balls/src/main/kotlin/Piece.kt similarity index 100% rename from examples/falling_balls/src/main/kotlin/Piece.kt rename to examples/falling-balls/src/main/kotlin/Piece.kt diff --git a/examples/falling-balls/src/main/kotlin/main.kt b/examples/falling-balls/src/main/kotlin/main.kt new file mode 100644 index 0000000000..8b24707295 --- /dev/null +++ b/examples/falling-balls/src/main/kotlin/main.kt @@ -0,0 +1,15 @@ +package org.jetbrains.compose.demo.falling + +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.WindowSize +import androidx.compose.ui.window.WindowState +import androidx.compose.ui.window.singleWindowApplication + +@OptIn(ExperimentalComposeUiApi::class) +fun main() = singleWindowApplication( + title = "Falling Balls", state = WindowState(size = WindowSize(800.dp, 800.dp)) +) { + FallingBallsGame() +} + diff --git a/examples/falling_balls/src/main/kotlin/main.kt b/examples/falling_balls/src/main/kotlin/main.kt deleted file mode 100644 index 223b5d1daf..0000000000 --- a/examples/falling_balls/src/main/kotlin/main.kt +++ /dev/null @@ -1,10 +0,0 @@ -package org.jetbrains.compose.demo.falling - -import androidx.compose.desktop.Window -import androidx.compose.ui.unit.IntSize - -fun main() = - Window(title = "Falling Balls", size = IntSize(800, 800)) { - FallingBallsGame() - } - diff --git a/examples/imageviewer/android/build.gradle.kts b/examples/imageviewer/android/build.gradle.kts index 4beda5caa5..81b0d08199 100755 --- a/examples/imageviewer/android/build.gradle.kts +++ b/examples/imageviewer/android/build.gradle.kts @@ -5,11 +5,11 @@ plugins { } android { - compileSdkVersion(30) + compileSdk = 30 defaultConfig { - minSdkVersion(21) - targetSdkVersion(30) + minSdk = 21 + targetSdk = 30 versionCode = 1 versionName = "1.0" } @@ -22,5 +22,5 @@ android { dependencies { implementation(project(":common")) - implementation("androidx.activity:activity-compose:1.3.0-alpha02") + implementation("androidx.activity:activity-compose:1.3.0") } diff --git a/examples/imageviewer/android/src/main/java/example/imageviewer/MainActivity.kt b/examples/imageviewer/android/src/main/java/example/imageviewer/MainActivity.kt index 6b0f591109..53bb8c6160 100755 --- a/examples/imageviewer/android/src/main/java/example/imageviewer/MainActivity.kt +++ b/examples/imageviewer/android/src/main/java/example/imageviewer/MainActivity.kt @@ -3,7 +3,7 @@ package example.imageviewer import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.activity.compose.setContent -import example.imageviewer.view.BuildAppUI +import example.imageviewer.view.AppUI import example.imageviewer.model.ContentState import example.imageviewer.model.ImageRepository @@ -17,7 +17,7 @@ class MainActivity : AppCompatActivity() { ) setContent { - BuildAppUI(content) + AppUI(content) } } } \ No newline at end of file diff --git a/examples/imageviewer/build.gradle.kts b/examples/imageviewer/build.gradle.kts index 455aeb2843..4dfe785a85 100755 --- a/examples/imageviewer/build.gradle.kts +++ b/examples/imageviewer/build.gradle.kts @@ -1,8 +1,5 @@ buildscript { repositories { - mavenLocal().mavenContent { - includeModule("org.jetbrains.compose", "compose-gradle-plugin") - } google() mavenCentral() maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") @@ -10,18 +7,16 @@ buildscript { dependencies { // __LATEST_COMPOSE_RELEASE_VERSION__ - classpath("org.jetbrains.compose:compose-gradle-plugin:0.4.0") - classpath("com.android.tools.build:gradle:4.0.1") - // __KOTLIN_COMPOSE_VERSION__ - classpath(kotlin("gradle-plugin", version = "1.5.10")) + classpath("org.jetbrains.compose:compose-gradle-plugin:1.0.0-alpha1") + classpath("com.android.tools.build:gradle:4.1.0") + classpath(kotlin("gradle-plugin", version = "1.5.21")) } } allprojects { repositories { - mavenLocal() google() mavenCentral() maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") } -} +} \ No newline at end of file diff --git a/examples/imageviewer/common/build.gradle.kts b/examples/imageviewer/common/build.gradle.kts index f1f3c97bfa..55ff20d258 100755 --- a/examples/imageviewer/common/build.gradle.kts +++ b/examples/imageviewer/common/build.gradle.kts @@ -20,7 +20,7 @@ kotlin { } named("androidMain") { dependencies { - api("androidx.appcompat:appcompat:1.3.0-beta01") + api("androidx.appcompat:appcompat:1.3.1") api("androidx.core:core-ktx:1.3.1") implementation("io.ktor:ktor-client-cio:1.4.1") } @@ -35,13 +35,11 @@ kotlin { } android { - compileSdkVersion(30) + compileSdk = 30 defaultConfig { - minSdkVersion(21) - targetSdkVersion(30) - versionCode = 1 - versionName = "1.0" + minSdk = 21 + targetSdk = 30 } compileOptions { diff --git a/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/ContentState.kt b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/AndroidContentState.kt old mode 100755 new mode 100644 similarity index 91% rename from examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/ContentState.kt rename to examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/AndroidContentState.kt index 95d38a2094..00d4b026bc --- a/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/ContentState.kt +++ b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/AndroidContentState.kt @@ -31,7 +31,7 @@ object ContentState { this.uriRepository = uriRepository repository = ImageRepository(uriRepository) appliedFilters = FiltersManager(context) - isAppUIReady.value = false + isContentReady.value = false initData() @@ -50,9 +50,14 @@ object ContentState { return context.resources.configuration.orientation } - private val isAppUIReady = mutableStateOf(false) + private val isAppReady = mutableStateOf(false) + fun isAppReady(): Boolean { + return isAppReady.value + } + + private val isContentReady = mutableStateOf(false) fun isContentReady(): Boolean { - return isAppUIReady.value + return isContentReady.value } fun getString(id: Int): String { @@ -142,7 +147,7 @@ object ContentState { // application content initialization private fun initData() { - if (isAppUIReady.value) + if (isContentReady.value) return val directory = context.cacheDir.absolutePath @@ -158,7 +163,7 @@ object ContentState { getString(R.string.repo_invalid), context ) - isAppUIReady.value = true + onContentReady() } return@execute } @@ -171,7 +176,7 @@ object ContentState { getString(R.string.repo_empty), context ) - isAppUIReady.value = true + onContentReady() } } else { val picture = loadFullImage(imageList[0]) @@ -186,7 +191,7 @@ object ContentState { mainImage.value = MainImageWrapper.getImage() currentImageIndex.value = MainImageWrapper.getId() } - isAppUIReady.value = true + onContentReady() } } } else { @@ -195,7 +200,7 @@ object ContentState { getString(R.string.no_internet), context ) - isAppUIReady.value = true + onContentReady() } } } catch (e: Exception) { @@ -210,7 +215,7 @@ object ContentState { } fun fullscreen(picture: Picture) { - isAppUIReady.value = false + isContentReady.value = false AppState.screenState(ScreenType.FullscreenImage) setMainImage(picture) } @@ -218,9 +223,10 @@ object ContentState { fun setMainImage(picture: Picture) { if (MainImageWrapper.getId() == picture.id) { if (!isContentReady()) - isAppUIReady.value = true + onContentReady() return } + isContentReady.value = false executor.execute { if (isInternetAvailable()) { @@ -230,7 +236,7 @@ object ContentState { handler.post { wrapPictureIntoMainImage(fullSizePicture) - isAppUIReady.value = true + onContentReady() } } else { handler.post { @@ -244,6 +250,11 @@ object ContentState { } } + private fun onContentReady() { + isContentReady.value = true + isAppReady.value = true + } + private fun wrapPictureIntoMainImage(picture: Picture) { MainImageWrapper.wrapPicture(picture) MainImageWrapper.saveOrigin() @@ -282,8 +293,9 @@ object ContentState { if (isInternetAvailable()) { handler.post { clearCache(context) + MainImageWrapper.clear() miniatures.clear() - isAppUIReady.value = false + isContentReady.value = false initData() } } else { @@ -334,6 +346,10 @@ private object MainImageWrapper { return (picture.value.name == "") } + fun clear() { + picture.value = Picture(image = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)) + } + fun getName(): String { return picture.value.name } diff --git a/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/utils/GraphicsMath.kt b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/utils/GraphicsMath.kt index 1878454c64..32e234da08 100755 --- a/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/utils/GraphicsMath.kt +++ b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/utils/GraphicsMath.kt @@ -8,6 +8,9 @@ import android.renderscript.Element import android.renderscript.RenderScript import android.renderscript.ScriptIntrinsicBlur import androidx.compose.ui.layout.ContentScale +import kotlin.math.pow +import kotlin.math.roundToInt +import example.imageviewer.view.DragHandler fun scaleBitmapAspectRatio( bitmap: Bitmap, @@ -116,3 +119,77 @@ fun displayWidth(): Int { fun displayHeight(): Int { return Resources.getSystem().displayMetrics.heightPixels } + +fun cropBitmapByScale(bitmap: Bitmap, scale: Float, drag: DragHandler): Bitmap { + val crop = cropBitmapByBounds( + bitmap, + getDisplayBounds(bitmap), + scale, + drag + ) + return Bitmap.createBitmap( + bitmap, + crop.left, + crop.top, + crop.right - crop.left, + crop.bottom - crop.top + ) +} + +fun cropBitmapByBounds( + bitmap: Bitmap, + bounds: Rect, + scaleFactor: Float, + drag: DragHandler +): Rect { + if (scaleFactor <= 1f) + return Rect(0, 0, bitmap.width, bitmap.height) + + var scale = scaleFactor.toDouble().pow(1.4) + + var boundW = (bounds.width() / scale).roundToInt() + var boundH = (bounds.height() / scale).roundToInt() + + scale *= displayWidth() / bounds.width().toDouble() + + val offsetX = drag.getAmount().x / scale + val offsetY = drag.getAmount().y / scale + + if (boundW > bitmap.width) { + boundW = bitmap.width + } + if (boundH > bitmap.height) { + boundH = bitmap.height + } + + val invisibleW = bitmap.width - boundW + var leftOffset = (invisibleW / 2.0 - offsetX).roundToInt().toFloat() + + if (leftOffset > invisibleW) { + leftOffset = invisibleW.toFloat() + drag.getAmount().x = -((invisibleW / 2.0) * scale).roundToInt().toFloat() + } + if (leftOffset < 0) { + drag.getAmount().x = ((invisibleW / 2.0) * scale).roundToInt().toFloat() + leftOffset = 0f + } + + val invisibleH = bitmap.height - boundH + var topOffset = (invisibleH / 2 - offsetY).roundToInt().toFloat() + + if (topOffset > invisibleH) { + topOffset = invisibleH.toFloat() + drag.getAmount().y = -((invisibleH / 2.0) * scale).roundToInt().toFloat() + } + if (topOffset < 0) { + drag.getAmount().y = ((invisibleH / 2.0) * scale).roundToInt().toFloat() + topOffset = 0f + } + + return Rect( + leftOffset.toInt(), + topOffset.toInt(), + (leftOffset + boundW).toInt(), + (topOffset + boundH).toInt() + ) +} \ No newline at end of file diff --git a/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/AppUI.kt b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/AppUI.kt index 89115c7e53..dacce3b7c3 100755 --- a/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/AppUI.kt +++ b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/AppUI.kt @@ -14,18 +14,18 @@ import example.imageviewer.model.ContentState import example.imageviewer.style.Gray @Composable -fun BuildAppUI(content: ContentState) { +fun AppUI(content: ContentState) { Surface( modifier = Modifier.fillMaxSize(), color = Gray ) { when (AppState.screenState()) { - ScreenType.Main -> { - setMainScreen(content) + ScreenType.MainScreen -> { + MainScreen(content) } ScreenType.FullscreenImage -> { - setImageFullScreen(content) + FullscreenImage(content) } } } diff --git a/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/FullImageScreen.kt b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/FullscreenImage.kt old mode 100755 new mode 100644 similarity index 69% rename from examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/FullImageScreen.kt rename to examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/FullscreenImage.kt index 97143b4c05..1c0e7d73b4 --- a/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/FullImageScreen.kt +++ b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/FullscreenImage.kt @@ -47,6 +47,7 @@ import example.imageviewer.style.icFilterGrayscaleOn import example.imageviewer.style.icFilterPixelOff import example.imageviewer.style.icFilterPixelOn import example.imageviewer.utils.adjustImageScale +import example.imageviewer.utils.cropBitmapByScale import example.imageviewer.utils.displayWidth import example.imageviewer.utils.getDisplayBounds import kotlin.math.abs @@ -54,37 +55,20 @@ import kotlin.math.pow import kotlin.math.roundToInt @Composable -fun setImageFullScreen( +fun FullscreenImage( content: ContentState ) { - if (content.isContentReady()) { - Column { - setToolBar(content.getSelectedImageName(), content) - setImage(content) - } - } else { - setLoadingScreen() + Column { + ToolBar(content.getSelectedImageName(), content) + Image(content) } -} - -@Composable -private fun setLoadingScreen() { - - Box { - Surface(color = MiniatureColor, modifier = Modifier.height(44.dp)) {} - Box { - Surface(color = DarkGray, elevation = 4.dp, shape = CircleShape) { - CircularProgressIndicator( - modifier = Modifier.size(50.dp).padding(3.dp, 3.dp, 4.dp, 4.dp), - color = DarkGreen - ) - } - } + if (!content.isContentReady()) { + LoadingScreen() } } @Composable -fun setToolBar( +fun ToolBar( text: String, content: ContentState ) { @@ -100,7 +84,7 @@ fun setToolBar( onClick = { if (content.isContentReady()) { content.restoreMainImage() - AppState.screenState(ScreenType.Main) + AppState.screenState(ScreenType.MainScreen) } }) { Image( @@ -160,7 +144,6 @@ fun FilterButton( @Composable fun getFilterImage(type: FilterType, content: ContentState): Painter { - return when (type) { FilterType.GrayScale -> if (content.isFilterEnabled(type)) icFilterGrayscaleOn() else icFilterGrayscaleOff() FilterType.Pixel -> if (content.isFilterEnabled(type)) icFilterPixelOn() else icFilterPixelOff() @@ -169,8 +152,7 @@ fun getFilterImage(type: FilterType, content: ContentState): Painter { } @Composable -fun setImage(content: ContentState) { - +fun Image(content: ContentState) { val drag = remember { DragHandler() } val scale = remember { ScaleHandler() } @@ -213,79 +195,3 @@ fun imageByGesture( return bitmap } - -private fun cropBitmapByScale(bitmap: Bitmap, scale: Float, drag: DragHandler): Bitmap { - - val crop = cropBitmapByBounds( - bitmap, - getDisplayBounds(bitmap), - scale, - drag - ) - return Bitmap.createBitmap( - bitmap, - crop.left, - crop.top, - crop.right - crop.left, - crop.bottom - crop.top - ) -} - -private fun cropBitmapByBounds( - bitmap: Bitmap, - bounds: Rect, - scaleFactor: Float, - drag: DragHandler -): Rect { - - if (scaleFactor <= 1f) - return Rect(0, 0, bitmap.width, bitmap.height) - - var scale = scaleFactor.toDouble().pow(1.4) - - var boundW = (bounds.width() / scale).roundToInt() - var boundH = (bounds.height() / scale).roundToInt() - - scale *= displayWidth() / bounds.width().toDouble() - - val offsetX = drag.getAmount().x / scale - val offsetY = drag.getAmount().y / scale - - if (boundW > bitmap.width) { - boundW = bitmap.width - } - if (boundH > bitmap.height) { - boundH = bitmap.height - } - - val invisibleW = bitmap.width - boundW - var leftOffset = (invisibleW / 2.0 - offsetX).roundToInt().toFloat() - - if (leftOffset > invisibleW) { - leftOffset = invisibleW.toFloat() - drag.getAmount().x = -((invisibleW / 2.0) * scale).roundToInt().toFloat() - } - if (leftOffset < 0) { - drag.getAmount().x = ((invisibleW / 2.0) * scale).roundToInt().toFloat() - leftOffset = 0f - } - - val invisibleH = bitmap.height - boundH - var topOffset = (invisibleH / 2 - offsetY).roundToInt().toFloat() - - if (topOffset > invisibleH) { - topOffset = invisibleH.toFloat() - drag.getAmount().y = -((invisibleH / 2.0) * scale).roundToInt().toFloat() - } - if (topOffset < 0) { - drag.getAmount().y = ((invisibleH / 2.0) * scale).roundToInt().toFloat() - topOffset = 0f - } - - return Rect( - leftOffset.toInt(), - topOffset.toInt(), - (leftOffset + boundW).toInt(), - (topOffset + boundH).toInt() - ) -} diff --git a/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/MainScreen.kt b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/MainScreen.kt index a8b88fb21b..5509e28050 100755 --- a/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/MainScreen.kt +++ b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/MainScreen.kt @@ -47,58 +47,30 @@ import example.imageviewer.style.icDots import example.imageviewer.style.icEmpty import example.imageviewer.style.icRefresh - @Composable -fun setMainScreen(content: ContentState) { - - if (content.isContentReady()) { - Column { - setTopContent(content) - setScrollableArea(content) - } - } else { - setLoadingScreen(content) +fun MainScreen(content: ContentState) { + Column { + TopContent(content) + ScrollableArea(content) } -} - -@Composable -fun setLoadingScreen(content: ContentState) { - - Box { - Column { - setTopContent(content) - } - Box(modifier = Modifier.align(Alignment.Center)) { - Surface(color = DarkGray, elevation = 4.dp, shape = CircleShape) { - CircularProgressIndicator( - modifier = Modifier.size(50.dp).padding(4.dp), - color = DarkGreen - ) - } - } - Text( - text = content.getString(R.string.loading), - modifier = Modifier.align(Alignment.Center).offset(0.dp, 70.dp), - style = MaterialTheme.typography.body1, - color = Foreground - ) + if (!content.isContentReady()) { + LoadingScreen(content.getString(R.string.loading)) } } @Composable -fun setTopContent(content: ContentState) { - setTitleBar(text = content.getString(R.string.app_name), content = content) +fun TopContent(content: ContentState) { + TitleBar(text = content.getString(R.string.app_name), content = content) if (content.getOrientation() == Configuration.ORIENTATION_PORTRAIT) { - setPreviewImageUI(content) - setSpacer(h = 10) - setDivider() + PreviewImage(content) + Spacer(modifier = Modifier.height(10.dp)) + Divider() } - setSpacer(h = 5) + Spacer(modifier = Modifier.height(5.dp)) } @Composable -fun setTitleBar(text: String, content: ContentState) { - +fun TitleBar(text: String, content: ContentState) { TopAppBar( backgroundColor = DarkGreen, title = { @@ -132,8 +104,7 @@ fun setTitleBar(text: String, content: ContentState) { } @Composable -fun setPreviewImageUI(content: ContentState) { - +fun PreviewImage(content: ContentState) { Clickable(onClick = { AppState.screenState(ScreenType.FullscreenImage) }) { @@ -159,11 +130,10 @@ fun setPreviewImageUI(content: ContentState) { } @Composable -fun setMiniatureUI( +fun Miniature( picture: Picture, content: ContentState ) { - Card( backgroundColor = MiniatureColor, modifier = Modifier.padding(start = 10.dp, end = 10.dp).height(70.dp) @@ -224,12 +194,12 @@ fun setMiniatureUI( } @Composable -fun setScrollableArea(content: ContentState) { +fun ScrollableArea(content: ContentState) { var index = 1 val scrollState = rememberScrollState() Column(Modifier.verticalScroll(scrollState)) { for (picture in content.getMiniatures()) { - setMiniatureUI( + Miniature( picture = picture, content = content ) @@ -240,16 +210,9 @@ fun setScrollableArea(content: ContentState) { } @Composable -fun setDivider() { - +fun Divider() { Divider( color = LightGray, modifier = Modifier.padding(start = 10.dp, end = 10.dp) ) } - -@Composable -fun setSpacer(h: Int) { - - Spacer(modifier = Modifier.height(h.dp)) -} diff --git a/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/model/ScreenType.kt b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/model/ScreenType.kt index d347ed85ab..8e38a79e52 100755 --- a/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/model/ScreenType.kt +++ b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/model/ScreenType.kt @@ -4,13 +4,13 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf enum class ScreenType { - Main, FullscreenImage + MainScreen, FullscreenImage } object AppState { private var screen: MutableState init { - screen = mutableStateOf(ScreenType.Main) + screen = mutableStateOf(ScreenType.MainScreen) } fun screenState() : ScreenType { diff --git a/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Draggable.kt b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Draggable.kt index ed9ac2f2ec..f9f3b30bcf 100755 --- a/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Draggable.kt +++ b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Draggable.kt @@ -15,6 +15,7 @@ import example.imageviewer.style.Transparent fun Draggable( dragHandler: DragHandler, modifier: Modifier = Modifier, + onUpdate: (() -> Unit)? = null, children: @Composable() () -> Unit ) { Surface( @@ -26,6 +27,7 @@ fun Draggable( onDragCancel = { dragHandler.cancel() }, ) { change, dragAmount -> dragHandler.drag(dragAmount) + onUpdate?.invoke() change.consumePositionChange() } } diff --git a/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/LoadingScreen.kt b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/LoadingScreen.kt new file mode 100644 index 0000000000..8a6a4191f6 --- /dev/null +++ b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/LoadingScreen.kt @@ -0,0 +1,43 @@ +package example.imageviewer.view + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import example.imageviewer.style.DarkGray +import example.imageviewer.style.DarkGreen +import example.imageviewer.style.Foreground +import example.imageviewer.style.TranslucentBlack + +@Composable +fun LoadingScreen(text: String = "") { + Box( + modifier = Modifier.fillMaxSize().background(color = TranslucentBlack) + ) { + Box(modifier = Modifier.align(Alignment.Center)) { + Surface(color = DarkGray, elevation = 4.dp, shape = CircleShape) { + CircularProgressIndicator( + modifier = Modifier.size(50.dp).padding(3.dp, 3.dp, 4.dp, 4.dp), + color = DarkGreen + ) + } + } + Text( + text = text, + modifier = Modifier.align(Alignment.Center).offset(0.dp, 70.dp), + style = MaterialTheme.typography.body1, + color = Foreground + ) + } +} \ No newline at end of file diff --git a/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Scalable.kt b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Scalable.kt index e380ff413f..ef9887c4f6 100755 --- a/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Scalable.kt +++ b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Scalable.kt @@ -18,7 +18,7 @@ fun Scalable( Surface( color = Transparent, modifier = modifier.pointerInput(Unit) { - detectTapGestures(onDoubleTap = { onScale.resetFactor() }) + detectTapGestures(onDoubleTap = { onScale.reset() }) detectTransformGestures { _, _, zoom, _ -> onScale.onScale(zoom) } @@ -31,7 +31,7 @@ fun Scalable( class ScaleHandler(private val maxFactor: Float = 5f, private val minFactor: Float = 1f) { val factor = mutableStateOf(1f) - fun resetFactor() { + fun reset() { if (factor.value > minFactor) factor.value = minFactor } diff --git a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/R.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/R.kt index 4b89b548a1..ef361ecc84 100755 --- a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/R.kt +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/R.kt @@ -14,6 +14,8 @@ object ResString { val picture: String val size: String val pixels: String + val back: String + val refresh: String init { if (System.getProperty("user.language").equals("ru")) { @@ -29,6 +31,8 @@ object ResString { picture = "Изображение:" size = "Размеры:" pixels = "пикселей." + back = "Назад" + refresh = "Обновить" } else { appName = "ImageViewer" loading = "Loading images..." @@ -42,6 +46,8 @@ object ResString { picture = "Picture:" size = "Size:" pixels = "pixels." + back = "Back" + refresh = "Refresh" } } } diff --git a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/DesktopContentState.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/DesktopContentState.kt index 82cb159e7b..35a60c5dd6 100644 --- a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/DesktopContentState.kt +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/DesktopContentState.kt @@ -1,8 +1,10 @@ package example.imageviewer.model import androidx.compose.runtime.MutableState -import androidx.compose.runtime.RememberObserver import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.window.WindowState +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.ImageBitmap import example.imageviewer.ResString import example.imageviewer.core.FilterType import example.imageviewer.model.filtration.FiltersManager @@ -10,19 +12,31 @@ import example.imageviewer.utils.cacheImagePath import example.imageviewer.utils.clearCache import example.imageviewer.utils.isInternetAvailable import example.imageviewer.view.showPopUpMessage +import example.imageviewer.view.DragHandler +import example.imageviewer.view.ScaleHandler +import example.imageviewer.utils.cropBitmapByScale +import example.imageviewer.utils.toByteArray import java.awt.image.BufferedImage import java.io.File import java.util.concurrent.ExecutorService import java.util.concurrent.Executors -import javax.swing.SwingUtilities.invokeLater - - -object ContentState : RememberObserver { - +import org.jetbrains.skija.Image.makeFromEncoded +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay + +object ContentState { + val drag = DragHandler() + val scale = ScaleHandler() + lateinit var windowState: WindowState private lateinit var repository: ImageRepository private lateinit var uriRepository: String + val scope = CoroutineScope(Dispatchers.IO) - fun applyContent(uriRepository: String): ContentState { + fun applyContent(state: WindowState, uriRepository: String): ContentState { + windowState = state if (this::uriRepository.isInitialized && this.uriRepository == uriRepository) { return this } @@ -35,8 +49,6 @@ object ContentState : RememberObserver { return this } - private val executor: ExecutorService by lazy { Executors.newFixedThreadPool(2) } - private val isAppReady = mutableStateOf(false) fun isAppReady(): Boolean { return isAppReady.value @@ -48,7 +60,6 @@ object ContentState : RememberObserver { } // drawable content - private val mainImageWrapper = MainImageWrapper private val mainImage = mutableStateOf(BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB)) private val currentImageIndex = mutableStateOf(0) private val miniatures = Miniatures() @@ -57,12 +68,12 @@ object ContentState : RememberObserver { return miniatures.getMiniatures() } - fun getSelectedImage(): BufferedImage { - return mainImage.value + fun getSelectedImage(): ImageBitmap { + return MainImageWrapper.mainImageAsImageBitmap.value } fun getSelectedImageName(): String { - return mainImageWrapper.getName() + return MainImageWrapper.getName() } // filters managing @@ -79,7 +90,6 @@ object ContentState : RememberObserver { } fun toggleFilter(filter: FilterType) { - if (containsFilter(filter)) { removeFilter(filter) } else { @@ -88,23 +98,24 @@ object ContentState : RememberObserver { toggleFilterState(filter) - var bitmap = mainImageWrapper.origin + var bitmap = MainImageWrapper.origin if (bitmap != null) { bitmap = appliedFilters.applyFilters(bitmap) - mainImageWrapper.setImage(bitmap) + MainImageWrapper.setImage(bitmap) mainImage.value = bitmap + updateMainImage() } } private fun addFilter(filter: FilterType) { appliedFilters.add(filter) - mainImageWrapper.addFilter(filter) + MainImageWrapper.addFilter(filter) } private fun removeFilter(filter: FilterType) { appliedFilters.remove(filter) - mainImageWrapper.removeFilter(filter) + MainImageWrapper.removeFilter(filter) } private fun containsFilter(type: FilterType): Boolean { @@ -121,7 +132,7 @@ object ContentState : RememberObserver { private fun restoreFilters(): BufferedImage { filterUIState.clear() appliedFilters.clear() - return mainImageWrapper.restore() + return MainImageWrapper.restore() } fun restoreMainImage() { @@ -138,52 +149,41 @@ object ContentState : RememberObserver { directory.mkdir() } - executor.execute { + scope.launch(Dispatchers.IO) { try { if (isInternetAvailable()) { val imageList = repository.get() if (imageList.isEmpty()) { - invokeLater { - showPopUpMessage( - ResString.repoInvalid - ) - onContentReady() - } - return@execute - } - - val pictureList = loadImages(cacheImagePath, imageList) + showPopUpMessage( + ResString.repoInvalid + ) + onContentReady() + } else { + val pictureList = loadImages(cacheImagePath, imageList) - if (pictureList.isEmpty()) { - invokeLater { + if (pictureList.isEmpty()) { showPopUpMessage( ResString.repoEmpty ) onContentReady() - } - } else { - val picture = loadFullImage(imageList[0]) - - invokeLater { + } else { + val picture = loadFullImage(imageList[0]) miniatures.setMiniatures(pictureList) - if (isMainImageEmpty()) { wrapPictureIntoMainImage(picture) } else { - appliedFilters.add(mainImageWrapper.getFilters()) - currentImageIndex.value = mainImageWrapper.getId() + appliedFilters.add(MainImageWrapper.getFilters()) + currentImageIndex.value = MainImageWrapper.getId() } onContentReady() } } } else { - invokeLater { - showPopUpMessage( - ResString.noInternet - ) - onContentReady() - } + showPopUpMessage( + ResString.noInternet + ) + onContentReady() } } catch (e: Exception) { e.printStackTrace() @@ -193,7 +193,7 @@ object ContentState : RememberObserver { // preview/fullscreen image managing fun isMainImageEmpty(): Boolean { - return mainImageWrapper.isEmpty() + return MainImageWrapper.isEmpty() } fun fullscreen(picture: Picture) { @@ -203,31 +203,27 @@ object ContentState : RememberObserver { } fun setMainImage(picture: Picture) { - if (mainImageWrapper.getId() == picture.id) { + if (MainImageWrapper.getId() == picture.id) { if (!isContentReady()) { onContentReady() } return } + isContentReady.value = false - executor.execute { + scope.launch(Dispatchers.IO) { + scale.reset() if (isInternetAvailable()) { - - invokeLater { val fullSizePicture = loadFullImage(picture.source) fullSizePicture.id = picture.id wrapPictureIntoMainImage(fullSizePicture) - onContentReady() - } } else { - invokeLater { showPopUpMessage( "${ResString.noInternet}\n${ResString.loadImageUnavailable}" ) wrapPictureIntoMainImage(picture) - onContentReady() - } } + onContentReady() } } @@ -237,10 +233,24 @@ object ContentState : RememberObserver { } private fun wrapPictureIntoMainImage(picture: Picture) { - mainImageWrapper.wrapPicture(picture) - mainImageWrapper.saveOrigin() + MainImageWrapper.wrapPicture(picture) + MainImageWrapper.saveOrigin() mainImage.value = picture.image currentImageIndex.value = picture.id + updateMainImage() + } + + fun updateMainImage() { + MainImageWrapper.mainImageAsImageBitmap.value = makeFromEncoded( + toByteArray( + cropBitmapByScale( + mainImage.value, + windowState.size, + scale.factor.value, + drag + ) + ) + ).asImageBitmap() } fun swipeNext() { @@ -264,29 +274,20 @@ object ContentState : RememberObserver { } fun refresh() { - executor.execute { + scope.launch(Dispatchers.IO) { if (isInternetAvailable()) { - invokeLater { - clearCache() - miniatures.clear() - isContentReady.value = false - initData() - } + clearCache() + MainImageWrapper.clear() + miniatures.clear() + isContentReady.value = false + initData() } else { - invokeLater { - showPopUpMessage( - "${ResString.noInternet}\n${ResString.refreshUnavailable}" - ) - } + showPopUpMessage( + "${ResString.noInternet}\n${ResString.refreshUnavailable}" + ) } } } - - override fun onRemembered() { } - override fun onAbandoned() { } - override fun onForgotten() { - executor.shutdown() - } } private object MainImageWrapper { @@ -299,15 +300,15 @@ private object MainImageWrapper { } fun restore(): BufferedImage { - if (origin != null) { picture.value.image = copy(origin!!) filtersSet.clear() } - return copy(picture.value.image) } + var mainImageAsImageBitmap = mutableStateOf(ImageBitmap(1, 1)) + // picture adapter private var picture = mutableStateOf( Picture(image = BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB)) @@ -325,6 +326,10 @@ private object MainImageWrapper { return (picture.value.name == "") } + fun clear() { + picture.value = Picture(image = BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB)) + } + fun getName(): String { return picture.value.name } diff --git a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/style/Decoration.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/style/Decoration.kt index f455f3e1c1..7c06d90124 100755 --- a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/style/Decoration.kt +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/style/Decoration.kt @@ -1,58 +1,42 @@ package example.imageviewer.style import androidx.compose.runtime.Composable -import androidx.compose.ui.res.imageResource +import androidx.compose.ui.res.painterResource import java.awt.image.BufferedImage import javax.imageio.ImageIO @Composable -fun icEmpty() = imageResource("images/empty.png") +fun icEmpty() = painterResource("images/empty.png") @Composable -fun icBack() = imageResource("images/back.png") +fun icBack() = painterResource("images/back.png") @Composable -fun icRefresh() = imageResource("images/refresh.png") +fun icRefresh() = painterResource("images/refresh.png") @Composable -fun icDots() = imageResource("images/dots.png") +fun icDots() = painterResource("images/dots.png") @Composable -fun icFilterGrayscaleOn() = imageResource("images/grayscale_on.png") +fun icFilterGrayscaleOn() = painterResource("images/grayscale_on.png") @Composable -fun icFilterGrayscaleOff() = imageResource("images/grayscale_off.png") +fun icFilterGrayscaleOff() = painterResource("images/grayscale_off.png") @Composable -fun icFilterPixelOn() = imageResource("images/pixel_on.png") +fun icFilterPixelOn() = painterResource("images/pixel_on.png") @Composable -fun icFilterPixelOff() = imageResource("images/pixel_off.png") +fun icFilterPixelOff() = painterResource("images/pixel_off.png") @Composable -fun icFilterBlurOn() = imageResource("images/blur_on.png") +fun icFilterBlurOn() = painterResource("images/blur_on.png") @Composable -fun icFilterBlurOff() = imageResource("images/blur_off.png") +fun icFilterBlurOff() = painterResource("images/blur_off.png") @Composable -fun icFilterUnknown() = imageResource("images/filter_unknown.png") +fun icFilterUnknown() = painterResource("images/filter_unknown.png") -private var icon: BufferedImage? = null -fun icAppRounded(): BufferedImage { - if (icon != null) { - return icon!! - } - try { - val imageRes = "images/ic_imageviewer_round.png" - val img = Thread.currentThread().contextClassLoader.getResource(imageRes) - val bitmap: BufferedImage? = ImageIO.read(img) - if (bitmap != null) { - icon = bitmap - return bitmap - } - } catch (e: Exception) { - e.printStackTrace() - } - return BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB) -} +@Composable +fun icAppRounded() = painterResource("images/ic_imageviewer_round.png") diff --git a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/Application.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/Application.kt deleted file mode 100644 index dc736047e5..0000000000 --- a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/Application.kt +++ /dev/null @@ -1,158 +0,0 @@ -package example.imageviewer.utils - -import androidx.compose.desktop.AppManager -import androidx.compose.desktop.AppWindow -import androidx.compose.desktop.WindowEvents -import androidx.compose.runtime.* -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.window.v1.MenuBar -import kotlinx.coroutines.* -import kotlinx.coroutines.swing.Swing -import java.awt.image.BufferedImage - -fun Application( - content: @Composable ApplicationScope.() -> Unit -) { - GlobalScope.launch(Dispatchers.Swing + ImmediateFrameClock()) { - AppManager.setEvents(onWindowsEmpty = null) - - withRunningRecomposer { recomposer -> - val latch = CompletableDeferred() - val applier = ApplicationApplier { latch.complete(Unit) } - - val composition = Composition(applier, recomposer) - try { - val scope = ApplicationScope(recomposer) - - composition.setContent { scope.content() } - - latch.join() - } finally { - composition.dispose() - } - } - } -} - -class ApplicationScope internal constructor(private val recomposer: Recomposer) { - @Composable - fun ComposableWindow( - title: String = "JetpackDesktopWindow", - size: IntSize = IntSize(800, 600), - location: IntOffset = IntOffset.Zero, - centered: Boolean = true, - icon: BufferedImage? = null, - menuBar: MenuBar? = null, - undecorated: Boolean = false, - resizable: Boolean = true, - events: WindowEvents = WindowEvents(), - onDismissRequest: (() -> Unit)? = null, - content: @Composable () -> Unit = {} - ) { - var isOpened by remember { mutableStateOf(true) } - if (!isOpened) return - ComposeNode( - factory = { - val window = AppWindow( - title = title, - size = size, - location = location, - centered = centered, - icon = icon, - menuBar = menuBar, - undecorated = undecorated, - resizable = resizable, - events = events, - onDismissRequest = { - onDismissRequest?.invoke() - isOpened = false - } - ) - window.show(recomposer, content) - window - }, - update = { - set(title) { setTitle(it) } - set(size) { setSize(it.width, it.height) } - // set(location) { setLocation(it.x, it.y) } - set(icon) { setIcon(it) } - // set(menuBar) { if (it != null) setMenuBar(it) else removeMenuBar() } - // set(resizable) { setResizable(it) } - // set(events) { setEvents(it) } - // set(onDismissRequest) { setDismiss(it) } - } - ) - } -} - -private class ImmediateFrameClock : MonotonicFrameClock { - override suspend fun withFrameNanos( - onFrame: (frameTimeNanos: Long) -> R - ) = onFrame(System.nanoTime()) -} - -private class ApplicationApplier( - private val onWindowsEmpty: () -> Unit -) : Applier { - private val windows = mutableListOf() - - override var current: AppWindow? = null - - override fun insertBottomUp(index: Int, instance: AppWindow?) { - requireNotNull(instance) - check(current == null) { "Windows cannot be nested!" } - windows.add(index, instance) - } - - override fun remove(index: Int, count: Int) { - repeat(count) { - val window = windows.removeAt(index) - if (!window.isClosed) { - window.close() - } - } - } - - override fun move(from: Int, to: Int, count: Int) { - if (from > to) { - var current = to - repeat(count) { - val node = windows.removeAt(from) - windows.add(current, node) - current++ - } - } else { - repeat(count) { - val node = windows.removeAt(from) - windows.add(to - 1, node) - } - } - } - - override fun clear() { - windows.forEach { if (!it.isClosed) it.close() } - windows.clear() - } - - override fun onEndChanges() { - if (windows.isEmpty()) { - onWindowsEmpty() - } - } - - override fun down(node: AppWindow?) { - requireNotNull(node) - check(current == null) { "Windows cannot be nested!" } - current = node - } - - override fun up() { - check(current != null) { "Windows cannot be nested!" } - current = null - } - - override fun insertTopDown(index: Int, instance: AppWindow?) { - // ignored. Building tree bottom-up - } -} diff --git a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/GraphicsMath.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/GraphicsMath.kt index 119a2457f4..8082fdb53c 100755 --- a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/GraphicsMath.kt +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/GraphicsMath.kt @@ -1,7 +1,7 @@ package example.imageviewer.utils -import androidx.compose.desktop.AppManager -import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.window.WindowSize +import androidx.compose.ui.unit.dp import java.awt.Dimension import java.awt.Graphics2D import java.awt.Rectangle @@ -14,6 +14,9 @@ import javax.imageio.ImageIO import java.awt.image.BufferedImageOp import java.awt.image.ConvolveOp import java.awt.image.Kernel +import kotlin.math.pow +import kotlin.math.roundToInt +import example.imageviewer.view.DragHandler fun scaleBitmapAspectRatio( bitmap: BufferedImage, @@ -38,10 +41,10 @@ fun scaleBitmapAspectRatio( return result } -fun getDisplayBounds(bitmap: BufferedImage): Rectangle { +fun getDisplayBounds(bitmap: BufferedImage, windowSize: WindowSize): Rectangle { - val boundW: Float = displayWidth().toFloat() - val boundH: Float = displayHeight().toFloat() + val boundW: Float = windowSize.width.value.toFloat() + val boundH: Float = windowSize.height.value.toFloat() val ratioX: Float = bitmap.width / boundW val ratioY: Float = bitmap.height / boundH @@ -108,22 +111,6 @@ fun applyBlurFilter(bitmap: BufferedImage): BufferedImage { ) } -fun displayWidth(): Int { - val window = AppManager.focusedWindow - if (window != null) { - return window.width - } - return 0 -} - -fun displayHeight(): Int { - val window = AppManager.focusedWindow - if (window != null) { - return window.height - } - return 0 -} - fun toByteArray(bitmap: BufferedImage) : ByteArray { val baos = ByteArrayOutputStream() ImageIO.write(bitmap, "png", baos) @@ -134,11 +121,86 @@ fun cropImage(bitmap: BufferedImage, crop: Rectangle) : BufferedImage { return bitmap.getSubimage(crop.x, crop.y, crop.width, crop.height) } -fun getPreferredWindowSize(desiredWidth: Int, desiredHeight: Int): IntSize { +fun cropBitmapByScale( + bitmap: BufferedImage, + size: WindowSize, + scale: Float, + drag: DragHandler +): BufferedImage { + val crop = cropBitmapByBounds( + bitmap, + getDisplayBounds(bitmap, size), + size, + scale, + drag + ) + return cropImage( + bitmap, + Rectangle(crop.x, crop.y, crop.width - crop.x, crop.height - crop.y) + ) +} + +fun cropBitmapByBounds( + bitmap: BufferedImage, + bounds: Rectangle, + size: WindowSize, + scaleFactor: Float, + drag: DragHandler +): Rectangle { + + if (scaleFactor <= 1f) { + return Rectangle(0, 0, bitmap.width, bitmap.height) + } + + var scale = scaleFactor.toDouble().pow(1.4) + + var boundW = (bounds.width / scale).roundToInt() + var boundH = (bounds.height / scale).roundToInt() + + scale *= size.width.value / bounds.width.toDouble() + + val offsetX = drag.getAmount().x / scale + val offsetY = drag.getAmount().y / scale + + if (boundW > bitmap.width) { + boundW = bitmap.width + } + if (boundH > bitmap.height) { + boundH = bitmap.height + } + + val invisibleW = bitmap.width - boundW + var leftOffset = (invisibleW / 2.0 - offsetX).roundToInt() + + if (leftOffset > invisibleW) { + leftOffset = invisibleW + drag.getAmount().x = -((invisibleW / 2.0) * scale).roundToInt().toFloat() + } + if (leftOffset < 0) { + drag.getAmount().x = ((invisibleW / 2.0) * scale).roundToInt().toFloat() + leftOffset = 0 + } + + val invisibleH = bitmap.height - boundH + var topOffset = (invisibleH / 2 - offsetY).roundToInt() + + if (topOffset > invisibleH) { + topOffset = invisibleH + drag.getAmount().y = -((invisibleH / 2.0) * scale).roundToInt().toFloat() + } + if (topOffset < 0) { + drag.getAmount().y = ((invisibleH / 2.0) * scale).roundToInt().toFloat() + topOffset = 0 + } + + return Rectangle(leftOffset, topOffset, leftOffset + boundW, topOffset + boundH) +} + +fun getPreferredWindowSize(desiredWidth: Int, desiredHeight: Int): WindowSize { val screenSize: Dimension = Toolkit.getDefaultToolkit().screenSize val preferredWidth: Int = (screenSize.width * 0.8f).toInt() val preferredHeight: Int = (screenSize.height * 0.8f).toInt() val width: Int = if (desiredWidth < preferredWidth) desiredWidth else preferredWidth val height: Int = if (desiredHeight < preferredHeight) desiredHeight else preferredHeight - return IntSize(width, height) + return WindowSize(width.dp, height.dp) } diff --git a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/AppUI.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/AppUI.kt index cb7eee69d1..ef002711bf 100755 --- a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/AppUI.kt +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/AppUI.kt @@ -15,18 +15,18 @@ private val message: MutableState = mutableStateOf("") private val state: MutableState = mutableStateOf(false) @Composable -fun BuildAppUI(content: ContentState) { +fun AppUI(content: ContentState) { Surface( modifier = Modifier.fillMaxSize(), color = Gray ) { when (AppState.screenState()) { - ScreenType.Main -> { - setMainScreen(content) + ScreenType.MainScreen -> { + MainScreen(content) } ScreenType.FullscreenImage -> { - setImageFullScreen(content) + FullscreenImage(content) } } } diff --git a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/FullImageScreen.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/FullImageScreen.kt deleted file mode 100755 index 582294e010..0000000000 --- a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/FullImageScreen.kt +++ /dev/null @@ -1,314 +0,0 @@ -package example.imageviewer.view - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.ScrollState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.CircularProgressIndicator -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.input.key.shortcuts -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.unit.dp -import example.imageviewer.core.FilterType -import example.imageviewer.model.AppState -import example.imageviewer.model.ContentState -import example.imageviewer.model.ScreenType -import example.imageviewer.style.DarkGray -import example.imageviewer.style.DarkGreen -import example.imageviewer.style.Foreground -import example.imageviewer.style.MiniatureColor -import example.imageviewer.style.TranslucentBlack -import example.imageviewer.style.Transparent -import example.imageviewer.style.icBack -import example.imageviewer.style.icFilterBlurOff -import example.imageviewer.style.icFilterBlurOn -import example.imageviewer.style.icFilterGrayscaleOff -import example.imageviewer.style.icFilterGrayscaleOn -import example.imageviewer.style.icFilterPixelOff -import example.imageviewer.style.icFilterPixelOn -import example.imageviewer.utils.cropImage -import example.imageviewer.utils.displayWidth -import example.imageviewer.utils.getDisplayBounds -import example.imageviewer.utils.toByteArray -import java.awt.Rectangle -import java.awt.event.KeyEvent -import java.awt.image.BufferedImage -import kotlin.math.pow -import kotlin.math.roundToInt - -@Composable -fun setImageFullScreen( - content: ContentState -) { - if (content.isContentReady()) { - Column { - setToolBar(content.getSelectedImageName(), content) - setImage(content) - } - } else { - setLoadingScreen() - } -} - -@Composable -private fun setLoadingScreen() { - - Box { - Surface(color = MiniatureColor, modifier = Modifier.height(44.dp)) {} - Box(modifier = Modifier.align(Alignment.Center)) { - Surface(color = DarkGray, elevation = 4.dp, shape = CircleShape) { - CircularProgressIndicator( - modifier = Modifier.size(50.dp).padding(3.dp, 3.dp, 4.dp, 4.dp), - color = DarkGreen - ) - } - } - } -} - -@Composable -fun setToolBar( - text: String, - content: ContentState -) { - val backButtonHover = remember { mutableStateOf(false) } - Surface( - color = MiniatureColor, - modifier = Modifier.height(44.dp) - ) { - Row(modifier = Modifier.padding(end = 30.dp)) { - Surface( - color = Transparent, - modifier = Modifier.padding(start = 20.dp).align(Alignment.CenterVertically), - shape = CircleShape - ) { - Clickable( - modifier = Modifier.hover( - onEnter = { - backButtonHover.value = true - false - }, - onExit = { - backButtonHover.value = false - false - }) - .background(color = if (backButtonHover.value) TranslucentBlack else Transparent), - onClick = { - if (content.isContentReady()) { - content.restoreMainImage() - AppState.screenState(ScreenType.Main) - } - }) { - Image( - icBack(), - contentDescription = null, - modifier = Modifier.size(38.dp) - ) - } - } - Text( - text, - color = Foreground, - maxLines = 1, - modifier = Modifier.padding(start = 30.dp).weight(1f) - .align(Alignment.CenterVertically), - style = MaterialTheme.typography.body1 - ) - - Surface( - color = Color(255, 255, 255, 40), - modifier = Modifier.size(154.dp, 38.dp) - .align(Alignment.CenterVertically), - shape = CircleShape - ) { - val state = rememberScrollState(0) - Row(modifier = Modifier.horizontalScroll(state)) { - Row { - for (type in FilterType.values()) { - FilterButton(content, type) - } - } - } - } - } - } -} - -@Composable -fun FilterButton( - content: ContentState, - type: FilterType, - modifier: Modifier = Modifier.size(38.dp) -) { - val filterButtonHover = remember { mutableStateOf(false) } - Box( - modifier = Modifier.background(color = Transparent).clip(CircleShape) - ) { - Clickable( - modifier = Modifier.hover( - onEnter = { - filterButtonHover.value = true - false - }, - onExit = { - filterButtonHover.value = false - false - }) - .background(color = if (filterButtonHover.value) TranslucentBlack else Transparent), - onClick = { content.toggleFilter(type)} - ) { - Image( - getFilterImage(type = type, content = content), - contentDescription = null, - modifier - ) - } - } - - Spacer(Modifier.width(20.dp)) -} - -@Composable -fun getFilterImage(type: FilterType, content: ContentState): ImageBitmap { - - return when (type) { - FilterType.GrayScale -> if (content.isFilterEnabled(type)) icFilterGrayscaleOn() else icFilterGrayscaleOff() - FilterType.Pixel -> if (content.isFilterEnabled(type)) icFilterPixelOn() else icFilterPixelOff() - FilterType.Blur -> if (content.isFilterEnabled(type)) icFilterBlurOn() else icFilterBlurOff() - } -} - -@Composable -fun setImage(content: ContentState) { - val drag = remember { DragHandler() } - val scale = remember { ScaleHandler() } - - Surface( - color = DarkGray, - modifier = Modifier.fillMaxSize() - ) { - Draggable(dragHandler = drag, modifier = Modifier.fillMaxSize()) { - Zoomable( - onScale = scale, - modifier = Modifier.fillMaxSize() - .shortcuts { - on(Key(KeyEvent.VK_LEFT)) { - content.swipePrevious() - } - on(Key(KeyEvent.VK_RIGHT)) { - content.swipeNext() - } - } - ) { - val bitmap = imageByGesture(content, scale, drag) - Image( - bitmap = bitmap, - contentDescription = null, - contentScale = ContentScale.Fit - ) - } - } - } -} - -@Composable -fun imageByGesture( - content: ContentState, - scale: ScaleHandler, - drag: DragHandler -): ImageBitmap { - val bitmap = cropBitmapByScale(content.getSelectedImage(), scale.factor.value, drag) - return org.jetbrains.skija.Image.makeFromEncoded(toByteArray(bitmap)).asImageBitmap() -} - -private fun cropBitmapByScale(bitmap: BufferedImage, scale: Float, drag: DragHandler): BufferedImage { - - val crop = cropBitmapByBounds( - bitmap, - getDisplayBounds(bitmap), - scale, - drag - ) - return cropImage( - bitmap, - Rectangle(crop.x, crop.y, crop.width - crop.x, crop.height - crop.y) - ) -} - -private fun cropBitmapByBounds( - bitmap: BufferedImage, - bounds: Rectangle, - scaleFactor: Float, - drag: DragHandler -): Rectangle { - - if (scaleFactor <= 1f) { - return Rectangle(0, 0, bitmap.width, bitmap.height) - } - - var scale = scaleFactor.toDouble().pow(1.4) - - var boundW = (bounds.width / scale).roundToInt() - var boundH = (bounds.height / scale).roundToInt() - - scale *= displayWidth() / bounds.width.toDouble() - - val offsetX = drag.getAmount().x / scale - val offsetY = drag.getAmount().y / scale - - if (boundW > bitmap.width) { - boundW = bitmap.width - } - if (boundH > bitmap.height) { - boundH = bitmap.height - } - - val invisibleW = bitmap.width - boundW - var leftOffset = (invisibleW / 2.0 - offsetX).roundToInt() - - if (leftOffset > invisibleW) { - leftOffset = invisibleW - drag.getAmount().x = -((invisibleW / 2.0) * scale).roundToInt().toFloat() - } - if (leftOffset < 0) { - drag.getAmount().x = ((invisibleW / 2.0) * scale).roundToInt().toFloat() - leftOffset = 0 - } - - val invisibleH = bitmap.height - boundH - var topOffset = (invisibleH / 2 - offsetY).roundToInt() - - if (topOffset > invisibleH) { - topOffset = invisibleH - drag.getAmount().y = -((invisibleH / 2.0) * scale).roundToInt().toFloat() - } - if (topOffset < 0) { - drag.getAmount().y = ((invisibleH / 2.0) * scale).roundToInt().toFloat() - topOffset = 0 - } - - return Rectangle(leftOffset, topOffset, leftOffset + boundW, topOffset + boundH) -} diff --git a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/FullscreenImage.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/FullscreenImage.kt new file mode 100644 index 0000000000..cd27a48085 --- /dev/null +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/FullscreenImage.kt @@ -0,0 +1,225 @@ +package example.imageviewer.view + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.type +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.WindowSize +import example.imageviewer.core.FilterType +import example.imageviewer.model.AppState +import example.imageviewer.model.ContentState +import example.imageviewer.model.ScreenType +import example.imageviewer.ResString +import example.imageviewer.style.DarkGray +import example.imageviewer.style.DarkGreen +import example.imageviewer.style.Foreground +import example.imageviewer.style.MiniatureColor +import example.imageviewer.style.TranslucentBlack +import example.imageviewer.style.Transparent +import example.imageviewer.style.icBack +import example.imageviewer.style.icFilterBlurOff +import example.imageviewer.style.icFilterBlurOn +import example.imageviewer.style.icFilterGrayscaleOff +import example.imageviewer.style.icFilterGrayscaleOn +import example.imageviewer.style.icFilterPixelOff +import example.imageviewer.style.icFilterPixelOn + +@Composable +fun FullscreenImage( + content: ContentState +) { + Column { + ToolBar(content.getSelectedImageName(), content) + Image(content) + } + if (!content.isContentReady()) { + LoadingScreen() + } +} + +@Composable +fun ToolBar( + text: String, + content: ContentState +) { + val backButtonHover = remember { mutableStateOf(false) } + Surface( + color = MiniatureColor, + modifier = Modifier.height(44.dp) + ) { + Row(modifier = Modifier.padding(end = 30.dp)) { + Surface( + color = Transparent, + modifier = Modifier.padding(start = 20.dp).align(Alignment.CenterVertically), + shape = CircleShape + ) { + Tooltip(ResString.back) { + Clickable( + modifier = Modifier.hover( + onEnter = { + backButtonHover.value = true + false + }, + onExit = { + backButtonHover.value = false + false + }) + .background(color = if (backButtonHover.value) TranslucentBlack else Transparent), + onClick = { + if (content.isContentReady()) { + content.restoreMainImage() + AppState.screenState(ScreenType.MainScreen) + } + }) { + Image( + icBack(), + contentDescription = null, + modifier = Modifier.size(38.dp) + ) + } + } + } + Text( + text, + color = Foreground, + maxLines = 1, + modifier = Modifier.padding(start = 30.dp).weight(1f) + .align(Alignment.CenterVertically), + style = MaterialTheme.typography.body1 + ) + + Surface( + color = Color(255, 255, 255, 40), + modifier = Modifier.size(154.dp, 38.dp) + .align(Alignment.CenterVertically), + shape = CircleShape + ) { + val state = rememberScrollState(0) + Row(modifier = Modifier.horizontalScroll(state)) { + Row { + for (type in FilterType.values()) { + FilterButton(content, type) + } + } + } + } + } + } +} + +@Composable +fun FilterButton( + content: ContentState, + type: FilterType, + modifier: Modifier = Modifier.size(38.dp) +) { + val filterButtonHover = remember { mutableStateOf(false) } + Box( + modifier = Modifier.background(color = Transparent).clip(CircleShape) + ) { + Tooltip("$type") { + Clickable( + modifier = Modifier.hover( + onEnter = { + filterButtonHover.value = true + false + }, + onExit = { + filterButtonHover.value = false + false + }) + .background(color = if (filterButtonHover.value) TranslucentBlack else Transparent), + onClick = { content.toggleFilter(type)} + ) { + Image( + getFilterImage(type = type, content = content), + contentDescription = null, + modifier + ) + } + } + } + Spacer(Modifier.width(20.dp)) +} + +@Composable +fun getFilterImage(type: FilterType, content: ContentState): Painter { + return when (type) { + FilterType.GrayScale -> if (content.isFilterEnabled(type)) icFilterGrayscaleOn() else icFilterGrayscaleOff() + FilterType.Pixel -> if (content.isFilterEnabled(type)) icFilterPixelOn() else icFilterPixelOff() + FilterType.Blur -> if (content.isFilterEnabled(type)) icFilterBlurOn() else icFilterBlurOff() + } +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun Image(content: ContentState) { + val onUpdate = remember { { content.updateMainImage() } } + Surface( + color = DarkGray, + modifier = Modifier.fillMaxSize() + ) { + Draggable( + onUpdate = onUpdate, + dragHandler = content.drag, + modifier = Modifier.fillMaxSize() + ) { + Zoomable( + onUpdate = onUpdate, + scaleHandler = content.scale, + modifier = Modifier.fillMaxSize() + .onPreviewKeyEvent { + if (it.type == KeyEventType.KeyUp) { + when (it.key) { + Key.DirectionLeft -> { + content.swipePrevious() + } + Key.DirectionRight -> { + content.swipeNext() + } + } + } + false + } + ) { + Image( + bitmap = content.getSelectedImage(), + contentDescription = null, + contentScale = ContentScale.Fit + ) + } + } + } +} diff --git a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/MainScreen.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/MainScreen.kt index cf3401c7ae..85b8d448e5 100755 --- a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/MainScreen.kt +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/MainScreen.kt @@ -35,8 +35,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import example.imageviewer.ResString @@ -59,51 +60,27 @@ import example.imageviewer.style.icRefresh import example.imageviewer.utils.toByteArray @Composable -fun setMainScreen(content: ContentState) { - if (content.isContentReady()) { - Column { - setTopContent(content) - setScrollableArea(content) - } - } else { - setLoadingScreen(content) +fun MainScreen(content: ContentState) { + Column { + TopContent(content) + ScrollableArea(content) } -} - -@Composable -private fun setLoadingScreen(content: ContentState) { - Box { - Column { - setTopContent(content) - } - Box(modifier = Modifier.align(Alignment.Center)) { - Surface(color = DarkGray, elevation = 4.dp, shape = CircleShape) { - CircularProgressIndicator( - modifier = Modifier.size(50.dp).padding(3.dp, 3.dp, 4.dp, 4.dp), - color = DarkGreen - ) - } - } - Text( - text = ResString.loading, - modifier = Modifier.align(Alignment.Center).offset(0.dp, 70.dp), - style = MaterialTheme.typography.body1, - color = Foreground - ) + if (!content.isContentReady()) { + LoadingScreen(ResString.loading) } } @Composable -fun setTopContent(content: ContentState) { - setTitleBar(text = ResString.appName, content = content) - setPreviewImageUI(content) - setSpacer(h = 10) - setDivider() - setSpacer(h = 5) +fun TopContent(content: ContentState) { + TitleBar(text = ResString.appName, content = content) + PreviewImage(content) + Spacer(modifier = Modifier.height(10.dp)) + Divider() + Spacer(modifier = Modifier.height(5.dp)) } @Composable -fun setTitleBar(text: String, content: ContentState) { +fun TitleBar(text: String, content: ContentState) { val refreshButtonHover = remember { mutableStateOf(false) } TopAppBar( backgroundColor = DarkGreen, @@ -119,29 +96,31 @@ fun setTitleBar(text: String, content: ContentState) { modifier = Modifier.padding(end = 20.dp).align(Alignment.CenterVertically), shape = CircleShape ) { - Clickable( - modifier = Modifier.hover( - onEnter = { - refreshButtonHover.value = true - false - }, - onExit = { - refreshButtonHover.value = false - false - } - ) - .background(color = if (refreshButtonHover.value) TranslucentBlack else Transparent), - onClick = { - if (content.isContentReady()) { - content.refresh() + Tooltip(ResString.refresh) { + Clickable( + modifier = Modifier.hover( + onEnter = { + refreshButtonHover.value = true + false + }, + onExit = { + refreshButtonHover.value = false + false + } + ) + .background(color = if (refreshButtonHover.value) TranslucentBlack else Transparent), + onClick = { + if (content.isContentReady()) { + content.refresh() + } } + ) { + Image( + icRefresh(), + contentDescription = null, + modifier = Modifier.size(35.dp) + ) } - ) { - Image( - icRefresh(), - contentDescription = null, - modifier = Modifier.size(35.dp) - ) } } } @@ -149,7 +128,7 @@ fun setTitleBar(text: String, content: ContentState) { } @Composable -fun setPreviewImageUI(content: ContentState) { +fun PreviewImage(content: ContentState) { Clickable( modifier = Modifier.background(color = DarkGray), onClick = { @@ -165,9 +144,8 @@ fun setPreviewImageUI(content: ContentState) { Image( if (content.isMainImageEmpty()) icEmpty() - else org.jetbrains.skija.Image.makeFromEncoded( - toByteArray(content.getSelectedImage()) - ).asImageBitmap(), + else + BitmapPainter(content.getSelectedImage()), contentDescription = null, modifier = Modifier .fillMaxWidth().padding(start = 1.dp, top = 1.dp, end = 1.dp, bottom = 5.dp), @@ -178,7 +156,7 @@ fun setPreviewImageUI(content: ContentState) { } @Composable -fun setMiniatureUI( +fun Miniature( picture: Picture, content: ContentState ) { @@ -265,7 +243,7 @@ fun setMiniatureUI( } @Composable -fun setScrollableArea(content: ContentState) { +fun ScrollableArea(content: ContentState) { Box( modifier = Modifier.fillMaxSize() .padding(end = 8.dp) @@ -275,7 +253,7 @@ fun setScrollableArea(content: ContentState) { var index = 1 Column { for (picture in content.getMiniatures()) { - setMiniatureUI( + Miniature( picture = picture, content = content ) @@ -293,16 +271,9 @@ fun setScrollableArea(content: ContentState) { } @Composable -fun setDivider() { - +fun Divider() { Divider( color = LightGray, modifier = Modifier.padding(start = 10.dp, end = 10.dp) ) } - -@Composable -fun setSpacer(h: Int) { - - Spacer(modifier = Modifier.height(h.dp)) -} diff --git a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Tooltip.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Tooltip.kt new file mode 100644 index 0000000000..9d6819058f --- /dev/null +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Tooltip.kt @@ -0,0 +1,35 @@ +package example.imageviewer.view + +import androidx.compose.foundation.BoxWithTooltip +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun Tooltip( + text: String = "Tooltip", + content: @Composable () -> Unit +) { + BoxWithTooltip( + tooltip = { + Surface( + color = Color(210, 210, 210), + shape = RoundedCornerShape(4.dp) + ) { + Text( + text = text, + modifier = Modifier.padding(10.dp), + style = MaterialTheme.typography.caption + ) + } + } + ) { + content() + } +} \ No newline at end of file diff --git a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Zoomable.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Zoomable.kt index f255b8b0c8..e9f6321be0 100644 --- a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Zoomable.kt +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Zoomable.kt @@ -1,44 +1,58 @@ package example.imageviewer.view +import androidx.compose.foundation.focusable import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.material.Surface import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusModifier import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.input.key.Key -import androidx.compose.ui.input.key.shortcuts +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.type +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.ExperimentalComposeUiApi import example.imageviewer.style.Transparent -import androidx.compose.runtime.DisposableEffect +@OptIn(ExperimentalComposeUiApi::class) @Composable fun Zoomable( - onScale: ScaleHandler, + scaleHandler: ScaleHandler, modifier: Modifier = Modifier, + onUpdate: (() -> Unit)? = null, children: @Composable() () -> Unit ) { val focusRequester = FocusRequester() Surface( color = Transparent, - modifier = modifier.shortcuts { - on(Key.I) { - onScale.onScale(1.2f) - } - on(Key.O) { - onScale.onScale(0.8f) - } - on(Key.R) { - onScale.resetFactor() + modifier = modifier.onPreviewKeyEvent { + if (it.type == KeyEventType.KeyUp) { + when (it.key) { + Key.I -> { + scaleHandler.onScale(1.2f) + onUpdate?.invoke() + } + Key.O -> { + scaleHandler.onScale(0.8f) + onUpdate?.invoke() + } + Key.R -> { + scaleHandler.reset() + onUpdate?.invoke() + } + } } + false } .focusRequester(focusRequester) - .focusModifier() + .focusable() .pointerInput(Unit) { - detectTapGestures(onDoubleTap = { onScale.resetFactor() }) { + detectTapGestures(onDoubleTap = { scaleHandler.reset() }) { focusRequester.requestFocus() } } diff --git a/examples/imageviewer/desktop/build.gradle.kts b/examples/imageviewer/desktop/build.gradle.kts index 46bccd234b..7365a9e788 100755 --- a/examples/imageviewer/desktop/build.gradle.kts +++ b/examples/imageviewer/desktop/build.gradle.kts @@ -28,8 +28,6 @@ compose.desktop { targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) packageName = "ImageViewer" packageVersion = "1.0.0" - - modules("jdk.crypto.ec") val iconsRoot = project.file("../common/src/desktopMain/resources/images") macOS { diff --git a/examples/imageviewer/desktop/src/jvmMain/kotlin/example/imageviewer/Main.kt b/examples/imageviewer/desktop/src/jvmMain/kotlin/example/imageviewer/Main.kt index a955947865..c17682c405 100644 --- a/examples/imageviewer/desktop/src/jvmMain/kotlin/example/imageviewer/Main.kt +++ b/examples/imageviewer/desktop/src/jvmMain/kotlin/example/imageviewer/Main.kt @@ -1,47 +1,58 @@ package example.imageviewer -import androidx.compose.desktop.DesktopTheme import androidx.compose.material.MaterialTheme import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.WindowState +import androidx.compose.ui.window.WindowPosition +import androidx.compose.ui.window.application +import androidx.compose.ui.window.rememberWindowState import example.imageviewer.model.ContentState import example.imageviewer.style.icAppRounded -import example.imageviewer.utils.Application import example.imageviewer.utils.getPreferredWindowSize -import example.imageviewer.view.BuildAppUI +import example.imageviewer.view.AppUI import example.imageviewer.view.SplashUI -fun main() = Application { +fun main() = application { + val state = rememberWindowState() val content = remember { ContentState.applyContent( + state, "https://raw.githubusercontent.com/JetBrains/compose-jb/master/artwork/imageviewerrepo/fetching.list" ) } - val icon = remember(::icAppRounded) + val icon = icAppRounded() if (content.isAppReady()) { - ComposableWindow( + Window( + onCloseRequest = ::exitApplication, title = "Image Viewer", - size = getPreferredWindowSize(800, 1000), + state = WindowState( + position = WindowPosition.Aligned(Alignment.Center), + size = getPreferredWindowSize(800, 1000) + ), icon = icon ) { MaterialTheme { - DesktopTheme { - BuildAppUI(content) - } + AppUI(content) } } } else { - ComposableWindow( + Window( + onCloseRequest = ::exitApplication, title = "Image Viewer", - size = getPreferredWindowSize(800, 300), + state = WindowState( + position = WindowPosition.Aligned(Alignment.Center), + size = getPreferredWindowSize(800, 300) + ), undecorated = true, icon = icon, ) { MaterialTheme { - DesktopTheme { - SplashUI() - } + SplashUI() } } } diff --git a/examples/imageviewer/gradle/wrapper/gradle-wrapper.properties b/examples/imageviewer/gradle/wrapper/gradle-wrapper.properties index 549d84424d..05679dc3c1 100755 --- a/examples/imageviewer/gradle/wrapper/gradle-wrapper.properties +++ b/examples/imageviewer/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/examples/intelliJPlugin/src/main/kotlin/com/jetbrains/compose/ComposeSizeAdjustmentWrapper.kt b/examples/intelliJPlugin/src/main/kotlin/com/jetbrains/compose/ComposeSizeAdjustmentWrapper.kt deleted file mode 100755 index 5fc21da40a..0000000000 --- a/examples/intelliJPlugin/src/main/kotlin/com/jetbrains/compose/ComposeSizeAdjustmentWrapper.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.jetbrains.compose - -import androidx.compose.desktop.ComposePanel -import androidx.compose.foundation.layout.Box -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.Layout -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.unit.IntSize -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import com.intellij.openapi.ui.DialogWrapper -import java.awt.Dimension -import java.awt.Window -import javax.swing.JComponent - -@Composable -fun ComposeSizeAdjustmentWrapper( - window: DialogWrapper, - panel: ComposePanel, - preferredSize: IntSize, - content: @Composable () -> Unit -) { - var packed = false - Box { - content() - Layout( - content = {}, - modifier = Modifier.onGloballyPositioned { childCoordinates -> - // adjust size of the dialog - if (!packed) { - val contentSize = childCoordinates.parentCoordinates!!.size - panel.preferredSize = Dimension( - if (contentSize.width < preferredSize.width) preferredSize.width else contentSize.width, - if (contentSize.height < preferredSize.height) preferredSize.height else contentSize.height, - ) - window.pack() - packed = true - } - }, - measurePolicy = { _, _ -> - layout(0, 0) {} - } - ) - } -} diff --git a/examples/intelliJPlugin/.gitignore b/examples/intellij-plugin/.gitignore similarity index 100% rename from examples/intelliJPlugin/.gitignore rename to examples/intellij-plugin/.gitignore diff --git a/examples/intelliJPlugin/README.md b/examples/intellij-plugin/README.md similarity index 100% rename from examples/intelliJPlugin/README.md rename to examples/intellij-plugin/README.md diff --git a/examples/intelliJPlugin/build.gradle.kts b/examples/intellij-plugin/build.gradle.kts similarity index 51% rename from examples/intelliJPlugin/build.gradle.kts rename to examples/intellij-plugin/build.gradle.kts index 26b273e4b6..65048a542c 100644 --- a/examples/intelliJPlugin/build.gradle.kts +++ b/examples/intellij-plugin/build.gradle.kts @@ -1,11 +1,12 @@ import org.jetbrains.compose.compose plugins { - id("org.jetbrains.intellij") version "0.6.5" + id("org.jetbrains.intellij") version "1.1.4" java - kotlin("jvm") version "1.5.10" + kotlin("jvm") version "1.5.21" // __LATEST_COMPOSE_RELEASE_VERSION__ - id("org.jetbrains.compose") version "0.4.0" + id("org.jetbrains.compose") version "1.0.0-alpha1" + id("idea") } group = "org.example" @@ -13,24 +14,18 @@ version = "1.0-SNAPSHOT" repositories { mavenCentral() + google() maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") } } dependencies { - implementation(kotlin("stdlib")) - implementation("org.jetbrains.compose.material:material:") implementation(compose.desktop.currentOs) - testCompile("junit", "junit", "4.12") + testImplementation("junit", "junit", "4.12") } // See https://github.com/JetBrains/gradle-intellij-plugin/ intellij { - version = "2021.1" -} -tasks.getByName("patchPluginXml") { - changeNotes(""" - Add change notes here.
- most HTML tags may be used""") + version.set("2021.2") } tasks.withType { diff --git a/examples/intelliJPlugin/gradle.properties b/examples/intellij-plugin/gradle.properties similarity index 100% rename from examples/intelliJPlugin/gradle.properties rename to examples/intellij-plugin/gradle.properties diff --git a/examples/intelliJPlugin/gradle/wrapper/gradle-wrapper.jar b/examples/intellij-plugin/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from examples/intelliJPlugin/gradle/wrapper/gradle-wrapper.jar rename to examples/intellij-plugin/gradle/wrapper/gradle-wrapper.jar diff --git a/examples/web-getting-started/gradle/wrapper/gradle-wrapper.properties b/examples/intellij-plugin/gradle/wrapper/gradle-wrapper.properties similarity index 92% rename from examples/web-getting-started/gradle/wrapper/gradle-wrapper.properties rename to examples/intellij-plugin/gradle/wrapper/gradle-wrapper.properties index 29e4134576..05679dc3c1 100644 --- a/examples/web-getting-started/gradle/wrapper/gradle-wrapper.properties +++ b/examples/intellij-plugin/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/examples/intelliJPlugin/gradlew b/examples/intellij-plugin/gradlew similarity index 100% rename from examples/intelliJPlugin/gradlew rename to examples/intellij-plugin/gradlew diff --git a/examples/intelliJPlugin/gradlew.bat b/examples/intellij-plugin/gradlew.bat similarity index 100% rename from examples/intelliJPlugin/gradlew.bat rename to examples/intellij-plugin/gradlew.bat diff --git a/examples/intelliJPlugin/screenshots/screenshot.png b/examples/intellij-plugin/screenshots/screenshot.png similarity index 100% rename from examples/intelliJPlugin/screenshots/screenshot.png rename to examples/intellij-plugin/screenshots/screenshot.png diff --git a/examples/intelliJPlugin/screenshots/toolsshow.png b/examples/intellij-plugin/screenshots/toolsshow.png similarity index 100% rename from examples/intelliJPlugin/screenshots/toolsshow.png rename to examples/intellij-plugin/screenshots/toolsshow.png diff --git a/examples/intelliJPlugin/settings.gradle.kts b/examples/intellij-plugin/settings.gradle.kts similarity index 100% rename from examples/intelliJPlugin/settings.gradle.kts rename to examples/intellij-plugin/settings.gradle.kts diff --git a/examples/intelliJPlugin/src/main/kotlin/com/jetbrains/compose/ComposeDemoAction.kt b/examples/intellij-plugin/src/main/kotlin/com/jetbrains/compose/ComposeDemoAction.kt similarity index 54% rename from examples/intelliJPlugin/src/main/kotlin/com/jetbrains/compose/ComposeDemoAction.kt rename to examples/intellij-plugin/src/main/kotlin/com/jetbrains/compose/ComposeDemoAction.kt index 8b32298ff9..581c8cbe91 100755 --- a/examples/intelliJPlugin/src/main/kotlin/com/jetbrains/compose/ComposeDemoAction.kt +++ b/examples/intellij-plugin/src/main/kotlin/com/jetbrains/compose/ComposeDemoAction.kt @@ -1,6 +1,5 @@ package com.jetbrains.compose -import androidx.compose.desktop.ComposePanel import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -8,12 +7,14 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.Surface import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.awt.ComposePanel +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.MeasurePolicy +import androidx.compose.ui.layout.onGloballyPositioned import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.project.Project import com.intellij.openapi.ui.DialogWrapper -import com.intellij.ui.layout.panel import com.jetbrains.compose.theme.WidgetTheme import com.jetbrains.compose.widgets.Buttons import com.jetbrains.compose.widgets.LazyScrollable @@ -22,6 +23,7 @@ import com.jetbrains.compose.widgets.TextInputs import com.jetbrains.compose.widgets.Toggles import java.awt.Dimension import javax.swing.JComponent +import javax.swing.SwingUtilities /** @@ -39,31 +41,24 @@ class ComposeDemoAction : DumbAwareAction() { } override fun createCenterPanel(): JComponent { - val dialog = this return ComposePanel().apply { - preferredSize = Dimension(800, 600) + setBounds(0, 0, 800, 600) setContent { - ComposeSizeAdjustmentWrapper( - window = dialog, - panel = this, - preferredSize = IntSize(800, 600) - ) { - WidgetTheme(darkTheme = true) { - Surface(modifier = Modifier.fillMaxSize()) { - Row { - Column( - modifier = Modifier.fillMaxHeight().weight(1f) - ) { - Buttons() - Loaders() - TextInputs() - Toggles() - } - Box( - modifier = Modifier.fillMaxHeight().weight(1f) - ) { - LazyScrollable() - } + WidgetTheme(darkTheme = true) { + Surface(modifier = Modifier.fillMaxSize()) { + Row { + Column( + modifier = Modifier.fillMaxHeight().weight(1f) + ) { + Buttons() + Loaders() + TextInputs() + Toggles() + } + Box( + modifier = Modifier.fillMaxHeight().weight(1f) + ) { + LazyScrollable() } } } diff --git a/examples/intelliJPlugin/src/main/kotlin/com/jetbrains/compose/theme/Color.kt b/examples/intellij-plugin/src/main/kotlin/com/jetbrains/compose/theme/Color.kt similarity index 100% rename from examples/intelliJPlugin/src/main/kotlin/com/jetbrains/compose/theme/Color.kt rename to examples/intellij-plugin/src/main/kotlin/com/jetbrains/compose/theme/Color.kt diff --git a/examples/intelliJPlugin/src/main/kotlin/com/jetbrains/compose/theme/Shape.kt b/examples/intellij-plugin/src/main/kotlin/com/jetbrains/compose/theme/Shape.kt similarity index 100% rename from examples/intelliJPlugin/src/main/kotlin/com/jetbrains/compose/theme/Shape.kt rename to examples/intellij-plugin/src/main/kotlin/com/jetbrains/compose/theme/Shape.kt diff --git a/examples/intelliJPlugin/src/main/kotlin/com/jetbrains/compose/theme/Theme.kt b/examples/intellij-plugin/src/main/kotlin/com/jetbrains/compose/theme/Theme.kt similarity index 100% rename from examples/intelliJPlugin/src/main/kotlin/com/jetbrains/compose/theme/Theme.kt rename to examples/intellij-plugin/src/main/kotlin/com/jetbrains/compose/theme/Theme.kt diff --git a/examples/intelliJPlugin/src/main/kotlin/com/jetbrains/compose/theme/Type.kt b/examples/intellij-plugin/src/main/kotlin/com/jetbrains/compose/theme/Type.kt similarity index 100% rename from examples/intelliJPlugin/src/main/kotlin/com/jetbrains/compose/theme/Type.kt rename to examples/intellij-plugin/src/main/kotlin/com/jetbrains/compose/theme/Type.kt diff --git a/examples/intelliJPlugin/src/main/kotlin/com/jetbrains/compose/theme/intellij/SwingColor.kt b/examples/intellij-plugin/src/main/kotlin/com/jetbrains/compose/theme/intellij/SwingColor.kt similarity index 100% rename from examples/intelliJPlugin/src/main/kotlin/com/jetbrains/compose/theme/intellij/SwingColor.kt rename to examples/intellij-plugin/src/main/kotlin/com/jetbrains/compose/theme/intellij/SwingColor.kt diff --git a/examples/intelliJPlugin/src/main/kotlin/com/jetbrains/compose/theme/intellij/ThemeChangeListener.kt b/examples/intellij-plugin/src/main/kotlin/com/jetbrains/compose/theme/intellij/ThemeChangeListener.kt similarity index 100% rename from examples/intelliJPlugin/src/main/kotlin/com/jetbrains/compose/theme/intellij/ThemeChangeListener.kt rename to examples/intellij-plugin/src/main/kotlin/com/jetbrains/compose/theme/intellij/ThemeChangeListener.kt diff --git a/examples/intelliJPlugin/src/main/kotlin/com/jetbrains/compose/widgets/Buttons.kt b/examples/intellij-plugin/src/main/kotlin/com/jetbrains/compose/widgets/Buttons.kt similarity index 100% rename from examples/intelliJPlugin/src/main/kotlin/com/jetbrains/compose/widgets/Buttons.kt rename to examples/intellij-plugin/src/main/kotlin/com/jetbrains/compose/widgets/Buttons.kt diff --git a/examples/intelliJPlugin/src/main/kotlin/com/jetbrains/compose/widgets/LazyScrollable.kt b/examples/intellij-plugin/src/main/kotlin/com/jetbrains/compose/widgets/LazyScrollable.kt similarity index 91% rename from examples/intelliJPlugin/src/main/kotlin/com/jetbrains/compose/widgets/LazyScrollable.kt rename to examples/intellij-plugin/src/main/kotlin/com/jetbrains/compose/widgets/LazyScrollable.kt index 320b86ef49..e5bba421a0 100755 --- a/examples/intelliJPlugin/src/main/kotlin/com/jetbrains/compose/widgets/LazyScrollable.kt +++ b/examples/intellij-plugin/src/main/kotlin/com/jetbrains/compose/widgets/LazyScrollable.kt @@ -24,7 +24,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -@OptIn(ExperimentalFoundationApi::class) @Composable fun LazyScrollable() { MaterialTheme { @@ -46,9 +45,7 @@ fun LazyScrollable() { VerticalScrollbar( modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight(), adapter = rememberScrollbarAdapter( - scrollState = state, - itemCount = itemCount, - averageItemSize = 37.dp // TextBox height + Spacer height + scrollState = state ) ) } diff --git a/examples/intelliJPlugin/src/main/kotlin/com/jetbrains/compose/widgets/Loaders.kt b/examples/intellij-plugin/src/main/kotlin/com/jetbrains/compose/widgets/Loaders.kt similarity index 100% rename from examples/intelliJPlugin/src/main/kotlin/com/jetbrains/compose/widgets/Loaders.kt rename to examples/intellij-plugin/src/main/kotlin/com/jetbrains/compose/widgets/Loaders.kt diff --git a/examples/intelliJPlugin/src/main/kotlin/com/jetbrains/compose/widgets/TextInputs.kt b/examples/intellij-plugin/src/main/kotlin/com/jetbrains/compose/widgets/TextInputs.kt similarity index 100% rename from examples/intelliJPlugin/src/main/kotlin/com/jetbrains/compose/widgets/TextInputs.kt rename to examples/intellij-plugin/src/main/kotlin/com/jetbrains/compose/widgets/TextInputs.kt diff --git a/examples/intelliJPlugin/src/main/kotlin/com/jetbrains/compose/widgets/Toggles.kt b/examples/intellij-plugin/src/main/kotlin/com/jetbrains/compose/widgets/Toggles.kt similarity index 100% rename from examples/intelliJPlugin/src/main/kotlin/com/jetbrains/compose/widgets/Toggles.kt rename to examples/intellij-plugin/src/main/kotlin/com/jetbrains/compose/widgets/Toggles.kt diff --git a/examples/intelliJPlugin/src/main/resources/META-INF/plugin.xml b/examples/intellij-plugin/src/main/resources/META-INF/plugin.xml similarity index 100% rename from examples/intelliJPlugin/src/main/resources/META-INF/plugin.xml rename to examples/intellij-plugin/src/main/resources/META-INF/plugin.xml diff --git a/examples/issues/android/build.gradle.kts b/examples/issues/android/build.gradle.kts index 1ae8733563..009c84eabe 100644 --- a/examples/issues/android/build.gradle.kts +++ b/examples/issues/android/build.gradle.kts @@ -5,11 +5,11 @@ plugins { } android { - compileSdkVersion(30) + compileSdk = 30 defaultConfig { - minSdkVersion(26) - targetSdkVersion(30) + minSdk = 26 + targetSdk = 30 versionCode = 1 versionName = "1.0" } @@ -22,5 +22,5 @@ android { dependencies { implementation(project(":common")) - implementation("androidx.activity:activity-compose:1.3.0-alpha02") + implementation("androidx.activity:activity-compose:1.3.0") } diff --git a/examples/issues/android/src/main/java/androidx/ui/examples/jetissues/MainActivity.kt b/examples/issues/android/src/main/java/androidx/ui/examples/jetissues/MainActivity.kt index 208bcefb0c..1edf1af2d4 100644 --- a/examples/issues/android/src/main/java/androidx/ui/examples/jetissues/MainActivity.kt +++ b/examples/issues/android/src/main/java/androidx/ui/examples/jetissues/MainActivity.kt @@ -8,8 +8,9 @@ import androidx.ui.examples.jetissues.view.JetIssuesView import androidx.ui.examples.jetissues.view.Repository import androidx.ui.examples.jetissues.data.IssuesRepositoryImpl import androidx.ui.examples.jetissues.data.defaultAuth +import androidx.ui.examples.jetissues.data.defaultRepo -val repo = IssuesRepositoryImpl("ktorio", "ktor", System.getenv("GITHUB_TOKEN") ?: defaultAuth) +val repo = IssuesRepositoryImpl(defaultRepo.first, defaultRepo.second, System.getenv("GITHUB_TOKEN") ?: defaultAuth) class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { diff --git a/examples/issues/build.gradle.kts b/examples/issues/build.gradle.kts index 865e792b8d..fe1e4f9855 100644 --- a/examples/issues/build.gradle.kts +++ b/examples/issues/build.gradle.kts @@ -8,10 +8,10 @@ buildscript { dependencies { // __LATEST_COMPOSE_RELEASE_VERSION__ - classpath("org.jetbrains.compose:compose-gradle-plugin:0.4.0") - classpath("com.android.tools.build:gradle:4.0.1") + classpath("org.jetbrains.compose:compose-gradle-plugin:1.0.0-alpha1") + classpath("com.android.tools.build:gradle:4.1.0") // __KOTLIN_COMPOSE_VERSION__ - classpath(kotlin("gradle-plugin", version = "1.5.10")) + classpath(kotlin("gradle-plugin", version = "1.5.21")) } } diff --git a/examples/issues/common/build.gradle.kts b/examples/issues/common/build.gradle.kts index 75f08c4629..ac5ce5e548 100644 --- a/examples/issues/common/build.gradle.kts +++ b/examples/issues/common/build.gradle.kts @@ -24,7 +24,7 @@ kotlin { named("androidMain") { kotlin.srcDirs("src/jvmAndAndroidMain/kotlin") dependencies { - api("androidx.appcompat:appcompat:1.3.0-beta01") + api("androidx.appcompat:appcompat:1.3.1") api("androidx.core:core-ktx:1.3.1") } } @@ -40,13 +40,11 @@ apollo { } android { - compileSdkVersion(30) + compileSdk = 30 defaultConfig { - minSdkVersion(26) - targetSdkVersion(30) - versionCode = 1 - versionName = "1.0" + minSdk = 26 + targetSdk = 30 } compileOptions { @@ -57,9 +55,6 @@ android { sourceSets { named("main") { manifest.srcFile("src/androidMain/AndroidManifest.xml") - - -// res.srcDirs("src/androidMain/res") } } } diff --git a/examples/issues/common/src/jvmAndAndroidMain/kotlin/androidx/ui/examples/jetissues/data/IssuesRepository.kt b/examples/issues/common/src/jvmAndAndroidMain/kotlin/androidx/ui/examples/jetissues/data/IssuesRepository.kt index 78d5e97653..577ea84942 100644 --- a/examples/issues/common/src/jvmAndAndroidMain/kotlin/androidx/ui/examples/jetissues/data/IssuesRepository.kt +++ b/examples/issues/common/src/jvmAndAndroidMain/kotlin/androidx/ui/examples/jetissues/data/IssuesRepository.kt @@ -36,6 +36,7 @@ import java.util.* private fun decode(input: String) = input.toCharArray().map { it + 1 }.joinToString("") val defaultAuth = decode("/`4/81b6db605e8d6``bdc7ecba8d2/a7/370`20") +val defaultRepo = Pair("JetBrains", "compose-jb") sealed class Result { data class Success(val data: T) : Result() diff --git a/examples/issues/desktop/src/jvmMain/kotlin/androidx/ui/examples/jetissues/Main.kt b/examples/issues/desktop/src/jvmMain/kotlin/androidx/ui/examples/jetissues/Main.kt index 8ef62eac5a..b584add0f5 100644 --- a/examples/issues/desktop/src/jvmMain/kotlin/androidx/ui/examples/jetissues/Main.kt +++ b/examples/issues/desktop/src/jvmMain/kotlin/androidx/ui/examples/jetissues/Main.kt @@ -1,20 +1,27 @@ package androidx.ui.examples.jetissues -import androidx.compose.desktop.Window import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.WindowSize +import androidx.compose.ui.window.WindowState +import androidx.compose.ui.window.application import androidx.ui.examples.jetissues.view.JetIssuesView import androidx.ui.examples.jetissues.view.Repository import androidx.ui.examples.jetissues.data.IssuesRepositoryImpl import androidx.ui.examples.jetissues.data.defaultAuth +import androidx.ui.examples.jetissues.data.defaultRepo -val repo = IssuesRepositoryImpl("ktorio", "ktor", System.getenv("GITHUB_TOKEN") ?: defaultAuth) +val repo = IssuesRepositoryImpl(defaultRepo.first, defaultRepo.second, System.getenv("GITHUB_TOKEN") ?: defaultAuth) -fun main() = Window( - title = "JetIssues", - size = IntSize(1440, 768) -) { - CompositionLocalProvider(Repository provides repo) { - JetIssuesView() +fun main() = application { + Window( + onCloseRequest = ::exitApplication, + title = "JetIssues", + state = WindowState(size = WindowSize(1440.dp, 768.dp)) + ) { + CompositionLocalProvider(Repository provides repo) { + JetIssuesView() + } } -} +} \ No newline at end of file diff --git a/examples/issues/gradle/wrapper/gradle-wrapper.properties b/examples/issues/gradle/wrapper/gradle-wrapper.properties index 7665b0fa93..05679dc3c1 100644 --- a/examples/issues/gradle/wrapper/gradle-wrapper.properties +++ b/examples/issues/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/examples/notepad/build.gradle.kts b/examples/notepad/build.gradle.kts index 36a2a3adbf..41fcc4d4d6 100644 --- a/examples/notepad/build.gradle.kts +++ b/examples/notepad/build.gradle.kts @@ -3,12 +3,13 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat plugins { // __KOTLIN_COMPOSE_VERSION__ - kotlin("jvm") version "1.5.10" + kotlin("jvm") version "1.5.21" // __LATEST_COMPOSE_RELEASE_VERSION__ - id("org.jetbrains.compose") version (System.getenv("COMPOSE_TEMPLATE_COMPOSE_VERSION") ?: "0.4.0") + id("org.jetbrains.compose") version ("1.0.0-alpha1") } repositories { + google() mavenCentral() maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") } @@ -21,7 +22,6 @@ dependencies { compose.desktop { application { mainClass = "MainKt" - jvmArgs("-Dskiko.rendering.laf.global=true") nativeDistributions { targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) diff --git a/examples/notepad/gradle/wrapper/gradle-wrapper.properties b/examples/notepad/gradle/wrapper/gradle-wrapper.properties index 7665b0fa93..05679dc3c1 100644 --- a/examples/notepad/gradle/wrapper/gradle-wrapper.properties +++ b/examples/notepad/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/examples/notepad/src/main/kotlin/NotepadApplication.kt b/examples/notepad/src/main/kotlin/NotepadApplication.kt index e30c663c1f..270e036609 100644 --- a/examples/notepad/src/main/kotlin/NotepadApplication.kt +++ b/examples/notepad/src/main/kotlin/NotepadApplication.kt @@ -1,4 +1,5 @@ import androidx.compose.runtime.Composable +import androidx.compose.runtime.key import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.window.MenuScope @@ -14,7 +15,9 @@ fun NotepadApplication(state: NotepadApplicationState) { } for (window in state.windows) { - NotepadWindow(window) + key(window) { + NotepadWindow(window) + } } } @@ -22,7 +25,7 @@ fun NotepadApplication(state: NotepadApplicationState) { @Composable private fun ApplicationTray(state: NotepadApplicationState) { Tray( - LocalAppResources.current.icon ?: return, + LocalAppResources.current.icon, state = state.tray, hint = "Notepad", menu = { ApplicationMenu(state) } diff --git a/examples/notepad/src/main/kotlin/common/AppResources.kt b/examples/notepad/src/main/kotlin/common/AppResources.kt index 21a06d3517..840c6cc65d 100644 --- a/examples/notepad/src/main/kotlin/common/AppResources.kt +++ b/examples/notepad/src/main/kotlin/common/AppResources.kt @@ -3,15 +3,14 @@ package common import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Description import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.graphics.Color -import util.toAwtImage -import java.awt.image.BufferedImage +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.RenderVectorGroup +import androidx.compose.ui.graphics.vector.VectorPainter +import androidx.compose.ui.graphics.vector.rememberVectorPainter val LocalAppResources = staticCompositionLocalOf { error("LocalNotepadResources isn't provided") @@ -19,20 +18,22 @@ val LocalAppResources = staticCompositionLocalOf { @Composable fun rememberAppResources(): AppResources { - val resources = remember { AppResources() } - - LaunchedEffect(Unit) { - resources.init() - } - - return resources + val icon = rememberVectorPainter(Icons.Default.Description, Color(0xFF2CA4E1)) + return remember { AppResources(icon) } } -class AppResources { - var icon: BufferedImage? by mutableStateOf(null) - private set +class AppResources(val icon: VectorPainter) - suspend fun init() { - icon = Icons.Default.Description.toAwtImage(Color(0xFF2CA4E1)) - } -} \ No newline at end of file +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun rememberVectorPainter(image: ImageVector, tintColor: Color) = + rememberVectorPainter( + defaultWidth = image.defaultWidth, + defaultHeight = image.defaultHeight, + viewportWidth = image.viewportWidth, + viewportHeight = image.viewportHeight, + name = image.name, + tintColor = tintColor, + tintBlendMode = image.tintBlendMode, + content = { _, _ -> RenderVectorGroup(group = image.root) } + ) diff --git a/examples/notepad/src/main/kotlin/util/Dialogs.kt b/examples/notepad/src/main/kotlin/util/Dialogs.kt index a25f5c78cc..281d93e1e9 100644 --- a/examples/notepad/src/main/kotlin/util/Dialogs.kt +++ b/examples/notepad/src/main/kotlin/util/Dialogs.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.window.AwtWindow +import androidx.compose.ui.window.FrameWindowScope import androidx.compose.ui.window.WindowScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -16,7 +17,7 @@ import javax.swing.JOptionPane @OptIn(ExperimentalComposeUiApi::class) @Composable -fun WindowScope.FileDialog( +fun FrameWindowScope.FileDialog( title: String, isLoad: Boolean, onResult: (result: Path?) -> Unit diff --git a/examples/notepad/src/main/kotlin/window/NotepadWindow.kt b/examples/notepad/src/main/kotlin/window/NotepadWindow.kt index 420739117c..d04eb4828e 100644 --- a/examples/notepad/src/main/kotlin/window/NotepadWindow.kt +++ b/examples/notepad/src/main/kotlin/window/NotepadWindow.kt @@ -7,10 +7,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.window.MenuBar -import androidx.compose.ui.window.Notification -import androidx.compose.ui.window.Window -import androidx.compose.ui.window.WindowScope +import androidx.compose.ui.window.* import common.LocalAppResources import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch @@ -27,7 +24,7 @@ fun NotepadWindow(state: NotepadWindowState) { Window( state = state.window, title = titleOf(state), - icon = LocalAppResources.current.icon ?: return, + icon = LocalAppResources.current.icon, onCloseRequest = { exit() } ) { LaunchedEffect(Unit) { state.run() } @@ -97,7 +94,7 @@ private fun WindowNotifications(state: NotepadWindowState) { } @Composable -private fun WindowScope.WindowMenuBar(state: NotepadWindowState) = MenuBar { +private fun FrameWindowScope.WindowMenuBar(state: NotepadWindowState) = MenuBar { val scope = rememberCoroutineScope() fun save() = scope.launch { state.save() } @@ -118,7 +115,7 @@ private fun WindowScope.WindowMenuBar(state: NotepadWindowState) = MenuBar { onClick = state.settings::toggleTray ) Item( - if (state.window.isFullscreen) "Exit fullscreen" else "Enter fullscreen", + if (state.window.placement == WindowPlacement.Fullscreen) "Exit fullscreen" else "Enter fullscreen", onClick = state::toggleFullscreen ) } diff --git a/examples/notepad/src/main/kotlin/window/NotepadWindowState.kt b/examples/notepad/src/main/kotlin/window/NotepadWindowState.kt index 4c2ecfc3f4..8afee9ad37 100644 --- a/examples/notepad/src/main/kotlin/window/NotepadWindowState.kt +++ b/examples/notepad/src/main/kotlin/window/NotepadWindowState.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.window.Notification +import androidx.compose.ui.window.WindowPlacement import androidx.compose.ui.window.WindowState import common.Settings import kotlinx.coroutines.CompletableDeferred @@ -55,7 +56,11 @@ class NotepadWindowState( private set fun toggleFullscreen() { - window.isFullscreen = !window.isFullscreen + window.placement = if (window.placement == WindowPlacement.Fullscreen) { + WindowPlacement.Floating + } else { + WindowPlacement.Fullscreen + } } suspend fun run() { diff --git a/examples/todoapp/android/src/main/java/example/todo/android/MainActivity.kt b/examples/todoapp/android/src/main/java/example/todo/android/MainActivity.kt index 3c06a1fc99..063c27f4d4 100755 --- a/examples/todoapp/android/src/main/java/example/todo/android/MainActivity.kt +++ b/examples/todoapp/android/src/main/java/example/todo/android/MainActivity.kt @@ -6,7 +6,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import com.arkivanov.decompose.ComponentContext -import com.arkivanov.decompose.extensions.compose.jetbrains.rememberRootComponent +import com.arkivanov.decompose.defaultComponentContext import com.arkivanov.mvikotlin.logging.store.LoggingStoreFactory import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory import com.arkivanov.mvikotlin.timetravel.store.TimeTravelStoreFactory @@ -20,10 +20,12 @@ class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val root = todoRoot(defaultComponentContext()) + setContent { ComposeAppTheme { Surface(color = MaterialTheme.colors.background) { - TodoRootContent(rememberRootComponent(::todoRoot)) + TodoRootContent(root) } } } @@ -32,7 +34,7 @@ class MainActivity : AppCompatActivity() { private fun todoRoot(componentContext: ComponentContext): TodoRoot = TodoRootComponent( componentContext = componentContext, - storeFactory = LoggingStoreFactory(TimeTravelStoreFactory(DefaultStoreFactory)), + storeFactory = LoggingStoreFactory(TimeTravelStoreFactory(DefaultStoreFactory())), database = DefaultTodoSharedDatabase(TodoDatabaseDriver(context = this)) ) } diff --git a/examples/todoapp/buildSrc/buildSrc/src/main/kotlin/Deps.kt b/examples/todoapp/buildSrc/buildSrc/src/main/kotlin/Deps.kt index 39e1dcfb93..e96e50807f 100644 --- a/examples/todoapp/buildSrc/buildSrc/src/main/kotlin/Deps.kt +++ b/examples/todoapp/buildSrc/buildSrc/src/main/kotlin/Deps.kt @@ -3,7 +3,7 @@ object Deps { object JetBrains { object Kotlin { // __KOTLIN_COMPOSE_VERSION__ - private const val VERSION = "1.5.10" + private const val VERSION = "1.5.21" const val gradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$VERSION" const val testCommon = "org.jetbrains.kotlin:kotlin-test-common:$VERSION" const val testJunit = "org.jetbrains.kotlin:kotlin-test-junit:$VERSION" @@ -13,7 +13,7 @@ object Deps { object Compose { // __LATEST_COMPOSE_RELEASE_VERSION__ - private const val VERSION = "0.5.0-build225" + private const val VERSION = "1.0.0-alpha2" const val gradlePlugin = "org.jetbrains.compose:compose-gradle-plugin:$VERSION" } } @@ -21,24 +21,24 @@ object Deps { object Android { object Tools { object Build { - const val gradlePlugin = "com.android.tools.build:gradle:4.0.1" + const val gradlePlugin = "com.android.tools.build:gradle:4.1.0" } } } object AndroidX { object AppCompat { - const val appCompat = "androidx.appcompat:appcompat:1.3.0-beta01" + const val appCompat = "androidx.appcompat:appcompat:1.3.0" } object Activity { - const val activityCompose = "androidx.activity:activity-compose:1.3.0-alpha02" + const val activityCompose = "androidx.activity:activity-compose:1.3.0" } } object ArkIvanov { object MVIKotlin { - private const val VERSION = "2.0.3" + private const val VERSION = "3.0.0-alpha01" const val rx = "com.arkivanov.mvikotlin:rx:$VERSION" const val mvikotlin = "com.arkivanov.mvikotlin:mvikotlin:$VERSION" const val mvikotlinMain = "com.arkivanov.mvikotlin:mvikotlin-main:$VERSION" @@ -48,7 +48,7 @@ object Deps { } object Decompose { - private const val VERSION = "0.2.6" + private const val VERSION = "0.3.1" const val decompose = "com.arkivanov.decompose:decompose:$VERSION" const val extensionsCompose = "com.arkivanov.decompose:extensions-compose-jetbrains:$VERSION" } diff --git a/examples/todoapp/common/compose-ui/src/desktopMain/kotlin/example/todo/common/ui/TodoEditPreview.kt b/examples/todoapp/common/compose-ui/src/desktopMain/kotlin/example/todo/common/ui/TodoEditPreview.kt new file mode 100644 index 0000000000..2c917d9954 --- /dev/null +++ b/examples/todoapp/common/compose-ui/src/desktopMain/kotlin/example/todo/common/ui/TodoEditPreview.kt @@ -0,0 +1,28 @@ +package example.todo.common.ui + +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.runtime.Composable +import com.arkivanov.decompose.value.MutableValue +import com.arkivanov.decompose.value.Value +import example.todo.common.edit.TodoEdit +import example.todo.common.edit.TodoEdit.Model + +@Composable +@Preview +fun TodoEditContentPreview() { + TodoEditContent(TodoEditPreview()) +} + +class TodoEditPreview : TodoEdit { + override val models: Value = + MutableValue( + Model( + text = "Some text", + isDone = true + ) + ) + + override fun onTextChanged(text: String) {} + override fun onDoneChanged(isDone: Boolean) {} + override fun onCloseClicked() {} +} diff --git a/examples/todoapp/common/compose-ui/src/desktopMain/kotlin/example/todo/common/ui/TodoMainPreview.kt b/examples/todoapp/common/compose-ui/src/desktopMain/kotlin/example/todo/common/ui/TodoMainPreview.kt new file mode 100644 index 0000000000..bff9c17b6b --- /dev/null +++ b/examples/todoapp/common/compose-ui/src/desktopMain/kotlin/example/todo/common/ui/TodoMainPreview.kt @@ -0,0 +1,37 @@ +package example.todo.common.ui + +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.runtime.Composable +import com.arkivanov.decompose.value.MutableValue +import com.arkivanov.decompose.value.Value +import example.todo.common.main.TodoItem +import example.todo.common.main.TodoMain +import example.todo.common.main.TodoMain.Model + +@Preview +@Composable +fun TodoMainContentPreview() { + TodoMainContent(TodoMainPreview()) +} + +class TodoMainPreview : TodoMain { + override val models: Value = + MutableValue( + Model( + items = List(5) { index -> + TodoItem( + id = index.toLong(), + text = "Item $index", + isDone = index % 2 == 0 + ) + }, + text = "Some text" + ) + ) + + override fun onItemClicked(id: Long) {} + override fun onItemDoneChanged(id: Long, isDone: Boolean) {} + override fun onItemDeleteClicked(id: Long) {} + override fun onInputTextChanged(text: String) {} + override fun onAddItemClicked() {} +} diff --git a/examples/todoapp/common/main/src/commonTest/kotlin/example/todo/common/main/integration/TodoMainTest.kt b/examples/todoapp/common/main/src/commonTest/kotlin/example/todo/common/main/integration/TodoMainTest.kt index 7d34263c38..2ce2159b99 100644 --- a/examples/todoapp/common/main/src/commonTest/kotlin/example/todo/common/main/integration/TodoMainTest.kt +++ b/examples/todoapp/common/main/src/commonTest/kotlin/example/todo/common/main/integration/TodoMainTest.kt @@ -1,7 +1,7 @@ package example.todo.common.main.integration import com.arkivanov.decompose.DefaultComponentContext -import com.arkivanov.decompose.lifecycle.LifecycleRegistry +import com.arkivanov.essenty.lifecycle.LifecycleRegistry import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory import com.badoo.reaktive.scheduler.overrideSchedulers import com.badoo.reaktive.subject.publish.PublishSubject @@ -32,7 +32,7 @@ class TodoMainTest { private val impl by lazy { TodoMainComponent( componentContext = DefaultComponentContext(lifecycle = lifecycle), - storeFactory = DefaultStoreFactory, + storeFactory = DefaultStoreFactory(), database = database, output = outputSubject ) diff --git a/examples/todoapp/common/main/src/commonTest/kotlin/example/todo/common/main/store/TodoMainStoreTest.kt b/examples/todoapp/common/main/src/commonTest/kotlin/example/todo/common/main/store/TodoMainStoreTest.kt index 4a74b7fa18..c5ace73e79 100644 --- a/examples/todoapp/common/main/src/commonTest/kotlin/example/todo/common/main/store/TodoMainStoreTest.kt +++ b/examples/todoapp/common/main/src/commonTest/kotlin/example/todo/common/main/store/TodoMainStoreTest.kt @@ -15,7 +15,7 @@ import kotlin.test.assertTrue class TodoMainStoreTest { private val database = TestTodoMainStoreDatabase() - private val provider = TodoMainStoreProvider(storeFactory = DefaultStoreFactory, database = database) + private val provider = TodoMainStoreProvider(storeFactory = DefaultStoreFactory(), database = database) @BeforeTest fun before() { diff --git a/examples/todoapp/common/root/build.gradle.kts b/examples/todoapp/common/root/build.gradle.kts index 75e7a9a4a3..bf618dc4a0 100755 --- a/examples/todoapp/common/root/build.gradle.kts +++ b/examples/todoapp/common/root/build.gradle.kts @@ -11,6 +11,7 @@ kotlin { binaries { framework { baseName = "Todo" + transitiveExport = true linkerOpts.add("-lsqlite3") export(project(":common:database")) export(project(":common:main")) diff --git a/examples/todoapp/common/root/src/commonMain/kotlin/example/todo/common/root/integration/TodoRootComponent.kt b/examples/todoapp/common/root/src/commonMain/kotlin/example/todo/common/root/integration/TodoRootComponent.kt index c3d4c6dfb8..5f467cec3f 100644 --- a/examples/todoapp/common/root/src/commonMain/kotlin/example/todo/common/root/integration/TodoRootComponent.kt +++ b/examples/todoapp/common/root/src/commonMain/kotlin/example/todo/common/root/integration/TodoRootComponent.kt @@ -5,9 +5,9 @@ import com.arkivanov.decompose.RouterState import com.arkivanov.decompose.pop import com.arkivanov.decompose.push import com.arkivanov.decompose.router -import com.arkivanov.decompose.statekeeper.Parcelable -import com.arkivanov.decompose.statekeeper.Parcelize import com.arkivanov.decompose.value.Value +import com.arkivanov.essenty.parcelable.Parcelable +import com.arkivanov.essenty.parcelable.Parcelize import com.arkivanov.mvikotlin.core.store.StoreFactory import com.badoo.reaktive.base.Consumer import example.todo.common.database.TodoSharedDatabase diff --git a/examples/todoapp/common/root/src/commonTest/kotlin/example/todo/common/root/integration/TodoRootTest.kt b/examples/todoapp/common/root/src/commonTest/kotlin/example/todo/common/root/integration/TodoRootTest.kt index 1530efb764..c5ffd60e6e 100644 --- a/examples/todoapp/common/root/src/commonTest/kotlin/example/todo/common/root/integration/TodoRootTest.kt +++ b/examples/todoapp/common/root/src/commonTest/kotlin/example/todo/common/root/integration/TodoRootTest.kt @@ -1,8 +1,8 @@ package example.todo.common.root.integration import com.arkivanov.decompose.DefaultComponentContext -import com.arkivanov.decompose.lifecycle.LifecycleRegistry -import com.arkivanov.decompose.lifecycle.resume +import com.arkivanov.essenty.lifecycle.LifecycleRegistry +import com.arkivanov.essenty.lifecycle.resume import com.badoo.reaktive.base.invoke import example.todo.common.edit.TodoEdit import example.todo.common.main.TodoMain diff --git a/examples/todoapp/common/utils/src/commonMain/kotlin/example/todo/common/utils/InstanceKeeperExt.kt b/examples/todoapp/common/utils/src/commonMain/kotlin/example/todo/common/utils/InstanceKeeperExt.kt index cac2c68262..433fba0d72 100644 --- a/examples/todoapp/common/utils/src/commonMain/kotlin/example/todo/common/utils/InstanceKeeperExt.kt +++ b/examples/todoapp/common/utils/src/commonMain/kotlin/example/todo/common/utils/InstanceKeeperExt.kt @@ -1,7 +1,7 @@ package example.todo.common.utils -import com.arkivanov.decompose.instancekeeper.InstanceKeeper -import com.arkivanov.decompose.instancekeeper.getOrCreate +import com.arkivanov.essenty.instancekeeper.InstanceKeeper +import com.arkivanov.essenty.instancekeeper.getOrCreate import com.arkivanov.mvikotlin.core.store.Store fun > InstanceKeeper.getStore(key: Any, factory: () -> T): T = diff --git a/examples/todoapp/desktop/build.gradle.kts b/examples/todoapp/desktop/build.gradle.kts index 6fcccab190..b1f790b076 100755 --- a/examples/todoapp/desktop/build.gradle.kts +++ b/examples/todoapp/desktop/build.gradle.kts @@ -36,9 +36,9 @@ compose.desktop { nativeDistributions { targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) - packageName = "ComoseDesktopTodo" + packageName = "ComposeDesktopTodo" packageVersion = "1.0.0" - + modules("java.sql") windows { diff --git a/examples/todoapp/desktop/src/jvmMain/kotlin/example/todo/desktop/Main.kt b/examples/todoapp/desktop/src/jvmMain/kotlin/example/todo/desktop/Main.kt index 0853039a36..08a7de3b87 100644 --- a/examples/todoapp/desktop/src/jvmMain/kotlin/example/todo/desktop/Main.kt +++ b/examples/todoapp/desktop/src/jvmMain/kotlin/example/todo/desktop/Main.kt @@ -1,13 +1,18 @@ package example.todo.desktop import androidx.compose.desktop.DesktopTheme -import androidx.compose.desktop.Window import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.ui.Modifier +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import androidx.compose.ui.window.rememberWindowState import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.DefaultComponentContext +import com.arkivanov.decompose.extensions.compose.jetbrains.lifecycle.LifecycleController import com.arkivanov.decompose.extensions.compose.jetbrains.rememberRootComponent +import com.arkivanov.essenty.lifecycle.LifecycleRegistry import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory import com.badoo.reaktive.coroutinesinterop.asScheduler import com.badoo.reaktive.scheduler.overrideSchedulers @@ -21,11 +26,23 @@ import kotlinx.coroutines.Dispatchers fun main() { overrideSchedulers(main = Dispatchers.Main::asScheduler) - Window("Todo") { - Surface(modifier = Modifier.fillMaxSize()) { - MaterialTheme { - DesktopTheme { - TodoRootContent(rememberRootComponent(factory = ::todoRoot)) + val lifecycle = LifecycleRegistry() + val root = todoRoot(DefaultComponentContext(lifecycle = lifecycle)) + + application { + val windowState = rememberWindowState() + LifecycleController(lifecycle, windowState) + + Window( + onCloseRequest = ::exitApplication, + state = windowState, + title = "Todo" + ) { + Surface(modifier = Modifier.fillMaxSize()) { + MaterialTheme { + DesktopTheme { + TodoRootContent(root) + } } } } @@ -35,6 +52,6 @@ fun main() { private fun todoRoot(componentContext: ComponentContext): TodoRoot = TodoRootComponent( componentContext = componentContext, - storeFactory = DefaultStoreFactory, + storeFactory = DefaultStoreFactory(), database = DefaultTodoSharedDatabase(TodoDatabaseDriver()) ) diff --git a/examples/todoapp/gradle/wrapper/gradle-wrapper.properties b/examples/todoapp/gradle/wrapper/gradle-wrapper.properties index 549d84424d..f371643eed 100755 --- a/examples/todoapp/gradle/wrapper/gradle-wrapper.properties +++ b/examples/todoapp/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/examples/todoapp/web/src/jsMain/kotlin/example/todo/web/App.kt b/examples/todoapp/web/src/jsMain/kotlin/example/todo/web/App.kt index e94d469e4b..61af087e5e 100644 --- a/examples/todoapp/web/src/jsMain/kotlin/example/todo/web/App.kt +++ b/examples/todoapp/web/src/jsMain/kotlin/example/todo/web/App.kt @@ -1,8 +1,8 @@ package example.todo.web import com.arkivanov.decompose.DefaultComponentContext -import com.arkivanov.decompose.lifecycle.LifecycleRegistry -import com.arkivanov.decompose.lifecycle.resume +import com.arkivanov.essenty.lifecycle.LifecycleRegistry +import com.arkivanov.essenty.lifecycle.resume import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory import example.todo.common.database.DefaultTodoSharedDatabase import example.todo.common.database.todoDatabaseDriver @@ -21,7 +21,7 @@ fun main() { val root = TodoRootComponent( componentContext = DefaultComponentContext(lifecycle = lifecycle), - storeFactory = DefaultStoreFactory, + storeFactory = DefaultStoreFactory(), database = DefaultTodoSharedDatabase(todoDatabaseDriver()) ) diff --git a/examples/todoapp/web/src/jsMain/kotlin/example/todo/web/Components.kt b/examples/todoapp/web/src/jsMain/kotlin/example/todo/web/Components.kt index e4dedb27cc..7db4874b18 100644 --- a/examples/todoapp/web/src/jsMain/kotlin/example/todo/web/Components.kt +++ b/examples/todoapp/web/src/jsMain/kotlin/example/todo/web/Components.kt @@ -43,7 +43,7 @@ fun MaterialCheckbox( attrs = { classes("filled-in") if (checked) checked() - onCheckboxInput { onCheckedChange(it.checked) } + onChange { onCheckedChange(it.value) } } ) @@ -85,7 +85,7 @@ fun MaterialTextArea( attrs = { id("text_area_add_todo") classes("materialize-textarea") - onTextInput { onTextChanged(it.inputValue) } + onInput { onTextChanged(it.value) } style { width(100.percent) height(100.percent) diff --git a/examples/todoapp/web/src/jsMain/kotlin/example/todo/web/TodoRootUi.kt b/examples/todoapp/web/src/jsMain/kotlin/example/todo/web/TodoRootUi.kt index 7f964a7638..5c367f4919 100644 --- a/examples/todoapp/web/src/jsMain/kotlin/example/todo/web/TodoRootUi.kt +++ b/examples/todoapp/web/src/jsMain/kotlin/example/todo/web/TodoRootUi.kt @@ -4,7 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import example.todo.common.root.TodoRoot import org.jetbrains.compose.web.css.Position -import org.jetbrains.compose.web.css.auto +import org.jetbrains.compose.web.css.keywords.auto import org.jetbrains.compose.web.css.bottom import org.jetbrains.compose.web.css.height import org.jetbrains.compose.web.css.left diff --git a/examples/web-compose-bird/build.gradle.kts b/examples/web-compose-bird/build.gradle.kts index 8d627071a3..fc1767b3fa 100644 --- a/examples/web-compose-bird/build.gradle.kts +++ b/examples/web-compose-bird/build.gradle.kts @@ -1,7 +1,6 @@ -// Add compose gradle plugin plugins { - kotlin("multiplatform") version "1.5.10" - id("org.jetbrains.compose") version "0.5.0-build228" + kotlin("multiplatform") version "1.5.21" + id("org.jetbrains.compose") version "1.0.0-alpha1" } group = "com.theapache64.composebird" version = "1.0.0-alpha01" @@ -10,6 +9,7 @@ version = "1.0.0-alpha01" repositories { mavenCentral() maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + google() } kotlin { diff --git a/examples/web-compose-bird/demo.gif b/examples/web-compose-bird/demo.gif index 588adf7f5e..b826c3137b 100644 Binary files a/examples/web-compose-bird/demo.gif and b/examples/web-compose-bird/demo.gif differ diff --git a/examples/web-compose-bird/gradle/wrapper/gradle-wrapper.properties b/examples/web-compose-bird/gradle/wrapper/gradle-wrapper.properties index da9702f9e7..05679dc3c1 100644 --- a/examples/web-compose-bird/gradle/wrapper/gradle-wrapper.properties +++ b/examples/web-compose-bird/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/examples/web-compose-bird/settings.gradle.kts b/examples/web-compose-bird/settings.gradle.kts index 283db2f32c..f063d947c5 100644 --- a/examples/web-compose-bird/settings.gradle.kts +++ b/examples/web-compose-bird/settings.gradle.kts @@ -2,7 +2,8 @@ pluginManagement { repositories { gradlePluginPortal() maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + google() } } -rootProject.name = "compose-bird" +rootProject.name = "web-compose-bird" diff --git a/examples/web-compose-bird/src/jsMain/kotlin/core/ComposeBirdGame.kt b/examples/web-compose-bird/src/jsMain/kotlin/core/ComposeBirdGame.kt index d702efd5bd..e445cb3909 100644 --- a/examples/web-compose-bird/src/jsMain/kotlin/core/ComposeBirdGame.kt +++ b/examples/web-compose-bird/src/jsMain/kotlin/core/ComposeBirdGame.kt @@ -7,6 +7,9 @@ import data.GameFrame import data.Tube import kotlin.js.Date +/** + * Game logic + */ class ComposeBirdGame : Game { companion object { @@ -39,6 +42,9 @@ class ComposeBirdGame : Game { ) } + /** + * To build a random level + */ private fun buildLevel(): List { return mutableListOf().apply { var tubesAdded = 0 @@ -59,6 +65,9 @@ class ComposeBirdGame : Game { } + /** + * To build a random vertical tube/pipe + */ private fun buildRandomTube(): List { // creating a full tube val tube = mutableListOf().apply { @@ -130,6 +139,9 @@ class ComposeBirdGame : Game { } } + /** + * To check if the bird collided with the tube (collision-detection) + */ private fun isCollidedWithTube(newBirdPos: Int, tubes: List): Boolean { val birdTube = tubes.find { it.position == BIRD_COLUMN } return birdTube?.coordinates?.get(newBirdPos) ?: false diff --git a/examples/web-compose-bird/src/jsMain/kotlin/core/Game.kt b/examples/web-compose-bird/src/jsMain/kotlin/core/Game.kt index d128fd68ea..35e593e903 100644 --- a/examples/web-compose-bird/src/jsMain/kotlin/core/Game.kt +++ b/examples/web-compose-bird/src/jsMain/kotlin/core/Game.kt @@ -3,6 +3,9 @@ package core import androidx.compose.runtime.State import data.GameFrame +/** + * A generic game interface + */ interface Game { val gameFrame: State fun step() diff --git a/examples/web-compose-bird/src/jsMain/kotlin/main.kt b/examples/web-compose-bird/src/jsMain/kotlin/main.kt index 972c2dd4ec..5525fc84c5 100644 --- a/examples/web-compose-bird/src/jsMain/kotlin/main.kt +++ b/examples/web-compose-bird/src/jsMain/kotlin/main.kt @@ -7,8 +7,11 @@ import data.GameFrame import kotlinx.browser.document import kotlinx.browser.window import kotlinx.coroutines.delay -import org.jetbrains.compose.web.attributes.* -import org.jetbrains.compose.web.css.* +import org.jetbrains.compose.web.attributes.InputType +import org.jetbrains.compose.web.attributes.checked +import org.jetbrains.compose.web.attributes.disabled +import org.jetbrains.compose.web.css.marginTop +import org.jetbrains.compose.web.css.px import org.jetbrains.compose.web.dom.* import org.jetbrains.compose.web.renderComposable import org.w3c.dom.HTMLElement @@ -31,20 +34,12 @@ fun main() { } }) - body.onclick = { - game.moveBirdUp() - } - renderComposable(rootElementId = "root") { Div( attrs = { style { - display(DisplayStyle.Flex) - justifyContent(JustifyContent.Center) - } - onClick { - game.moveBirdUp() + property("text-align", "center") } } ) { @@ -60,29 +55,19 @@ fun main() { } } - Div { + Header(gameFrame) - // Title - GameTitle() - Score(gameFrame) - Br() - - if (gameFrame.isGameOver || gameFrame.isGameWon) { - Div( - attrs = { - style { - display(DisplayStyle.Flex) - flexDirection(FlexDirection.Column) - justifyContent(JustifyContent.Center) - } - } - ) { - GameStatus(gameFrame) - TryAgain() + Div( + attrs = { + style { + marginTop(30.px) } - - + } + ) { + if (gameFrame.isGameOver || gameFrame.isGameWon) { + GameResult(gameFrame) } else { + // Play area repeat(ComposeBirdGame.ROWS) { rowIndex -> Div { repeat(ComposeBirdGame.COLUMNS) { columnIndex -> @@ -90,12 +75,6 @@ fun main() { InputType.Radio, attrs = { - - style { - width(25.px) - height(25.px) - } - val tube = gameFrame.tubes.find { it.position == columnIndex } val isTube = tube?.coordinates?.get(rowIndex) ?: false val isBird = @@ -126,27 +105,20 @@ fun main() { } @Composable -private fun TryAgain() { - Button( - attrs = { - onClick { - window.location.reload() - } - } - ) { - Text("Try Again!") +private fun Header(gameFrame: GameFrame) { + // Game title + H1 { + Text(value = "🐦 Compose Bird!") } + + // Game score + Text(value = "Your Score: ${gameFrame.score} || Top Score: ${ComposeBirdGame.TOTAL_TUBES}") } @Composable -private fun GameStatus(gameFrame: GameFrame) { - H2( - attrs = { - style { - alignSelf(AlignSelf.Center) - } - } - ) { +private fun GameResult(gameFrame: GameFrame) { + // Game Status + H2 { if (gameFrame.isGameWon) { Text("🚀 Won the game! 🚀") } else { @@ -154,32 +126,15 @@ private fun GameStatus(gameFrame: GameFrame) { Text("💀 Game Over 💀") } } -} - -@Composable -private fun Score(gameFrame: GameFrame) { - Div( - attrs = { - style { - display(DisplayStyle.Flex) - justifyContent(JustifyContent.Center) - } - } - ) { - Text("Your Score: ${gameFrame.score} || Top Score: ${ComposeBirdGame.TOTAL_TUBES}") - } -} -@Composable -private fun GameTitle() { - H1( + // Try Again + Button( attrs = { - style { - display(DisplayStyle.Flex) - justifyContent(JustifyContent.Center) + onClick { + window.location.reload() } } ) { - Text("🐦 Compose Bird!") + Text("Try Again!") } } \ No newline at end of file diff --git a/examples/web-getting-started/.gitignore b/examples/web-getting-started/.gitignore deleted file mode 100644 index 8ea68f1b7d..0000000000 --- a/examples/web-getting-started/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/.gradle/ -/.idea/ -/build/ diff --git a/examples/web-getting-started/README.md b/examples/web-getting-started/README.md deleted file mode 100644 index c9f47d93bc..0000000000 --- a/examples/web-getting-started/README.md +++ /dev/null @@ -1,4 +0,0 @@ -### Running web application -``` -./gradlew jsBrowserRun -``` \ No newline at end of file diff --git a/examples/web-getting-started/settings.gradle.kts b/examples/web-getting-started/settings.gradle.kts deleted file mode 100644 index 7072e1fd1a..0000000000 --- a/examples/web-getting-started/settings.gradle.kts +++ /dev/null @@ -1,9 +0,0 @@ -pluginManagement { - repositories { - gradlePluginPortal() - maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") - } -} - -rootProject.name = "web-getting-started" - diff --git a/examples/web-getting-started/src/jsMain/kotlin/Main.kt b/examples/web-getting-started/src/jsMain/kotlin/Main.kt deleted file mode 100644 index 95d7be667c..0000000000 --- a/examples/web-getting-started/src/jsMain/kotlin/Main.kt +++ /dev/null @@ -1,34 +0,0 @@ -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue -import org.jetbrains.compose.web.css.padding -import org.jetbrains.compose.web.css.px -import org.jetbrains.compose.web.dom.Button -import org.jetbrains.compose.web.dom.Div -import org.jetbrains.compose.web.dom.Span -import org.jetbrains.compose.web.dom.Text -import org.jetbrains.compose.web.renderComposable - -fun main() { - var count: Int by mutableStateOf(0) - - renderComposable(rootElementId = "root") { - Div({ style { padding(25.px) } }) { - Button(attrs = { - onClick { count -= 1 } - }) { - Text("-") - } - - Span({style { padding(15.px) }}) { - Text("$count") - } - - Button({ - onClick { count += 1 } - }) { - Text("+") - } - } - } -} \ No newline at end of file diff --git a/examples/web-getting-started/src/jsMain/resources/index.html b/examples/web-getting-started/src/jsMain/resources/index.html deleted file mode 100644 index 60e237b3aa..0000000000 --- a/examples/web-getting-started/src/jsMain/resources/index.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - Getting Started - - -
- - - \ No newline at end of file diff --git a/examples/web_landing/.gitignore b/examples/web-landing/.gitignore similarity index 100% rename from examples/web_landing/.gitignore rename to examples/web-landing/.gitignore diff --git a/examples/web_landing/build.gradle.kts b/examples/web-landing/build.gradle.kts similarity index 80% rename from examples/web_landing/build.gradle.kts rename to examples/web-landing/build.gradle.kts index eae09aeed3..89a52beef0 100644 --- a/examples/web_landing/build.gradle.kts +++ b/examples/web-landing/build.gradle.kts @@ -1,11 +1,12 @@ plugins { - kotlin("multiplatform") version "1.5.10" - id("org.jetbrains.compose") version "0.5.0-build228" + kotlin("multiplatform") version "1.5.21" + id("org.jetbrains.compose") version "1.0.0-alpha1" } repositories { mavenCentral() maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + google() } kotlin { @@ -24,3 +25,4 @@ kotlin { } } } + diff --git a/examples/web-getting-started/gradle.properties b/examples/web-landing/gradle.properties similarity index 100% rename from examples/web-getting-started/gradle.properties rename to examples/web-landing/gradle.properties diff --git a/examples/web-getting-started/gradle/wrapper/gradle-wrapper.jar b/examples/web-landing/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from examples/web-getting-started/gradle/wrapper/gradle-wrapper.jar rename to examples/web-landing/gradle/wrapper/gradle-wrapper.jar diff --git a/examples/falling_balls_with_web/gradle/wrapper/gradle-wrapper.properties b/examples/web-landing/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from examples/falling_balls_with_web/gradle/wrapper/gradle-wrapper.properties rename to examples/web-landing/gradle/wrapper/gradle-wrapper.properties diff --git a/examples/web-getting-started/gradlew b/examples/web-landing/gradlew similarity index 100% rename from examples/web-getting-started/gradlew rename to examples/web-landing/gradlew diff --git a/examples/web-getting-started/gradlew.bat b/examples/web-landing/gradlew.bat similarity index 100% rename from examples/web-getting-started/gradlew.bat rename to examples/web-landing/gradlew.bat diff --git a/examples/web_landing/license/LICENSE.txt b/examples/web-landing/license/LICENSE.txt similarity index 100% rename from examples/web_landing/license/LICENSE.txt rename to examples/web-landing/license/LICENSE.txt diff --git a/examples/web_landing/license/third_party/README.md b/examples/web-landing/license/third_party/README.md similarity index 100% rename from examples/web_landing/license/third_party/README.md rename to examples/web-landing/license/third_party/README.md diff --git a/examples/web_landing/license/third_party/highlightjs.txt b/examples/web-landing/license/third_party/highlightjs.txt similarity index 100% rename from examples/web_landing/license/third_party/highlightjs.txt rename to examples/web-landing/license/third_party/highlightjs.txt diff --git a/examples/web_landing/settings.gradle.kts b/examples/web-landing/settings.gradle.kts similarity index 92% rename from examples/web_landing/settings.gradle.kts rename to examples/web-landing/settings.gradle.kts index 8e6877f344..6ffdb3e370 100644 --- a/examples/web_landing/settings.gradle.kts +++ b/examples/web-landing/settings.gradle.kts @@ -3,6 +3,7 @@ pluginManagement { gradlePluginPortal() mavenCentral() maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") } + google() } } diff --git a/examples/web_landing/src/jsMain/kotlin/com/sample/HighlightJs.kt b/examples/web-landing/src/jsMain/kotlin/com/sample/HighlightJs.kt similarity index 100% rename from examples/web_landing/src/jsMain/kotlin/com/sample/HighlightJs.kt rename to examples/web-landing/src/jsMain/kotlin/com/sample/HighlightJs.kt diff --git a/examples/web_landing/src/jsMain/kotlin/com/sample/Main.kt b/examples/web-landing/src/jsMain/kotlin/com/sample/Main.kt similarity index 100% rename from examples/web_landing/src/jsMain/kotlin/com/sample/Main.kt rename to examples/web-landing/src/jsMain/kotlin/com/sample/Main.kt diff --git a/examples/web_landing/src/jsMain/kotlin/com/sample/components/Card.kt b/examples/web-landing/src/jsMain/kotlin/com/sample/components/Card.kt similarity index 100% rename from examples/web_landing/src/jsMain/kotlin/com/sample/components/Card.kt rename to examples/web-landing/src/jsMain/kotlin/com/sample/components/Card.kt diff --git a/examples/web_landing/src/jsMain/kotlin/com/sample/components/Layout.kt b/examples/web-landing/src/jsMain/kotlin/com/sample/components/Layout.kt similarity index 89% rename from examples/web_landing/src/jsMain/kotlin/com/sample/components/Layout.kt rename to examples/web-landing/src/jsMain/kotlin/com/sample/components/Layout.kt index de4887da04..90293df5ed 100644 --- a/examples/web_landing/src/jsMain/kotlin/com/sample/components/Layout.kt +++ b/examples/web-landing/src/jsMain/kotlin/com/sample/components/Layout.kt @@ -17,7 +17,7 @@ fun Layout(content: @Composable () -> Unit) { flexDirection(FlexDirection.Column) height(100.percent) margin(0.px) - property("box-sizing", "border-box") + boxSizing("border-box") } }) { content() @@ -28,8 +28,8 @@ fun Layout(content: @Composable () -> Unit) { fun MainContentLayout(content: @Composable () -> Unit) { Main({ style { - property("flex", "1 0 auto") - property("box-sizing", "border-box") + flex("1 0 auto") + boxSizing("border-box") } }) { content() diff --git a/examples/web_landing/src/jsMain/kotlin/com/sample/content/AboutComposeWebLibsSection.kt b/examples/web-landing/src/jsMain/kotlin/com/sample/content/AboutComposeWebLibsSection.kt similarity index 96% rename from examples/web_landing/src/jsMain/kotlin/com/sample/content/AboutComposeWebLibsSection.kt rename to examples/web-landing/src/jsMain/kotlin/com/sample/content/AboutComposeWebLibsSection.kt index c2daa1f94f..56bb49edb2 100644 --- a/examples/web_landing/src/jsMain/kotlin/com/sample/content/AboutComposeWebLibsSection.kt +++ b/examples/web-landing/src/jsMain/kotlin/com/sample/content/AboutComposeWebLibsSection.kt @@ -8,6 +8,7 @@ import com.sample.components.Card import com.sample.components.ContainerInSection import com.sample.components.LinkOnCard import com.sample.style.* +import org.jetbrains.compose.web.css.paddingTop data class CardWithListPresentation( val title: String, @@ -84,13 +85,13 @@ private fun CardWithList(card: CardWithListPresentation) { Ul(attrs = { classes(WtTexts.wtText2) }) { - card.list.forEachIndexed { ix, it -> + card.list.forEachIndexed { _, it -> Li({ style { - property("padding-top", 24.px) + paddingTop(24.px) } }) { Text(it) } } } } -} \ No newline at end of file +} diff --git a/examples/web_landing/src/jsMain/kotlin/com/sample/content/CodeSamplesSwitcher.kt b/examples/web-landing/src/jsMain/kotlin/com/sample/content/CodeSamplesSwitcher.kt similarity index 85% rename from examples/web_landing/src/jsMain/kotlin/com/sample/content/CodeSamplesSwitcher.kt rename to examples/web-landing/src/jsMain/kotlin/com/sample/content/CodeSamplesSwitcher.kt index e2f40d6a7d..d1e2095146 100644 --- a/examples/web_landing/src/jsMain/kotlin/com/sample/content/CodeSamplesSwitcher.kt +++ b/examples/web-landing/src/jsMain/kotlin/com/sample/content/CodeSamplesSwitcher.kt @@ -15,7 +15,7 @@ import org.jetbrains.compose.web.dom.Text import com.sample.style.AppStylesheet import org.jetbrains.compose.web.attributes.value -private object SwitcherVariables : CSSVariables { +private object SwitcherVariables { val labelWidth by variable() val labelPadding by variable() } @@ -23,7 +23,7 @@ private object SwitcherVariables : CSSVariables { object SwitcherStylesheet : StyleSheet(AppStylesheet) { val boxed by style { - media(maxWidth(640.px)) { + media(mediaMaxWidth(640.px)) { self style { SwitcherVariables.labelWidth(48.px) SwitcherVariables.labelPadding(5.px) @@ -32,11 +32,11 @@ object SwitcherStylesheet : StyleSheet(AppStylesheet) { descendant(self, CSSSelector.Type("label")) style { display(DisplayStyle.InlineBlock) - property("width", SwitcherVariables.labelWidth.value(56.px)) - property("padding", SwitcherVariables.labelPadding.value(10.px)) + width(SwitcherVariables.labelWidth.value(56.px)) + padding(SwitcherVariables.labelPadding.value(10.px)) property("transition", "all 0.3s") - property("text-align", "center") - property("box-sizing", "border-box") + textAlign("center") + boxSizing("border-box") border { style(LineStyle.Solid) @@ -44,7 +44,7 @@ object SwitcherStylesheet : StyleSheet(AppStylesheet) { color(Color("transparent")) borderRadius(20.px, 20.px, 20.px) } - color("#aaa") + color(Color("#aaa")) } border { @@ -66,7 +66,7 @@ object SwitcherStylesheet : StyleSheet(AppStylesheet) { color(Color("#167dff")) borderRadius(20.px, 20.px, 20.px) } - color("#000") + color(Color("#000")) } } } @@ -82,9 +82,9 @@ fun CodeSampleSwitcher(count: Int, current: Int, onSelect: (Int) -> Unit) { value("snippet$ix") id("snippet$ix") if (current == ix) checked() - onRadioInput { onSelect(ix) } + onChange { onSelect(ix) } }) Label(forId = "snippet$ix") { Text("${ix + 1}") } } } -} \ No newline at end of file +} diff --git a/examples/web_landing/src/jsMain/kotlin/com/sample/content/CodeSnippets.kt b/examples/web-landing/src/jsMain/kotlin/com/sample/content/CodeSnippets.kt similarity index 97% rename from examples/web_landing/src/jsMain/kotlin/com/sample/content/CodeSnippets.kt rename to examples/web-landing/src/jsMain/kotlin/com/sample/content/CodeSnippets.kt index 0a74c3fdbb..27e9a062a1 100644 --- a/examples/web_landing/src/jsMain/kotlin/com/sample/content/CodeSnippets.kt +++ b/examples/web-landing/src/jsMain/kotlin/com/sample/content/CodeSnippets.kt @@ -236,9 +236,9 @@ private fun TitledCodeSample(title: String, code: String) { Div({ classes(WtOffsets.wtTopOffset24) style { - backgroundColor(Color.RGBA(39, 40, 44, 0.05)) + backgroundColor(rgba(39, 40, 44, 0.05)) borderRadius(8.px, 8.px, 8.px) - property("padding", "12px 16px") + padding(12.px, 16.px) } }) { FormattedCodeSnippet(code = code) @@ -249,8 +249,8 @@ private fun TitledCodeSample(title: String, code: String) { fun FormattedCodeSnippet(code: String, language: String = "kotlin") { Pre({ style { - property("max-height", 25.em) - property("overflow", "auto") + maxHeight(25.em) + overflow("auto") height(auto) } }) { diff --git a/examples/web_landing/src/jsMain/kotlin/com/sample/content/Footer.kt b/examples/web-landing/src/jsMain/kotlin/com/sample/content/Footer.kt similarity index 95% rename from examples/web_landing/src/jsMain/kotlin/com/sample/content/Footer.kt rename to examples/web-landing/src/jsMain/kotlin/com/sample/content/Footer.kt index 2d565f7c7b..6afc3e72d9 100644 --- a/examples/web_landing/src/jsMain/kotlin/com/sample/content/Footer.kt +++ b/examples/web-landing/src/jsMain/kotlin/com/sample/content/Footer.kt @@ -13,13 +13,13 @@ fun PageFooter() { Footer({ style { flexShrink(0) - property("box-sizing", "border-box") + boxSizing("border-box") } }) { Section({ classes(WtSections.wtSectionBgGrayDark) style { - property("padding", "24px 0") + padding(24.px, 0.px) } }) { Div({ classes(WtContainer.wtContainer) }) { @@ -61,7 +61,7 @@ private fun CopyrightInFooter() { style { justifyContent(JustifyContent.SpaceEvenly) flexWrap(FlexWrap.Wrap) - property("padding", "0px 12px") + padding(0.px, 12.px) } }) { Span({ diff --git a/examples/web_landing/src/jsMain/kotlin/com/sample/content/GetStartedSection.kt b/examples/web-landing/src/jsMain/kotlin/com/sample/content/GetStartedSection.kt similarity index 97% rename from examples/web_landing/src/jsMain/kotlin/com/sample/content/GetStartedSection.kt rename to examples/web-landing/src/jsMain/kotlin/com/sample/content/GetStartedSection.kt index b3db58c8f1..1996f385ad 100644 --- a/examples/web_landing/src/jsMain/kotlin/com/sample/content/GetStartedSection.kt +++ b/examples/web-landing/src/jsMain/kotlin/com/sample/content/GetStartedSection.kt @@ -32,7 +32,7 @@ private fun getCards(): List { links = listOf( LinkOnCard( linkText = "Explore the source code", - linkUrl = "https://github.com/JetBrains/compose-jb/tree/master/examples/web_landing" + linkUrl = "https://github.com/JetBrains/compose-jb/tree/master/examples/web-landing" ) ) ), @@ -42,7 +42,7 @@ private fun getCards(): List { links = listOf( LinkOnCard( linkText = "Explore the source code", - linkUrl = "https://github.com/JetBrains/compose-jb/tree/master/examples/falling_balls_with_web" + linkUrl = "https://github.com/JetBrains/compose-jb/tree/master/examples/falling-balls-web" ), LinkOnCard( linkText = "Play", diff --git a/examples/web_landing/src/jsMain/kotlin/com/sample/content/Header.kt b/examples/web-landing/src/jsMain/kotlin/com/sample/content/Header.kt similarity index 93% rename from examples/web_landing/src/jsMain/kotlin/com/sample/content/Header.kt rename to examples/web-landing/src/jsMain/kotlin/com/sample/content/Header.kt index dadb0a1d06..169a9e0a6e 100644 --- a/examples/web_landing/src/jsMain/kotlin/com/sample/content/Header.kt +++ b/examples/web-landing/src/jsMain/kotlin/com/sample/content/Header.kt @@ -50,8 +50,8 @@ private fun LanguageButton() { onClick { window.alert("Oops! This feature is yet to be implemented") } }) { Img(src = "ic_lang.svg", attrs = { style { - property("padding-left", 8.px) - property("padding-right", 8.px) + paddingLeft(8.px) + paddingRight(8.px) }}) Text("English") } diff --git a/examples/web_landing/src/jsMain/kotlin/com/sample/content/IntroSection.kt b/examples/web-landing/src/jsMain/kotlin/com/sample/content/IntroSection.kt similarity index 96% rename from examples/web_landing/src/jsMain/kotlin/com/sample/content/IntroSection.kt rename to examples/web-landing/src/jsMain/kotlin/com/sample/content/IntroSection.kt index c5228a7a6d..54f1bdb87b 100644 --- a/examples/web_landing/src/jsMain/kotlin/com/sample/content/IntroSection.kt +++ b/examples/web-landing/src/jsMain/kotlin/com/sample/content/IntroSection.kt @@ -40,7 +40,7 @@ fun Intro() { classes(WtTexts.wtHero) style { display(DisplayStyle.InlineBlock) - property("white-space", "nowrap") + whiteSpace("nowrap") } }) { Text("Web") @@ -121,7 +121,7 @@ private fun IntroCodeSample() { Div({ style { marginTop(24.px) - backgroundColor(Color.RGBA(39, 40, 44, 0.05)) + backgroundColor(rgba(39, 40, 44, 0.05)) borderRadius(8.px) property("font-family", "'JetBrains Mono', monospace") } @@ -149,7 +149,7 @@ private fun IntroCodeSample() { style { height(1.px) border(width = 0.px) - backgroundColor(Color.RGBA(39, 40, 44, 0.15)) + backgroundColor(rgba(39, 40, 44, 0.15)) } }) @@ -170,7 +170,7 @@ private fun IntroCodeSampleResult() { Span({ classes(WtTexts.wtText2) style { - property("margin-right", 8.px) + marginRight(8.px) } }) { Text("Result:") @@ -220,4 +220,4 @@ private fun ComposeWebStatusMessage() { } } } -} \ No newline at end of file +} diff --git a/examples/web_landing/src/jsMain/kotlin/com/sample/content/JoinUs.kt b/examples/web-landing/src/jsMain/kotlin/com/sample/content/JoinUs.kt similarity index 100% rename from examples/web_landing/src/jsMain/kotlin/com/sample/content/JoinUs.kt rename to examples/web-landing/src/jsMain/kotlin/com/sample/content/JoinUs.kt diff --git a/examples/web_landing/src/jsMain/kotlin/com/sample/style/Stylesheet.kt b/examples/web-landing/src/jsMain/kotlin/com/sample/style/Stylesheet.kt similarity index 76% rename from examples/web_landing/src/jsMain/kotlin/com/sample/style/Stylesheet.kt rename to examples/web-landing/src/jsMain/kotlin/com/sample/style/Stylesheet.kt index 3c9605cbe1..58b2978474 100644 --- a/examples/web_landing/src/jsMain/kotlin/com/sample/style/Stylesheet.kt +++ b/examples/web-landing/src/jsMain/kotlin/com/sample/style/Stylesheet.kt @@ -3,9 +3,9 @@ package com.sample.style import org.jetbrains.compose.web.css.* import org.jetbrains.compose.web.css.selectors.CSSSelector -object AppCSSVariables : CSSVariables { - val wtColorGreyLight by variable() - val wtColorGreyDark by variable() +object AppCSSVariables { + val wtColorGreyLight by variable() + val wtColorGreyDark by variable() val wtOffsetTopUnit by variable() val wtHorizontalLayoutGutter by variable() @@ -26,24 +26,24 @@ object AppCSSVariables : CSSVariables { object AppStylesheet : StyleSheet() { val composeLogo by style { - property("max-width", 100.percent) + maxWidth(100.percent) } val composeTitleTag by style { - property("padding", "5px 12px") - property("letter-spacing", "normal") - property("font-weight", 400) - property("line-height", 24.px) + padding(5.px, 12.px) + letterSpacing("normal") + fontWeight(400) + lineHeight(24.px) position(Position.Relative) top((-32).px) marginLeft(8.px) fontSize(15.px) - backgroundColor(Color.RGBA(39, 40, 44, .05)) - color(Color.RGBA(39,40,44,.7)) + backgroundColor(rgba(39, 40, 44, .05)) + color(rgba(39, 40, 44, .7)) borderRadius(4.px, 4.px, 4.px) - media(maxWidth(640.px)) { + media(mediaMaxWidth(640.px)) { self style { top((-16).px) } @@ -66,7 +66,7 @@ object AppStylesheet : StyleSheet() { margin(0.px) } - media(maxWidth(640.px)) { + media(mediaMaxWidth(640.px)) { CSSSelector.Universal style { AppCSSVariables.wtOffsetTopUnit(16.px) AppCSSVariables.wtFlowUnit(16.px) @@ -78,8 +78,8 @@ object AppStylesheet : StyleSheet() { value = "wtCol", operator = CSSSelector.Attribute.Operator.Contains ) style { - property("margin-right", AppCSSVariables.wtHorizontalLayoutGutter.value()) - property("margin-left", AppCSSVariables.wtHorizontalLayoutGutter.value()) + marginRight(AppCSSVariables.wtHorizontalLayoutGutter.value()) + marginLeft(AppCSSVariables.wtHorizontalLayoutGutter.value()) property( "flex-basis", @@ -89,7 +89,7 @@ object AppStylesheet : StyleSheet() { "max-width", "calc(8.33333%*${AppCSSVariables.wtColCount.value()} - ${AppCSSVariables.wtHorizontalLayoutGutter.value()}*2)" ) - property("box-sizing", "border-box") + boxSizing("border-box") } } } diff --git a/examples/web_landing/src/jsMain/kotlin/com/sample/style/WtCard.kt b/examples/web-landing/src/jsMain/kotlin/com/sample/style/WtCard.kt similarity index 55% rename from examples/web_landing/src/jsMain/kotlin/com/sample/style/WtCard.kt rename to examples/web-landing/src/jsMain/kotlin/com/sample/style/WtCard.kt index b603796b05..f7407bf1aa 100644 --- a/examples/web_landing/src/jsMain/kotlin/com/sample/style/WtCard.kt +++ b/examples/web-landing/src/jsMain/kotlin/com/sample/style/WtCard.kt @@ -7,43 +7,43 @@ object WtCards : StyleSheet(AppStylesheet) { display(DisplayStyle.Flex) flexDirection(FlexDirection.Column) border(1.px, LineStyle.Solid) - property("min-height", 0) - property("box-sizing", "border-box") + minHeight(0.px) + boxSizing("border-box") } - + val wtCardThemeLight by style { - border(color = Color.RGBA(39,40,44,.2)) - color("#27282c") - backgroundColor("white") + border(color = rgba(39,40,44,.2)) + color(Color("#27282c")) + backgroundColor(Color("white")) } val wtCardThemeDark by style { - backgroundColor(Color.RGBA(255, 255, 255, 0.05)) - color(Color.RGBA(255, 255, 255, 0.6)) + backgroundColor(rgba(255, 255, 255, 0.05)) + color(rgba(255, 255, 255, 0.6)) border(0.px) } - + val wtCardSection by style { position(Position.Relative) - property("overflow", "auto") - property("flex", "1 1 auto") - property("min-height", 0) - property("box-sizing", "border-box") - property("padding", "24px 32px") + overflow("auto") + flex( "1 1 auto") + minHeight( 0.px) + boxSizing("border-box") + padding(24.px, 32.px) - media(maxWidth(640.px)) { + media(mediaMaxWidth(640.px)) { self style { padding(16.px) } } } - + val wtVerticalFlex by style { display(DisplayStyle.Flex) flexDirection(FlexDirection.Column) alignItems(AlignItems.FlexStart) } - + val wtVerticalFlexGrow by style { flexGrow(1) - property("max-width", 100.percent) + maxWidth(100.percent) } -} \ No newline at end of file +} diff --git a/web/benchmark-core/src/jsMain/kotlin/com/sample/style/WtCol.kt b/examples/web-landing/src/jsMain/kotlin/com/sample/style/WtCol.kt similarity index 94% rename from web/benchmark-core/src/jsMain/kotlin/com/sample/style/WtCol.kt rename to examples/web-landing/src/jsMain/kotlin/com/sample/style/WtCol.kt index 525a11018f..f8e5e428fe 100644 --- a/web/benchmark-core/src/jsMain/kotlin/com/sample/style/WtCol.kt +++ b/examples/web-landing/src/jsMain/kotlin/com/sample/style/WtCol.kt @@ -9,6 +9,7 @@ import org.jetbrains.compose.web.css.maxWidth import org.jetbrains.compose.web.css.media import org.jetbrains.compose.web.css.percent import org.jetbrains.compose.web.css.px +import org.jetbrains.compose.web.css.mediaMaxWidth import org.jetbrains.compose.web.css.selectors.CSSSelector fun GenericStyleSheetBuilder.mediaMaxWidth( @@ -16,7 +17,7 @@ fun GenericStyleSheetBuilder.mediaMaxWidth( cssSelector: CSSSelector, rulesBuild: TBuilder.() -> Unit ) { - media(maxWidth(value)) { + media(mediaMaxWidth(value)) { cssSelector style rulesBuild } } @@ -118,19 +119,19 @@ object WtCols : StyleSheet(AppStylesheet) { forMaxWidth(640.px) { AppCSSVariables.wtColCount(0) flexGrow(1) - property("max-width", 100.percent) + maxWidth(100.percent) } } val wtColAutoFill by style { AppCSSVariables.wtColCount(0) flexGrow(1) - property("max-width", 100.percent) + maxWidth(100.percent) } val wtColInline by style { AppCSSVariables.wtColCount(0) - property("max-width", 100.percent) + maxWidth(100.percent) property("flex-basis", "auto") } } diff --git a/examples/web-landing/src/jsMain/kotlin/com/sample/style/WtContainer.kt b/examples/web-landing/src/jsMain/kotlin/com/sample/style/WtContainer.kt new file mode 100644 index 0000000000..c9fdb011cb --- /dev/null +++ b/examples/web-landing/src/jsMain/kotlin/com/sample/style/WtContainer.kt @@ -0,0 +1,38 @@ +package com.sample.style + +import org.jetbrains.compose.web.css.* + +object WtContainer : StyleSheet(AppStylesheet) { + val wtContainer by style { + property("margin-left", "auto") + property("margin-right", "auto") + boxSizing("border-box") + paddingLeft(22.px) + paddingRight(22.px) + maxWidth(1276.px) + + media(mediaMaxWidth(640.px)) { + self style { + maxWidth(100.percent) + paddingLeft(6.px) + paddingRight(16.px) + } + } + + media(mediaMaxWidth(1276.px)) { + self style { + maxWidth(996.px) + paddingLeft(2.px) + paddingRight(22.px) + } + } + + media(mediaMaxWidth(1000.px)) { + self style { + maxWidth(100.percent) + paddingLeft(2.px) + paddingRight(22.px) + } + } + } +} diff --git a/examples/web_landing/src/jsMain/kotlin/com/sample/style/WtDisplay.kt b/examples/web-landing/src/jsMain/kotlin/com/sample/style/WtDisplay.kt similarity index 84% rename from examples/web_landing/src/jsMain/kotlin/com/sample/style/WtDisplay.kt rename to examples/web-landing/src/jsMain/kotlin/com/sample/style/WtDisplay.kt index 8868c2099f..2930f6f395 100644 --- a/examples/web_landing/src/jsMain/kotlin/com/sample/style/WtDisplay.kt +++ b/examples/web-landing/src/jsMain/kotlin/com/sample/style/WtDisplay.kt @@ -8,7 +8,7 @@ object WtDisplay : StyleSheet(AppStylesheet) { } val wtDisplayMdBlock by style { - media(maxWidth(1000.px)) { + media(mediaMaxWidth(1000.px)) { self style { display(DisplayStyle.Block) } @@ -16,10 +16,10 @@ object WtDisplay : StyleSheet(AppStylesheet) { } val wtDisplayMdNone by style { - media(maxWidth(1000.px)) { + media(mediaMaxWidth(1000.px)) { self style { display(DisplayStyle.None) } } } -} \ No newline at end of file +} diff --git a/examples/web_landing/src/jsMain/kotlin/com/sample/style/WtOffest.kt b/examples/web-landing/src/jsMain/kotlin/com/sample/style/WtOffest.kt similarity index 90% rename from examples/web_landing/src/jsMain/kotlin/com/sample/style/WtOffest.kt rename to examples/web-landing/src/jsMain/kotlin/com/sample/style/WtOffest.kt index 1f47befae9..ba70d247c0 100644 --- a/examples/web_landing/src/jsMain/kotlin/com/sample/style/WtOffest.kt +++ b/examples/web-landing/src/jsMain/kotlin/com/sample/style/WtOffest.kt @@ -22,9 +22,9 @@ object WtOffsets : StyleSheet(AppStylesheet) { val wtTopOffset48 by style { marginTop(48.px) } - + val wtTopOffsetSm12 by style { - media(maxWidth(640.px)) { + media(mediaMaxWidth(640.px)) { self style { marginTop(12.px) } @@ -32,10 +32,10 @@ object WtOffsets : StyleSheet(AppStylesheet) { } val wtTopOffsetSm24 by style { - media(maxWidth(640.px)) { + media(mediaMaxWidth(640.px)) { self style { marginTop(24.px) } } } -} \ No newline at end of file +} diff --git a/examples/web_landing/src/jsMain/kotlin/com/sample/style/WtRow.kt b/examples/web-landing/src/jsMain/kotlin/com/sample/style/WtRow.kt similarity index 92% rename from examples/web_landing/src/jsMain/kotlin/com/sample/style/WtRow.kt rename to examples/web-landing/src/jsMain/kotlin/com/sample/style/WtRow.kt index cbca28aabc..5980a763d1 100644 --- a/examples/web_landing/src/jsMain/kotlin/com/sample/style/WtRow.kt +++ b/examples/web-landing/src/jsMain/kotlin/com/sample/style/WtRow.kt @@ -17,13 +17,13 @@ object WtRows : StyleSheet(AppStylesheet) { "margin-left", "calc(-1*${AppCSSVariables.wtHorizontalLayoutGutter.value()})" ) - property("box-sizing", "border-box") + boxSizing("border-box") } val wtRowSizeM by style { AppCSSVariables.wtHorizontalLayoutGutter(16.px) - media(maxWidth(640.px)) { + media(mediaMaxWidth(640.px)) { self style { AppCSSVariables.wtHorizontalLayoutGutter(8.px) } @@ -39,4 +39,4 @@ object WtRows : StyleSheet(AppStylesheet) { alignItems(AlignItems.Center) } } -} \ No newline at end of file +} diff --git a/examples/web_landing/src/jsMain/kotlin/com/sample/style/WtSection.kt b/examples/web-landing/src/jsMain/kotlin/com/sample/style/WtSection.kt similarity index 69% rename from examples/web_landing/src/jsMain/kotlin/com/sample/style/WtSection.kt rename to examples/web-landing/src/jsMain/kotlin/com/sample/style/WtSection.kt index 9ff4a403d6..9ac4096ee2 100644 --- a/examples/web_landing/src/jsMain/kotlin/com/sample/style/WtSection.kt +++ b/examples/web-landing/src/jsMain/kotlin/com/sample/style/WtSection.kt @@ -5,23 +5,23 @@ import org.jetbrains.compose.web.css.* object WtSections : StyleSheet(AppStylesheet) { val wtSection by style { - property("box-sizing", "border-box") - property("padding-bottom", 96.px) - property("padding-top", 1.px) + boxSizing("border-box") + paddingBottom(96.px) + paddingTop(1.px) property( propertyName = "padding-bottom", value = "calc(4*${AppCSSVariables.wtOffsetTopUnit.value(24.px)})" ) - backgroundColor("#fff") + backgroundColor(Color("#fff")) } val wtSectionBgGrayLight by style { - backgroundColor("#f4f4f4") + backgroundColor(Color("#f4f4f4")) backgroundColor(AppCSSVariables.wtColorGreyLight.value()) } val wtSectionBgGrayDark by style { - backgroundColor("#323236") + backgroundColor(Color("#323236")) backgroundColor(AppCSSVariables.wtColorGreyDark.value()) } -} \ No newline at end of file +} diff --git a/examples/web_landing/src/jsMain/kotlin/com/sample/style/WtText.kt b/examples/web-landing/src/jsMain/kotlin/com/sample/style/WtText.kt similarity index 56% rename from examples/web_landing/src/jsMain/kotlin/com/sample/style/WtText.kt rename to examples/web-landing/src/jsMain/kotlin/com/sample/style/WtText.kt index ded73e36c7..8fa22c4d57 100644 --- a/examples/web_landing/src/jsMain/kotlin/com/sample/style/WtText.kt +++ b/examples/web-landing/src/jsMain/kotlin/com/sample/style/WtText.kt @@ -6,15 +6,15 @@ import org.jetbrains.compose.web.css.selectors.hover object WtTexts : StyleSheet(AppStylesheet) { val wtHero by style { - color("#27282c") + color(Color("#27282c")) fontSize(60.px) - property("font-size", AppCSSVariables.wtHeroFontSize.value(60.px)) - property("letter-spacing", (-1.5).px) - property("font-weight", 900) - property("line-height", 64.px) - property("line-height", AppCSSVariables.wtHeroLineHeight.value(64.px)) + fontSize(AppCSSVariables.wtHeroFontSize.value(60.px)) + letterSpacing((-1.5).px) + fontWeight(900) + lineHeight(64.px) + lineHeight(AppCSSVariables.wtHeroLineHeight.value(64.px)) - media(maxWidth(640.px)) { + media(mediaMaxWidth(640.px)) { self style { AppCSSVariables.wtHeroFontSize(42.px) AppCSSVariables.wtHeroLineHeight(48.px) @@ -28,15 +28,15 @@ object WtTexts : StyleSheet(AppStylesheet) { } val wtSubtitle2 by style { - color("#27282c") + color(Color("#27282c")) fontSize(28.px) - property("font-size", AppCSSVariables.wtSubtitle2FontSize.value(28.px)) - property("letter-spacing", "normal") - property("font-weight", 300) - property("line-height", 40.px) - property("line-height", AppCSSVariables.wtSubtitle2LineHeight.value(40.px)) + fontSize(AppCSSVariables.wtSubtitle2FontSize.value(28.px)) + letterSpacing("normal") + fontWeight(300) + lineHeight(40.px) + lineHeight(AppCSSVariables.wtSubtitle2LineHeight.value(40.px)) - media(maxWidth(640.px)) { + media(mediaMaxWidth(640.px)) { self style { AppCSSVariables.wtSubtitle2FontSize(24.px) AppCSSVariables.wtSubtitle2LineHeight(32.px) @@ -50,11 +50,11 @@ object WtTexts : StyleSheet(AppStylesheet) { } val wtText1 by style { - color(Color.RGBA(39, 40, 44, .7)) + color(rgba(39, 40, 44, .7)) fontSize(18.px) - property("letter-spacing", "normal") - property("font-weight", 400) - property("line-height", 28.px) + letterSpacing("normal") + fontWeight(400) + lineHeight(28.px) property( "font-family", @@ -63,15 +63,15 @@ object WtTexts : StyleSheet(AppStylesheet) { } val wtText1ThemeDark by style { - color(Color.RGBA(255, 255, 255, 0.6)) + color(rgba(255, 255, 255, 0.6)) } val wtText2 by style { - color(Color.RGBA(39, 40, 44, .7)) + color(rgba(39, 40, 44, .7)) fontSize(15.px) - property("letter-spacing", "normal") - property("font-weight", 400) - property("line-height", 24.px) + letterSpacing("normal") + fontWeight(400) + lineHeight(24.px) property( "font-family", @@ -80,11 +80,11 @@ object WtTexts : StyleSheet(AppStylesheet) { } val wtText3 by style { - color(Color.RGBA(39, 40, 44, .7)) + color(rgba(39, 40, 44, .7)) fontSize(12.px) - property("letter-spacing", "normal") - property("font-weight", 400) - property("line-height", 16.px) + letterSpacing("normal") + fontWeight(400) + lineHeight(16.px) property( "font-family", @@ -93,21 +93,21 @@ object WtTexts : StyleSheet(AppStylesheet) { } val wtTextPale by style { - color(Color.RGBA(255, 255, 255, 0.30)) + color(rgba(255, 255, 255, 0.30)) } val wtText2ThemeDark by style { - color(Color.RGBA(255, 255, 255, 0.6)) + color(rgba(255, 255, 255, 0.6)) } val wtText3ThemeDark by style { - color(Color.RGBA(255, 255, 255, 0.6)) + color(rgba(255, 255, 255, 0.6)) } val wtLink by style { property("border-bottom", "1px solid transparent") property("text-decoration", "none") - color("#167dff") + color(Color("#167dff")) hover(self) style { property("border-bottom-color", "#167dff") @@ -115,15 +115,15 @@ object WtTexts : StyleSheet(AppStylesheet) { } val wtH2 by style { - color("#27282c") + color(Color("#27282c")) fontSize(31.px) - property("font-size", AppCSSVariables.wtH2FontSize.value(31.px)) - property("letter-spacing", (-.5).px) - property("font-weight", 700) - property("line-height", 40.px) - property("line-height", 40.px) + fontSize(AppCSSVariables.wtH2FontSize.value(31.px)) + letterSpacing((-.5).px) + fontWeight(700) + lineHeight(40.px) + lineHeight(40.px) - media(maxWidth(640.px)) { + media(mediaMaxWidth(640.px)) { self style { AppCSSVariables.wtH2FontSize(24.px) AppCSSVariables.wtH2LineHeight(32.px) @@ -137,17 +137,17 @@ object WtTexts : StyleSheet(AppStylesheet) { } val wtH2ThemeDark by style { - color("#fff") + color(Color("#fff")) } val wtH3 by style { - color("#27282c") + color(Color("#27282c")) fontSize(21.px) - property("font-size", AppCSSVariables.wtH3FontSize.value(20.px)) - property("letter-spacing", "normal") - property("font-weight", 700) - property("line-height", 28.px) - property("line-height", AppCSSVariables.wtH3LineHeight.value(28.px)) + fontSize(AppCSSVariables.wtH3FontSize.value(20.px)) + letterSpacing("normal") + fontWeight(700) + lineHeight(28.px) + lineHeight(AppCSSVariables.wtH3LineHeight.value(28.px)) property( "font-family", @@ -156,23 +156,23 @@ object WtTexts : StyleSheet(AppStylesheet) { } val wtH3ThemeDark by style { - color("#fff") + color(Color("#fff")) } val wtButton by style { - color("white") - backgroundColor("#167dff") + color(Color("white")) + backgroundColor(Color("#167dff")) fontSize(15.px) display(DisplayStyle.InlineBlock) - property("text-decoration", "none") - property("border-radius", "24px") - property("padding", "12px 32px") - property("line-height", 24.px) - property("font-weight", 400) + textDecoration("none") + borderRadius(24.px) + padding(12.px, 32.px) + lineHeight(24.px) + fontWeight(400) property("width", "fit-content") hover(self) style { - backgroundColor(Color.RGBA(22, 125, 255, .8)) + backgroundColor(rgba(22, 125, 255, .8)) } } @@ -183,39 +183,39 @@ object WtTexts : StyleSheet(AppStylesheet) { backgroundColor(Color("transparent")) border(0.px) - property("outline", "none") + outline("none") hover(self) style { - backgroundColor(Color.RGBA(255, 255, 255, 0.1)) + backgroundColor(rgba(255, 255, 255, 0.1)) } } val wtButtonContrast by style { - color("white") - backgroundColor("#27282c") + color(Color("white")) + backgroundColor(Color("#27282c")) hover(self) style { - backgroundColor(Color.RGBA(39, 40, 44, .7)) + backgroundColor(rgba(39, 40, 44, .7)) } } val wtSocialButtonItem by style { - property("margin-right", 16.px) + marginRight(16.px) marginLeft(16.px) padding(12.px) - backgroundColor("transparent") + backgroundColor(Color("transparent")) display(DisplayStyle.LegacyInlineFlex) hover(self) style { - backgroundColor(Color.RGBA(255, 255, 255, 0.1)) - property("border-radius", "24px") + backgroundColor(rgba(255, 255, 255, 0.1)) + borderRadius(24.px) } - media(maxWidth(640.px)) { + media(mediaMaxWidth(640.px)) { self style { - property("margin-right", 8.px) - property("margin-left", 8.px) + marginRight(8.px) + marginLeft(8.px) } } } -} \ No newline at end of file +} diff --git a/examples/web_landing/src/jsMain/resources/favicon-32x32.png b/examples/web-landing/src/jsMain/resources/favicon-32x32.png similarity index 100% rename from examples/web_landing/src/jsMain/resources/favicon-32x32.png rename to examples/web-landing/src/jsMain/resources/favicon-32x32.png diff --git a/examples/web_landing/src/jsMain/resources/hljs.css b/examples/web-landing/src/jsMain/resources/hljs.css similarity index 100% rename from examples/web_landing/src/jsMain/resources/hljs.css rename to examples/web-landing/src/jsMain/resources/hljs.css diff --git a/examples/web-landing/src/jsMain/resources/i1.svg b/examples/web-landing/src/jsMain/resources/i1.svg new file mode 100644 index 0000000000..a5590edea3 --- /dev/null +++ b/examples/web-landing/src/jsMain/resources/i1.svg @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/examples/web_landing/src/jsMain/resources/ic_fb.svg b/examples/web-landing/src/jsMain/resources/ic_fb.svg similarity index 100% rename from examples/web_landing/src/jsMain/resources/ic_fb.svg rename to examples/web-landing/src/jsMain/resources/ic_fb.svg diff --git a/examples/web_landing/src/jsMain/resources/ic_feed.svg b/examples/web-landing/src/jsMain/resources/ic_feed.svg similarity index 100% rename from examples/web_landing/src/jsMain/resources/ic_feed.svg rename to examples/web-landing/src/jsMain/resources/ic_feed.svg diff --git a/examples/web_landing/src/jsMain/resources/ic_info.svg b/examples/web-landing/src/jsMain/resources/ic_info.svg similarity index 100% rename from examples/web_landing/src/jsMain/resources/ic_info.svg rename to examples/web-landing/src/jsMain/resources/ic_info.svg diff --git a/examples/web_landing/src/jsMain/resources/ic_insta.svg b/examples/web-landing/src/jsMain/resources/ic_insta.svg similarity index 100% rename from examples/web_landing/src/jsMain/resources/ic_insta.svg rename to examples/web-landing/src/jsMain/resources/ic_insta.svg diff --git a/examples/web_landing/src/jsMain/resources/ic_jb_blog.svg b/examples/web-landing/src/jsMain/resources/ic_jb_blog.svg similarity index 100% rename from examples/web_landing/src/jsMain/resources/ic_jb_blog.svg rename to examples/web-landing/src/jsMain/resources/ic_jb_blog.svg diff --git a/examples/web_landing/src/jsMain/resources/ic_lang.svg b/examples/web-landing/src/jsMain/resources/ic_lang.svg similarity index 100% rename from examples/web_landing/src/jsMain/resources/ic_lang.svg rename to examples/web-landing/src/jsMain/resources/ic_lang.svg diff --git a/examples/web_landing/src/jsMain/resources/ic_linkedin.svg b/examples/web-landing/src/jsMain/resources/ic_linkedin.svg similarity index 100% rename from examples/web_landing/src/jsMain/resources/ic_linkedin.svg rename to examples/web-landing/src/jsMain/resources/ic_linkedin.svg diff --git a/examples/web_landing/src/jsMain/resources/ic_twitter.svg b/examples/web-landing/src/jsMain/resources/ic_twitter.svg similarity index 100% rename from examples/web_landing/src/jsMain/resources/ic_twitter.svg rename to examples/web-landing/src/jsMain/resources/ic_twitter.svg diff --git a/examples/web_landing/src/jsMain/resources/ic_youtube.svg b/examples/web-landing/src/jsMain/resources/ic_youtube.svg similarity index 100% rename from examples/web_landing/src/jsMain/resources/ic_youtube.svg rename to examples/web-landing/src/jsMain/resources/ic_youtube.svg diff --git a/examples/web_landing/src/jsMain/resources/index.html b/examples/web-landing/src/jsMain/resources/index.html similarity index 100% rename from examples/web_landing/src/jsMain/resources/index.html rename to examples/web-landing/src/jsMain/resources/index.html diff --git a/examples/web_landing/src/jsMain/resources/logos.css b/examples/web-landing/src/jsMain/resources/logos.css similarity index 100% rename from examples/web_landing/src/jsMain/resources/logos.css rename to examples/web-landing/src/jsMain/resources/logos.css diff --git a/examples/web-with-react/build.gradle.kts b/examples/web-with-react/build.gradle.kts index 183a6391c7..ee05ce58ab 100644 --- a/examples/web-with-react/build.gradle.kts +++ b/examples/web-with-react/build.gradle.kts @@ -1,12 +1,13 @@ plugins { - kotlin("multiplatform") version "1.5.10" - id("org.jetbrains.compose") version "0.5.0-build228" + kotlin("multiplatform") version "1.5.21" + id("org.jetbrains.compose") version "1.0.0-alpha1" } repositories { mavenCentral() maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/kotlin-js-wrappers") + google() } kotlin { diff --git a/examples/web-with-react/gradle/wrapper/gradle-wrapper.properties b/examples/web-with-react/gradle/wrapper/gradle-wrapper.properties index 29e4134576..05679dc3c1 100644 --- a/examples/web-with-react/gradle/wrapper/gradle-wrapper.properties +++ b/examples/web-with-react/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/examples/web-with-react/settings.gradle.kts b/examples/web-with-react/settings.gradle.kts index bbdf9586ce..2080023004 100644 --- a/examples/web-with-react/settings.gradle.kts +++ b/examples/web-with-react/settings.gradle.kts @@ -2,6 +2,7 @@ pluginManagement { repositories { gradlePluginPortal() maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + google() } } diff --git a/examples/web-with-react/src/jsMain/kotlin/ComposeInReactApp.kt b/examples/web-with-react/src/jsMain/kotlin/ComposeInReactApp.kt index 7cd8f8e8c4..d3e041c6df 100644 --- a/examples/web-with-react/src/jsMain/kotlin/ComposeInReactApp.kt +++ b/examples/web-with-react/src/jsMain/kotlin/ComposeInReactApp.kt @@ -69,11 +69,7 @@ private val composeListComponentWrapper = functionalComponent { props } // This div will be a root for the Composition managed by Compose - div { - attrs { - ref { containerRef.current = it } - } - } + div { ref { containerRef.current = it } } } private val column = functionalComponent { diff --git a/examples/web_landing/gradle.properties b/examples/web_landing/gradle.properties deleted file mode 100644 index 29e08e8ca8..0000000000 --- a/examples/web_landing/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -kotlin.code.style=official \ No newline at end of file diff --git a/examples/web_landing/gradle/wrapper/gradle-wrapper.properties b/examples/web_landing/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 29e4134576..0000000000 --- a/examples/web_landing/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/examples/web_landing/gradlew b/examples/web_landing/gradlew deleted file mode 100755 index 4f906e0c81..0000000000 --- a/examples/web_landing/gradlew +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env sh - -# -# Copyright 2015 the original author or authors. -# -# 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 -# -# https://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. -# - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=`expr $i + 1` - done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -exec "$JAVACMD" "$@" diff --git a/examples/web_landing/gradlew.bat b/examples/web_landing/gradlew.bat deleted file mode 100644 index 107acd32c4..0000000000 --- a/examples/web_landing/gradlew.bat +++ /dev/null @@ -1,89 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/examples/web_landing/src/jsMain/kotlin/com/sample/style/WtCol.kt b/examples/web_landing/src/jsMain/kotlin/com/sample/style/WtCol.kt deleted file mode 100644 index 525a11018f..0000000000 --- a/examples/web_landing/src/jsMain/kotlin/com/sample/style/WtCol.kt +++ /dev/null @@ -1,136 +0,0 @@ -package com.sample.style - -import org.jetbrains.compose.web.css.CSSBuilder -import org.jetbrains.compose.web.css.CSSUnitValue -import org.jetbrains.compose.web.css.GenericStyleSheetBuilder -import org.jetbrains.compose.web.css.StyleSheet -import org.jetbrains.compose.web.css.flexGrow -import org.jetbrains.compose.web.css.maxWidth -import org.jetbrains.compose.web.css.media -import org.jetbrains.compose.web.css.percent -import org.jetbrains.compose.web.css.px -import org.jetbrains.compose.web.css.selectors.CSSSelector - -fun GenericStyleSheetBuilder.mediaMaxWidth( - value: CSSUnitValue, - cssSelector: CSSSelector, - rulesBuild: TBuilder.() -> Unit -) { - media(maxWidth(value)) { - cssSelector style rulesBuild - } -} - -fun CSSBuilder.forMaxWidth(value: CSSUnitValue, builder: CSSBuilder.() -> Unit) { - mediaMaxWidth(value, self, builder) -} - -object WtCols : StyleSheet(AppStylesheet) { - val wtCol2 by style { - AppCSSVariables.wtColCount(2) - } - - val wtCol3 by style { - AppCSSVariables.wtColCount(3) - } - - val wtCol4 by style { - AppCSSVariables.wtColCount(4) - } - - val wtCol5 by style { - AppCSSVariables.wtColCount(5) - } - - val wtCol6 by style { - AppCSSVariables.wtColCount(6) - } - - val wtCol9 by style { - AppCSSVariables.wtColCount(9) - } - - val wtCol10 by style { - AppCSSVariables.wtColCount(10) - } - - val wtColMd3 by style { - forMaxWidth(1000.px) { - AppCSSVariables.wtColCount(3) - } - } - - val wtColMd4 by style { - forMaxWidth(1000.px) { - AppCSSVariables.wtColCount(4) - } - } - - val wtColMd8 by style { - forMaxWidth(1000.px) { - AppCSSVariables.wtColCount(8) - } - } - - val wtColMd9 by style { - forMaxWidth(1000.px) { - AppCSSVariables.wtColCount(9) - } - } - - val wtColMd10 by style { - forMaxWidth(1000.px) { - AppCSSVariables.wtColCount(10) - } - } - - val wtColMd11 by style { - forMaxWidth(1000.px) { - AppCSSVariables.wtColCount(11) - } - } - - val wtColMd6 by style { - forMaxWidth(1000.px) { - AppCSSVariables.wtColCount(6) - } - } - - val wtColMd12 by style { - forMaxWidth(1000.px) { - AppCSSVariables.wtColCount(12) - } - } - - val wtColSm12 by style { - forMaxWidth(640.px) { - AppCSSVariables.wtColCount(12) - } - } - - val wtColLg6 by style { - forMaxWidth(1276.px) { - AppCSSVariables.wtColCount(6) - } - } - - val wtColSmAutoFill by style { - forMaxWidth(640.px) { - AppCSSVariables.wtColCount(0) - flexGrow(1) - property("max-width", 100.percent) - } - } - - val wtColAutoFill by style { - AppCSSVariables.wtColCount(0) - flexGrow(1) - property("max-width", 100.percent) - } - - val wtColInline by style { - AppCSSVariables.wtColCount(0) - property("max-width", 100.percent) - property("flex-basis", "auto") - } -} diff --git a/examples/web_landing/src/jsMain/kotlin/com/sample/style/WtContainer.kt b/examples/web_landing/src/jsMain/kotlin/com/sample/style/WtContainer.kt deleted file mode 100644 index 78bbb890a7..0000000000 --- a/examples/web_landing/src/jsMain/kotlin/com/sample/style/WtContainer.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.sample.style - -import org.jetbrains.compose.web.css.* - -object WtContainer : StyleSheet(AppStylesheet) { - val wtContainer by style { - property("margin-left", "auto") - property("margin-right", "auto") - property("box-sizing", "border-box") - property("padding-left", 22.px) - property("padding-right", 22.px) - property("max-width", 1276.px) - - media(maxWidth(640.px)) { - self style { - property("max-width", 100.percent) - property("padding-left", 16.px) - property("padding-right", 16.px) - } - } - - media(maxWidth(1276.px)) { - self style { - property("max-width", 996.px) - property("padding-left", 22.px) - property("padding-right", 22.px) - } - } - - media(maxWidth(1000.px)) { - self style { - property("max-width", 100.percent) - property("padding-left", 22.px) - property("padding-right", 22.px) - } - } - } -} \ No newline at end of file diff --git a/examples/web_landing/src/jsMain/resources/i1.svg b/examples/web_landing/src/jsMain/resources/i1.svg deleted file mode 100644 index 1aa3f8bd18..0000000000 --- a/examples/web_landing/src/jsMain/resources/i1.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples/widgetsgallery/.gitignore b/examples/widgets-gallery/.gitignore similarity index 100% rename from examples/widgetsgallery/.gitignore rename to examples/widgets-gallery/.gitignore diff --git a/examples/widgetsgallery/README.md b/examples/widgets-gallery/README.md similarity index 100% rename from examples/widgetsgallery/README.md rename to examples/widgets-gallery/README.md diff --git a/examples/widgetsgallery/android/build.gradle.kts b/examples/widgets-gallery/android/build.gradle.kts similarity index 71% rename from examples/widgetsgallery/android/build.gradle.kts rename to examples/widgets-gallery/android/build.gradle.kts index 2d3ef14095..0bec349e9e 100644 --- a/examples/widgetsgallery/android/build.gradle.kts +++ b/examples/widgets-gallery/android/build.gradle.kts @@ -5,11 +5,11 @@ plugins { } android { - compileSdkVersion(30) + compileSdk = 30 defaultConfig { - minSdkVersion(26) - targetSdkVersion(30) + minSdk = 26 + targetSdk = 30 versionCode = 1 versionName = "1.0" } @@ -22,5 +22,5 @@ android { dependencies { implementation(project(":common")) - implementation("androidx.activity:activity-compose:1.3.0-alpha02") + implementation("androidx.activity:activity-compose:1.3.0") } \ No newline at end of file diff --git a/examples/widgetsgallery/android/src/main/AndroidManifest.xml b/examples/widgets-gallery/android/src/main/AndroidManifest.xml similarity index 100% rename from examples/widgetsgallery/android/src/main/AndroidManifest.xml rename to examples/widgets-gallery/android/src/main/AndroidManifest.xml diff --git a/examples/widgetsgallery/android/src/main/java/org/jetbrains/compose/demo/widgets/MainActivity.kt b/examples/widgets-gallery/android/src/main/java/org/jetbrains/compose/demo/widgets/MainActivity.kt similarity index 100% rename from examples/widgetsgallery/android/src/main/java/org/jetbrains/compose/demo/widgets/MainActivity.kt rename to examples/widgets-gallery/android/src/main/java/org/jetbrains/compose/demo/widgets/MainActivity.kt diff --git a/examples/widgetsgallery/android/src/main/res/drawable-v24/ic_launcher_background.xml b/examples/widgets-gallery/android/src/main/res/drawable-v24/ic_launcher_background.xml similarity index 100% rename from examples/widgetsgallery/android/src/main/res/drawable-v24/ic_launcher_background.xml rename to examples/widgets-gallery/android/src/main/res/drawable-v24/ic_launcher_background.xml diff --git a/examples/widgetsgallery/android/src/main/res/drawable-v24/ic_launcher_foreground.xml b/examples/widgets-gallery/android/src/main/res/drawable-v24/ic_launcher_foreground.xml similarity index 100% rename from examples/widgetsgallery/android/src/main/res/drawable-v24/ic_launcher_foreground.xml rename to examples/widgets-gallery/android/src/main/res/drawable-v24/ic_launcher_foreground.xml diff --git a/examples/widgetsgallery/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/examples/widgets-gallery/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 100% rename from examples/widgetsgallery/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to examples/widgets-gallery/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml diff --git a/examples/widgetsgallery/android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/examples/widgets-gallery/android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml similarity index 100% rename from examples/widgetsgallery/android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml rename to examples/widgets-gallery/android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml diff --git a/examples/widgetsgallery/android/src/main/res/mipmap-hdpi/ic_launcher.png b/examples/widgets-gallery/android/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from examples/widgetsgallery/android/src/main/res/mipmap-hdpi/ic_launcher.png rename to examples/widgets-gallery/android/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/examples/widgetsgallery/android/src/main/res/mipmap-hdpi/ic_launcher_round.png b/examples/widgets-gallery/android/src/main/res/mipmap-hdpi/ic_launcher_round.png similarity index 100% rename from examples/widgetsgallery/android/src/main/res/mipmap-hdpi/ic_launcher_round.png rename to examples/widgets-gallery/android/src/main/res/mipmap-hdpi/ic_launcher_round.png diff --git a/examples/widgetsgallery/android/src/main/res/mipmap-mdpi/ic_launcher.png b/examples/widgets-gallery/android/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from examples/widgetsgallery/android/src/main/res/mipmap-mdpi/ic_launcher.png rename to examples/widgets-gallery/android/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/examples/widgetsgallery/android/src/main/res/mipmap-mdpi/ic_launcher_round.png b/examples/widgets-gallery/android/src/main/res/mipmap-mdpi/ic_launcher_round.png similarity index 100% rename from examples/widgetsgallery/android/src/main/res/mipmap-mdpi/ic_launcher_round.png rename to examples/widgets-gallery/android/src/main/res/mipmap-mdpi/ic_launcher_round.png diff --git a/examples/widgetsgallery/android/src/main/res/mipmap-xhdpi/ic_launcher.png b/examples/widgets-gallery/android/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from examples/widgetsgallery/android/src/main/res/mipmap-xhdpi/ic_launcher.png rename to examples/widgets-gallery/android/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/examples/widgetsgallery/android/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/examples/widgets-gallery/android/src/main/res/mipmap-xhdpi/ic_launcher_round.png similarity index 100% rename from examples/widgetsgallery/android/src/main/res/mipmap-xhdpi/ic_launcher_round.png rename to examples/widgets-gallery/android/src/main/res/mipmap-xhdpi/ic_launcher_round.png diff --git a/examples/widgetsgallery/android/src/main/res/mipmap-xxhdpi/ic_launcher.png b/examples/widgets-gallery/android/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from examples/widgetsgallery/android/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to examples/widgets-gallery/android/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/examples/widgetsgallery/android/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/examples/widgets-gallery/android/src/main/res/mipmap-xxhdpi/ic_launcher_round.png similarity index 100% rename from examples/widgetsgallery/android/src/main/res/mipmap-xxhdpi/ic_launcher_round.png rename to examples/widgets-gallery/android/src/main/res/mipmap-xxhdpi/ic_launcher_round.png diff --git a/examples/widgetsgallery/android/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/examples/widgets-gallery/android/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from examples/widgetsgallery/android/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to examples/widgets-gallery/android/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/examples/widgetsgallery/android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/examples/widgets-gallery/android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png similarity index 100% rename from examples/widgetsgallery/android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png rename to examples/widgets-gallery/android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/examples/widgetsgallery/android/src/main/res/values-night/themes.xml b/examples/widgets-gallery/android/src/main/res/values-night/themes.xml similarity index 100% rename from examples/widgetsgallery/android/src/main/res/values-night/themes.xml rename to examples/widgets-gallery/android/src/main/res/values-night/themes.xml diff --git a/examples/widgetsgallery/android/src/main/res/values/colors.xml b/examples/widgets-gallery/android/src/main/res/values/colors.xml similarity index 100% rename from examples/widgetsgallery/android/src/main/res/values/colors.xml rename to examples/widgets-gallery/android/src/main/res/values/colors.xml diff --git a/examples/widgetsgallery/android/src/main/res/values/strings.xml b/examples/widgets-gallery/android/src/main/res/values/strings.xml similarity index 100% rename from examples/widgetsgallery/android/src/main/res/values/strings.xml rename to examples/widgets-gallery/android/src/main/res/values/strings.xml diff --git a/examples/widgetsgallery/android/src/main/res/values/themes.xml b/examples/widgets-gallery/android/src/main/res/values/themes.xml similarity index 100% rename from examples/widgetsgallery/android/src/main/res/values/themes.xml rename to examples/widgets-gallery/android/src/main/res/values/themes.xml diff --git a/examples/widgetsgallery/build.gradle.kts b/examples/widgets-gallery/build.gradle.kts similarity index 68% rename from examples/widgetsgallery/build.gradle.kts rename to examples/widgets-gallery/build.gradle.kts index 1532fa35f3..23253c1fd2 100644 --- a/examples/widgetsgallery/build.gradle.kts +++ b/examples/widgets-gallery/build.gradle.kts @@ -8,10 +8,10 @@ buildscript { dependencies { // __LATEST_COMPOSE_RELEASE_VERSION__ - classpath("org.jetbrains.compose:compose-gradle-plugin:0.4.0") - classpath("com.android.tools.build:gradle:4.0.1") + classpath("org.jetbrains.compose:compose-gradle-plugin:1.0.0-alpha1") + classpath("com.android.tools.build:gradle:7.0.0") // __KOTLIN_COMPOSE_VERSION__ - classpath(kotlin("gradle-plugin", version = "1.5.10")) + classpath(kotlin("gradle-plugin", version = "1.5.21")) } } diff --git a/examples/widgetsgallery/common/build.gradle.kts b/examples/widgets-gallery/common/build.gradle.kts similarity index 75% rename from examples/widgetsgallery/common/build.gradle.kts rename to examples/widgets-gallery/common/build.gradle.kts index 1d30687a8e..0b8317b3da 100644 --- a/examples/widgetsgallery/common/build.gradle.kts +++ b/examples/widgets-gallery/common/build.gradle.kts @@ -20,15 +20,12 @@ kotlin { } } named("androidMain") { - kotlin.srcDirs("src/jvmMain/kotlin") dependencies { - api("androidx.appcompat:appcompat:1.3.0-beta01") + api("androidx.appcompat:appcompat:1.3.1") api("androidx.core:core-ktx:1.3.1") } } named("desktopMain") { - kotlin.srcDirs("src/jvmMain/kotlin") - resources.srcDirs("src/commonMain/resources") dependencies { api(compose.desktop.common) } @@ -37,13 +34,11 @@ kotlin { } android { - compileSdkVersion(30) + compileSdk = 30 defaultConfig { - minSdkVersion(21) - targetSdkVersion(30) - versionCode = 1 - versionName = "1.0" + minSdk = 21 + targetSdk = 30 } compileOptions { @@ -57,4 +52,4 @@ android { res.srcDirs("src/androidMain/res", "src/commonMain/resources") } } -} +} \ No newline at end of file diff --git a/examples/widgets-gallery/common/src/androidMain/AndroidManifest.xml b/examples/widgets-gallery/common/src/androidMain/AndroidManifest.xml new file mode 100644 index 0000000000..3c65a79fe0 --- /dev/null +++ b/examples/widgets-gallery/common/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/examples/widgetsgallery/common/src/androidMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Mouse.kt b/examples/widgets-gallery/common/src/androidMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Mouse.kt similarity index 100% rename from examples/widgetsgallery/common/src/androidMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Mouse.kt rename to examples/widgets-gallery/common/src/androidMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Mouse.kt diff --git a/examples/widgetsgallery/common/src/androidMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Resources.kt b/examples/widgets-gallery/common/src/androidMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Resources.kt similarity index 94% rename from examples/widgetsgallery/common/src/androidMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Resources.kt rename to examples/widgets-gallery/common/src/androidMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Resources.kt index 7ea8c3f46a..d0225f3588 100644 --- a/examples/widgetsgallery/common/src/androidMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Resources.kt +++ b/examples/widgets-gallery/common/src/androidMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Resources.kt @@ -5,7 +5,7 @@ import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.imageResource import androidx.compose.ui.res.vectorResource -import org.jetbrains.compose.demo.widgets.R +import org.jetbrains.compose.demo.widgets.platform.R @Composable actual fun imageResource(res: String): ImageBitmap { diff --git a/examples/widgetsgallery/common/src/androidMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Scrollbar.kt b/examples/widgets-gallery/common/src/androidMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Scrollbar.kt similarity index 100% rename from examples/widgetsgallery/common/src/androidMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Scrollbar.kt rename to examples/widgets-gallery/common/src/androidMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Scrollbar.kt diff --git a/examples/widgetsgallery/common/src/androidMain/kotlin/org/jetbrains/compose/demo/widgets/platform/System.kt b/examples/widgets-gallery/common/src/androidMain/kotlin/org/jetbrains/compose/demo/widgets/platform/System.kt similarity index 100% rename from examples/widgetsgallery/common/src/androidMain/kotlin/org/jetbrains/compose/demo/widgets/platform/System.kt rename to examples/widgets-gallery/common/src/androidMain/kotlin/org/jetbrains/compose/demo/widgets/platform/System.kt diff --git a/examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/data/DemoDataProvider.kt b/examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/data/DemoDataProvider.kt similarity index 100% rename from examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/data/DemoDataProvider.kt rename to examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/data/DemoDataProvider.kt diff --git a/examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/data/model/Item.kt b/examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/data/model/Item.kt similarity index 100% rename from examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/data/model/Item.kt rename to examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/data/model/Item.kt diff --git a/examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/data/model/Tweet.kt b/examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/data/model/Tweet.kt similarity index 100% rename from examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/data/model/Tweet.kt rename to examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/data/model/Tweet.kt diff --git a/examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Mouse.kt b/examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Mouse.kt similarity index 100% rename from examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Mouse.kt rename to examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Mouse.kt diff --git a/examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Res.kt b/examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Res.kt similarity index 100% rename from examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Res.kt rename to examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Res.kt diff --git a/examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Resources.kt b/examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Resources.kt similarity index 100% rename from examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Resources.kt rename to examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Resources.kt diff --git a/examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Scrollbar.kt b/examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Scrollbar.kt similarity index 100% rename from examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Scrollbar.kt rename to examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Scrollbar.kt diff --git a/examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/platform/System.kt b/examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/platform/System.kt similarity index 100% rename from examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/platform/System.kt rename to examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/platform/System.kt diff --git a/examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/theme/Color.kt b/examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/theme/Color.kt similarity index 100% rename from examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/theme/Color.kt rename to examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/theme/Color.kt diff --git a/examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/theme/Shape.kt b/examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/theme/Shape.kt similarity index 100% rename from examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/theme/Shape.kt rename to examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/theme/Shape.kt diff --git a/examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/theme/Theme.kt b/examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/theme/Theme.kt similarity index 100% rename from examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/theme/Theme.kt rename to examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/theme/Theme.kt diff --git a/examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/theme/Type.kt b/examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/theme/Type.kt similarity index 100% rename from examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/theme/Type.kt rename to examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/theme/Type.kt diff --git a/examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/MainView.kt b/examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/MainView.kt similarity index 99% rename from examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/MainView.kt rename to examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/MainView.kt index 9b6105ff17..5a94d3a859 100644 --- a/examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/MainView.kt +++ b/examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/MainView.kt @@ -33,7 +33,7 @@ import org.jetbrains.compose.demo.widgets.ui.utils.withoutWidthConstraints @Composable fun MainView() { DisableSelection { - WidgetGalleryTheme() { + WidgetGalleryTheme { WidgetsPanel() } } diff --git a/examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/WidgetView.kt b/examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/WidgetView.kt similarity index 100% rename from examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/WidgetView.kt rename to examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/WidgetView.kt diff --git a/examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/WidgetsType.kt b/examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/WidgetsType.kt similarity index 100% rename from examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/WidgetsType.kt rename to examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/WidgetsType.kt diff --git a/examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/screens/AppBars.kt b/examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/screens/AppBars.kt similarity index 100% rename from examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/screens/AppBars.kt rename to examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/screens/AppBars.kt diff --git a/examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/screens/Buttons.kt b/examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/screens/Buttons.kt similarity index 100% rename from examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/screens/Buttons.kt rename to examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/screens/Buttons.kt diff --git a/examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/screens/Chips.kt b/examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/screens/Chips.kt similarity index 100% rename from examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/screens/Chips.kt rename to examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/screens/Chips.kt diff --git a/examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/screens/Loaders.kt b/examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/screens/Loaders.kt similarity index 100% rename from examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/screens/Loaders.kt rename to examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/screens/Loaders.kt diff --git a/examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/screens/SnackBars.kt b/examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/screens/SnackBars.kt similarity index 100% rename from examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/screens/SnackBars.kt rename to examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/screens/SnackBars.kt diff --git a/examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/screens/TextInputs.kt b/examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/screens/TextInputs.kt similarity index 100% rename from examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/screens/TextInputs.kt rename to examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/screens/TextInputs.kt diff --git a/examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/screens/Texts.kt b/examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/screens/Texts.kt similarity index 100% rename from examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/screens/Texts.kt rename to examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/screens/Texts.kt diff --git a/examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/screens/Toggles.kt b/examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/screens/Toggles.kt similarity index 100% rename from examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/screens/Toggles.kt rename to examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/screens/Toggles.kt diff --git a/examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/screens/UICards.kt b/examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/screens/UICards.kt similarity index 100% rename from examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/screens/UICards.kt rename to examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/screens/UICards.kt diff --git a/examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/utils/LayoutModifiers.kt b/examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/utils/LayoutModifiers.kt similarity index 100% rename from examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/utils/LayoutModifiers.kt rename to examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/utils/LayoutModifiers.kt diff --git a/examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/utils/ResizablePanel.kt b/examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/utils/ResizablePanel.kt similarity index 100% rename from examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/utils/ResizablePanel.kt rename to examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/utils/ResizablePanel.kt diff --git a/examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/utils/Text.kt b/examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/utils/Text.kt similarity index 100% rename from examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/utils/Text.kt rename to examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/utils/Text.kt diff --git a/examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/utils/VerticalSplittable.kt b/examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/utils/VerticalSplittable.kt similarity index 100% rename from examples/widgetsgallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/utils/VerticalSplittable.kt rename to examples/widgets-gallery/common/src/commonMain/kotlin/org/jetbrains/compose/demo/widgets/ui/utils/VerticalSplittable.kt diff --git a/examples/widgetsgallery/common/src/commonMain/resources/drawable-nodpi/food6.jpg b/examples/widgets-gallery/common/src/commonMain/resources/drawable-nodpi/food6.jpg similarity index 100% rename from examples/widgetsgallery/common/src/commonMain/resources/drawable-nodpi/food6.jpg rename to examples/widgets-gallery/common/src/commonMain/resources/drawable-nodpi/food6.jpg diff --git a/examples/widgetsgallery/common/src/commonMain/resources/drawable-nodpi/p1.jpeg b/examples/widgets-gallery/common/src/commonMain/resources/drawable-nodpi/p1.jpeg similarity index 100% rename from examples/widgetsgallery/common/src/commonMain/resources/drawable-nodpi/p1.jpeg rename to examples/widgets-gallery/common/src/commonMain/resources/drawable-nodpi/p1.jpeg diff --git a/examples/widgetsgallery/common/src/commonMain/resources/drawable-nodpi/p2.jpeg b/examples/widgets-gallery/common/src/commonMain/resources/drawable-nodpi/p2.jpeg similarity index 100% rename from examples/widgetsgallery/common/src/commonMain/resources/drawable-nodpi/p2.jpeg rename to examples/widgets-gallery/common/src/commonMain/resources/drawable-nodpi/p2.jpeg diff --git a/examples/widgetsgallery/common/src/commonMain/resources/drawable-nodpi/p3.jpeg b/examples/widgets-gallery/common/src/commonMain/resources/drawable-nodpi/p3.jpeg similarity index 100% rename from examples/widgetsgallery/common/src/commonMain/resources/drawable-nodpi/p3.jpeg rename to examples/widgets-gallery/common/src/commonMain/resources/drawable-nodpi/p3.jpeg diff --git a/examples/widgetsgallery/common/src/commonMain/resources/drawable-nodpi/p6.jpeg b/examples/widgets-gallery/common/src/commonMain/resources/drawable-nodpi/p6.jpeg similarity index 100% rename from examples/widgetsgallery/common/src/commonMain/resources/drawable-nodpi/p6.jpeg rename to examples/widgets-gallery/common/src/commonMain/resources/drawable-nodpi/p6.jpeg diff --git a/examples/widgetsgallery/common/src/commonMain/resources/drawable/ic_instagram.xml b/examples/widgets-gallery/common/src/commonMain/resources/drawable/ic_instagram.xml similarity index 100% rename from examples/widgetsgallery/common/src/commonMain/resources/drawable/ic_instagram.xml rename to examples/widgets-gallery/common/src/commonMain/resources/drawable/ic_instagram.xml diff --git a/examples/widgetsgallery/common/src/commonMain/resources/drawable/ic_send.xml b/examples/widgets-gallery/common/src/commonMain/resources/drawable/ic_send.xml similarity index 100% rename from examples/widgetsgallery/common/src/commonMain/resources/drawable/ic_send.xml rename to examples/widgets-gallery/common/src/commonMain/resources/drawable/ic_send.xml diff --git a/examples/widgetsgallery/common/src/commonMain/resources/drawable/ic_twitter.xml b/examples/widgets-gallery/common/src/commonMain/resources/drawable/ic_twitter.xml similarity index 100% rename from examples/widgetsgallery/common/src/commonMain/resources/drawable/ic_twitter.xml rename to examples/widgets-gallery/common/src/commonMain/resources/drawable/ic_twitter.xml diff --git a/examples/widgets-gallery/common/src/desktopMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Mouse.kt b/examples/widgets-gallery/common/src/desktopMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Mouse.kt new file mode 100644 index 0000000000..1ad6cb3067 --- /dev/null +++ b/examples/widgets-gallery/common/src/desktopMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Mouse.kt @@ -0,0 +1,19 @@ +package org.jetbrains.compose.demo.widgets.platform + +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.PointerIcon +import androidx.compose.ui.input.pointer.pointerIcon +import androidx.compose.ui.input.pointer.pointerMoveFilter +import java.awt.Cursor + +actual fun Modifier.pointerMoveFilter( + onEnter: () -> Boolean, + onExit: () -> Boolean, + onMove: (Offset) -> Boolean +): Modifier = this.pointerMoveFilter(onEnter = onEnter, onExit = onExit, onMove = onMove) + +@OptIn(ExperimentalComposeUiApi::class) +actual fun Modifier.cursorForHorizontalResize(): Modifier = + pointerIcon(PointerIcon(Cursor(Cursor.E_RESIZE_CURSOR))) diff --git a/examples/widgetsgallery/common/src/desktopMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Resources.kt b/examples/widgets-gallery/common/src/desktopMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Resources.kt similarity index 100% rename from examples/widgetsgallery/common/src/desktopMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Resources.kt rename to examples/widgets-gallery/common/src/desktopMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Resources.kt diff --git a/examples/widgetsgallery/common/src/desktopMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Scrollbar.kt b/examples/widgets-gallery/common/src/desktopMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Scrollbar.kt similarity index 92% rename from examples/widgetsgallery/common/src/desktopMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Scrollbar.kt rename to examples/widgets-gallery/common/src/desktopMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Scrollbar.kt index 6801a2ead9..b4ba358b2f 100644 --- a/examples/widgetsgallery/common/src/desktopMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Scrollbar.kt +++ b/examples/widgets-gallery/common/src/desktopMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Scrollbar.kt @@ -25,6 +25,6 @@ actual fun VerticalScrollbar( itemCount: Int, averageItemSize: Dp ) = androidx.compose.foundation.VerticalScrollbar( - rememberScrollbarAdapter(scrollState, itemCount, averageItemSize), + rememberScrollbarAdapter(scrollState), modifier ) \ No newline at end of file diff --git a/examples/widgets-gallery/common/src/desktopMain/kotlin/org/jetbrains/compose/demo/widgets/platform/System.kt b/examples/widgets-gallery/common/src/desktopMain/kotlin/org/jetbrains/compose/demo/widgets/platform/System.kt new file mode 100644 index 0000000000..c479cb8cf0 --- /dev/null +++ b/examples/widgets-gallery/common/src/desktopMain/kotlin/org/jetbrains/compose/demo/widgets/platform/System.kt @@ -0,0 +1,5 @@ +package org.jetbrains.compose.demo.widgets.platform + +import org.jetbrains.skiko.SystemTheme + +actual fun isSystemInDarkTheme(): Boolean = org.jetbrains.skiko.currentSystemTheme == SystemTheme.DARK \ No newline at end of file diff --git a/examples/widgetsgallery/desktop/build.gradle.kts b/examples/widgets-gallery/desktop/build.gradle.kts similarity index 100% rename from examples/widgetsgallery/desktop/build.gradle.kts rename to examples/widgets-gallery/desktop/build.gradle.kts diff --git a/examples/widgets-gallery/desktop/src/jvmMain/kotlin/org/jetbrains/compose/demo/widgets/main.kt b/examples/widgets-gallery/desktop/src/jvmMain/kotlin/org/jetbrains/compose/demo/widgets/main.kt new file mode 100644 index 0000000000..fe892927b2 --- /dev/null +++ b/examples/widgets-gallery/desktop/src/jvmMain/kotlin/org/jetbrains/compose/demo/widgets/main.kt @@ -0,0 +1,18 @@ +package org.jetbrains.compose.demo.widgets + +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.WindowSize +import androidx.compose.ui.window.WindowState +import androidx.compose.ui.window.application +import org.jetbrains.compose.demo.widgets.ui.MainView + +fun main() = application { + Window( + onCloseRequest = ::exitApplication, + title = "Widgets Gallery", + state = WindowState(size = WindowSize(800.dp, 600.dp)) + ) { + MainView() + } +} \ No newline at end of file diff --git a/examples/widgetsgallery/gradle.properties b/examples/widgets-gallery/gradle.properties similarity index 100% rename from examples/widgetsgallery/gradle.properties rename to examples/widgets-gallery/gradle.properties diff --git a/examples/widgetsgallery/gradle/wrapper/gradle-wrapper.jar b/examples/widgets-gallery/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from examples/widgetsgallery/gradle/wrapper/gradle-wrapper.jar rename to examples/widgets-gallery/gradle/wrapper/gradle-wrapper.jar diff --git a/examples/widgets-gallery/gradle/wrapper/gradle-wrapper.properties b/examples/widgets-gallery/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..05679dc3c1 --- /dev/null +++ b/examples/widgets-gallery/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/examples/widgetsgallery/gradlew b/examples/widgets-gallery/gradlew similarity index 100% rename from examples/widgetsgallery/gradlew rename to examples/widgets-gallery/gradlew diff --git a/examples/widgetsgallery/gradlew.bat b/examples/widgets-gallery/gradlew.bat similarity index 100% rename from examples/widgetsgallery/gradlew.bat rename to examples/widgets-gallery/gradlew.bat diff --git a/examples/widgetsgallery/settings.gradle.kts b/examples/widgets-gallery/settings.gradle.kts similarity index 100% rename from examples/widgetsgallery/settings.gradle.kts rename to examples/widgets-gallery/settings.gradle.kts diff --git a/examples/widgetsgallery/third_party/ComposeCookBook_LICENSE.txt b/examples/widgets-gallery/third_party/ComposeCookBook_LICENSE.txt similarity index 100% rename from examples/widgetsgallery/third_party/ComposeCookBook_LICENSE.txt rename to examples/widgets-gallery/third_party/ComposeCookBook_LICENSE.txt diff --git a/examples/widgetsgallery/common/src/androidMain/AndroidManifest.xml b/examples/widgetsgallery/common/src/androidMain/AndroidManifest.xml deleted file mode 100644 index 35be3787c7..0000000000 --- a/examples/widgetsgallery/common/src/androidMain/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/examples/widgetsgallery/common/src/desktopMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Mouse.kt b/examples/widgetsgallery/common/src/desktopMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Mouse.kt deleted file mode 100644 index bb64680993..0000000000 --- a/examples/widgetsgallery/common/src/desktopMain/kotlin/org/jetbrains/compose/demo/widgets/platform/Mouse.kt +++ /dev/null @@ -1,33 +0,0 @@ -package org.jetbrains.compose.demo.widgets.platform - -import androidx.compose.desktop.LocalAppWindow -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.composed -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.input.pointer.pointerMoveFilter -import java.awt.Cursor - -actual fun Modifier.pointerMoveFilter( - onEnter: () -> Boolean, - onExit: () -> Boolean, - onMove: (Offset) -> Boolean -): Modifier = this.pointerMoveFilter(onEnter = onEnter, onExit = onExit, onMove = onMove) - -actual fun Modifier.cursorForHorizontalResize(): Modifier = composed { - var isHover by remember { mutableStateOf(false) } - - if (isHover) { - LocalAppWindow.current.window.cursor = Cursor(Cursor.E_RESIZE_CURSOR) - } else { - LocalAppWindow.current.window.cursor = Cursor.getDefaultCursor() - } - - pointerMoveFilter( - onEnter = { isHover = true; true }, - onExit = { isHover = false; true } - ) -} \ No newline at end of file diff --git a/examples/widgetsgallery/common/src/desktopMain/kotlin/org/jetbrains/compose/demo/widgets/platform/System.kt b/examples/widgetsgallery/common/src/desktopMain/kotlin/org/jetbrains/compose/demo/widgets/platform/System.kt deleted file mode 100644 index 1e8abcd7af..0000000000 --- a/examples/widgetsgallery/common/src/desktopMain/kotlin/org/jetbrains/compose/demo/widgets/platform/System.kt +++ /dev/null @@ -1,4 +0,0 @@ -package org.jetbrains.compose.demo.widgets.platform - -// TODO: https://github.com/JetBrains/compose-jb/issues/169 -actual fun isSystemInDarkTheme(): Boolean = false \ No newline at end of file diff --git a/examples/widgetsgallery/desktop/src/jvmMain/kotlin/org/jetbrains/compose/demo/widgets/main.kt b/examples/widgetsgallery/desktop/src/jvmMain/kotlin/org/jetbrains/compose/demo/widgets/main.kt deleted file mode 100644 index df9f411116..0000000000 --- a/examples/widgetsgallery/desktop/src/jvmMain/kotlin/org/jetbrains/compose/demo/widgets/main.kt +++ /dev/null @@ -1,28 +0,0 @@ -package org.jetbrains.compose.demo.widgets - -import androidx.compose.desktop.Window -import androidx.compose.ui.unit.IntSize -import org.jetbrains.compose.demo.widgets.ui.MainView -import java.awt.Dimension -import java.awt.Toolkit -import javax.swing.SwingUtilities.invokeLater - -fun main() { - invokeLater { - Window( - title = "Widgets Gallery", - size = getPreferredWindowSize(600, 800), - ) { - MainView() - } - } -} - -private fun getPreferredWindowSize(desiredWidth: Int, desiredHeight: Int): IntSize { - val screenSize: Dimension = Toolkit.getDefaultToolkit().screenSize - val preferredWidth: Int = (screenSize.width * 0.8f).toInt() - val preferredHeight: Int = (screenSize.height * 0.8f).toInt() - val width: Int = if (desiredWidth < preferredWidth) desiredWidth else preferredWidth - val height: Int = if (desiredHeight < preferredHeight) desiredHeight else preferredHeight - return IntSize(width, height) -} diff --git a/examples/widgetsgallery/gradle/wrapper/gradle-wrapper.properties b/examples/widgetsgallery/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 7665b0fa93..0000000000 --- a/examples/widgetsgallery/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-all.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt index a0c990d3ff..981b0647a5 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt @@ -16,6 +16,7 @@ import org.jetbrains.compose.desktop.DesktopExtension import org.jetbrains.compose.desktop.application.internal.configureApplicationImpl import org.jetbrains.compose.desktop.application.internal.currentTarget import org.jetbrains.compose.desktop.preview.internal.initializePreview +import org.jetbrains.compose.internal.checkAndWarnAboutComposeWithSerialization import org.jetbrains.compose.web.internal.initializeWeb import org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler import org.jetbrains.kotlin.gradle.tasks.KotlinCompile @@ -106,6 +107,8 @@ class ComposePlugin : Plugin { useIR = true } } + + project.checkAndWarnAboutComposeWithSerialization() } object Dependencies { @@ -116,6 +119,7 @@ class ComposePlugin : Plugin { val runtime get() = composeDependency("org.jetbrains.compose.runtime:runtime") val ui get() = composeDependency("org.jetbrains.compose.ui:ui") val uiTooling get() = composeDependency("org.jetbrains.compose.ui:ui-tooling") + val preview get() = composeDependency("org.jetbrains.compose.ui:ui-tooling-preview") val materialIconsExtended get() = composeDependency("org.jetbrains.compose.material:material-icons-extended") val web: WebDependencies get() = if (ComposeBuildConfig.isComposeWithWeb) WebDependencies @@ -132,22 +136,6 @@ class ComposePlugin : Plugin { val macos_x64 = composeDependency("org.jetbrains.compose.desktop:desktop-jvm-macos-x64") val macos_arm64 = composeDependency("org.jetbrains.compose.desktop:desktop-jvm-macos-arm64") - @Deprecated( - "compose.desktop.linux is deprecated, use compose.desktop.linux_x64 instead", - replaceWith = ReplaceWith("linux_x64") - ) - val linux = linux_x64 - @Deprecated( - "compose.desktop.windows is deprecated, use compose.desktop.windows_x64 instead", - replaceWith = ReplaceWith("windows_x64") - ) - val windows = windows_x64 - @Deprecated( - "compose.desktop.macos is deprecated, use compose.desktop.macos_x64 instead", - replaceWith = ReplaceWith("macos_x64") - ) - val macos = macos_x64 - val currentOs by lazy { composeDependency("org.jetbrains.compose.desktop:desktop-jvm-${currentTarget.id}") } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/NativeDistributions.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/NativeDistributions.kt index ff8c88b52f..f0bb01dad5 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/NativeDistributions.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/NativeDistributions.kt @@ -13,7 +13,7 @@ import java.util.* import javax.inject.Inject internal val DEFAULT_RUNTIME_MODULES = arrayOf( - "java.base", "java.desktop", "java.logging" + "java.base", "java.desktop", "java.logging", "jdk.crypto.ec" ) open class NativeDistributions @Inject constructor( @@ -24,16 +24,8 @@ open class NativeDistributions @Inject constructor( var description: String? = null var copyright: String? = null var vendor: String? = null - @Deprecated( - "version is deprecated, use packageVersion instead", - replaceWith = ReplaceWith("packageVersion") - ) - var version: String? - get() = packageVersion - set(value) { - packageVersion = value - } var packageVersion: String? = null + val appResourcesRootDir: DirectoryProperty = objects.directoryProperty() val outputBaseDir: DirectoryProperty = objects.directoryProperty().apply { set(layout.buildDirectory.dir("compose/binaries")) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/PlatformSettings.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/PlatformSettings.kt index ceb8609fb2..88257b3a75 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/PlatformSettings.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/PlatformSettings.kt @@ -41,6 +41,15 @@ open class MacOSPlatformSettings @Inject constructor(objects: ObjectFactory): Pl fun notarization(fn: Action) { fn.execute(notarization) } + + internal val infoPlistSettings = InfoPlistSettings() + fun infoPlist(fn: Action) { + fn.execute(infoPlistSettings) + } +} + +open class InfoPlistSettings { + var extraKeysRawXml: String? = null } open class LinuxPlatformSettings @Inject constructor(objects: ObjectFactory): PlatformSettings(objects) { diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ComposeSystemProperties.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ComposeSystemProperties.kt new file mode 100644 index 0000000000..b26c0519d8 --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ComposeSystemProperties.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.desktop.application.internal + +internal const val APP_RESOURCES_DIR = "compose.application.resources.dir" +internal const val SKIKO_LIBRARY_PATH = "skiko.library.path" +internal const val CONFIGURE_SWING_GLOBALS = "compose.application.configure.swing.globals" diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/InfoPlistBuilder.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/InfoPlistBuilder.kt index 9727216e51..85aad602f8 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/InfoPlistBuilder.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/InfoPlistBuilder.kt @@ -5,10 +5,11 @@ package org.jetbrains.compose.desktop.application.internal +import org.jetbrains.compose.desktop.application.dsl.InfoPlistSettings import java.io.File import kotlin.reflect.KProperty -internal class InfoPlistBuilder { +internal class InfoPlistBuilder(private val extraPlistKeysRawXml: String?) { private val values = LinkedHashMap() operator fun get(key: InfoPlistKey): String? = values[key] @@ -27,6 +28,7 @@ internal class InfoPlistBuilder { appendLine(" ${k.name}") appendLine(" $v") } + extraPlistKeysRawXml?.let { appendLine(it) } appendLine(" ") appendLine("") } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/cliArgUtils.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/cliArgUtils.kt index e8c3c385e1..0a67b6306a 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/cliArgUtils.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/cliArgUtils.kt @@ -30,6 +30,10 @@ internal fun MutableCollection.cliArg( cliArg(name, value.orNull, fn) } +internal fun MutableCollection.javaOption(value: String) { + cliArg("--java-options", "'$value'") +} + private fun defaultToString(): (T) -> String = { val asString = when (it) { diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureApplication.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureApplication.kt index d269e4aef1..90e3f6a6c2 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureApplication.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureApplication.kt @@ -9,7 +9,6 @@ import org.gradle.api.* import org.gradle.api.file.Directory import org.gradle.api.file.DuplicatesStrategy import org.gradle.api.file.FileCollection -import org.gradle.api.plugins.JavaPluginConvention import org.gradle.api.provider.Provider import org.gradle.api.tasks.* import org.gradle.jvm.tasks.Jar @@ -17,24 +16,22 @@ import org.jetbrains.compose.desktop.application.dsl.Application import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.compose.desktop.application.internal.validation.validatePackageVersions import org.jetbrains.compose.desktop.application.tasks.* -import org.jetbrains.compose.desktop.preview.internal.configureConfigureDesktopPreviewTask -import org.jetbrains.compose.desktop.preview.tasks.AbstractConfigureDesktopPreviewTask -import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.compose.internal.* import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType import java.io.File import java.util.* -private val defaultJvmArgs = listOf("-Dcompose.application.configure.swing.globals=true") +private val defaultJvmArgs = listOf("-D$CONFIGURE_SWING_GLOBALS=true") // todo: multiple launchers // todo: file associations // todo: use workers fun configureApplicationImpl(project: Project, app: Application) { if (app._isDefaultConfigurationEnabled) { - if (project.plugins.hasPlugin("org.jetbrains.kotlin.multiplatform")) { + if (project.plugins.hasPlugin(KOTLIN_MPP_PLUGIN_ID)) { project.configureFromMppPlugin(app) - } else if (project.plugins.hasPlugin("org.jetbrains.kotlin.jvm")) { - val mainSourceSet = project.convention.getPlugin(JavaPluginConvention::class.java).sourceSets.getByName("main") + } else if (project.plugins.hasPlugin(KOTLIN_JVM_PLUGIN_ID)) { + val mainSourceSet = project.javaSourceSets.getByName("main") app.from(mainSourceSet) } } @@ -44,9 +41,8 @@ fun configureApplicationImpl(project: Project, app: Application) { } internal fun Project.configureFromMppPlugin(mainApplication: Application) { - val kotlinExt = extensions.getByType(KotlinMultiplatformExtension::class.java) var isJvmTargetConfigured = false - kotlinExt.targets.all { target -> + mppExt.targets.all { target -> if (target.platformType == KotlinPlatformType.jvm) { if (!isJvmTargetConfigured) { mainApplication.from(target) @@ -84,6 +80,20 @@ internal fun Project.configurePackagingTasks(apps: Collection) { } } + val prepareAppResources = tasks.composeTask( + taskName("prepareAppResources", app) + ) { + val appResourcesRootDir = app.nativeDistributions.appResourcesRootDir + if (appResourcesRootDir.isPresent) { + from(appResourcesRootDir.dir("common")) + from(appResourcesRootDir.dir(currentOS.id)) + from(appResourcesRootDir.dir(currentTarget.id)) + } + + val destDir = project.layout.buildDirectory.dir("compose/tmp/${app.name}/resources") + into(destDir) + } + val createRuntimeImage = tasks.composeTask( taskName("createRuntimeImage", app) ) { @@ -99,7 +109,11 @@ internal fun Project.configurePackagingTasks(apps: Collection) { taskName("createDistributable", app), args = listOf(TargetFormat.AppImage) ) { - configurePackagingTask(app, createRuntimeImage = createRuntimeImage) + configurePackagingTask( + app, + createRuntimeImage = createRuntimeImage, + prepareAppResources = prepareAppResources + ) } val packageFormats = app.nativeDistributions.targetFormats.map { targetFormat -> @@ -114,7 +128,11 @@ internal fun Project.configurePackagingTasks(apps: Collection) { // in some cases there are failures with JDK 15. // See [AbstractJPackageTask.patchInfoPlistIfNeeded] if (currentOS != OS.MacOS) { - configurePackagingTask(app, createRuntimeImage = createRuntimeImage) + configurePackagingTask( + app, + createRuntimeImage = createRuntimeImage, + prepareAppResources = prepareAppResources + ) } else { configurePackagingTask(app, createAppImage = createDistributable) } @@ -157,11 +175,7 @@ internal fun Project.configurePackagingTasks(apps: Collection) { ) val run = project.tasks.composeTask(taskName("run", app)) { - configureRunTask(app) - } - - val configureDesktopPreviewTask = project.tasks.composeTask("configureDesktopPreview") { - configureConfigureDesktopPreviewTask(app) + configureRunTask(app, prepareAppResources = prepareAppResources) } } } @@ -169,7 +183,8 @@ internal fun Project.configurePackagingTasks(apps: Collection) { internal fun AbstractJPackageTask.configurePackagingTask( app: Application, createAppImage: TaskProvider? = null, - createRuntimeImage: TaskProvider? = null + createRuntimeImage: TaskProvider? = null, + prepareAppResources: TaskProvider? = null ) { enabled = targetFormat.isCompatibleWithCurrentOS @@ -183,6 +198,12 @@ internal fun AbstractJPackageTask.configurePackagingTask( runtimeImage.set(createRuntimeImage.flatMap { it.destinationDir }) } + prepareAppResources?.let { prepareResources -> + dependsOn(prepareResources) + val resourcesDir = project.layout.dir(prepareResources.map { it.destinationDir }) + appResourcesDir.set(resourcesDir) + } + configurePlatformSettings(app) app.nativeDistributions.let { executables -> @@ -275,6 +296,7 @@ internal fun AbstractJPackageTask.configurePlatformSettings(app: Application) { provider { mac.dockName } ) nonValidatedMacBundleID.set(provider { mac.bundleID }) + macExtraPlistKeysRawXml.set(provider { mac.infoPlistSettings.extraKeysRawXml }) nonValidatedMacSigningSettings = app.nativeDistributions.macOS.signing iconFile.set(mac.iconFile.orElse(DefaultIcons.forMac(project))) installationPath.set(mac.installationPath) @@ -283,10 +305,20 @@ internal fun AbstractJPackageTask.configurePlatformSettings(app: Application) { } } -private fun JavaExec.configureRunTask(app: Application) { +private fun JavaExec.configureRunTask( + app: Application, + prepareAppResources: TaskProvider +) { + dependsOn(prepareAppResources) + mainClass.set(provider { app.mainClass }) executable(javaExecutable(app.javaHomeOrDefault())) - jvmArgs = defaultJvmArgs + app.jvmArgs + jvmArgs = arrayListOf().apply { + addAll(defaultJvmArgs) + addAll(app.jvmArgs) + val appResourcesDir = prepareAppResources.get().destinationDir + add("-D$APP_RESOURCES_DIR=${appResourcesDir.absolutePath}") + } args = app.args val cp = project.objects.fileCollection() diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/files/MacJarSignFileCopyingProcessor.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/files/MacJarSignFileCopyingProcessor.kt index 52d87052c8..31f7999ad6 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/files/MacJarSignFileCopyingProcessor.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/files/MacJarSignFileCopyingProcessor.kt @@ -62,4 +62,4 @@ internal class MacJarSignFileCopyingProcessor( } private val String.isDylibPath - get() = endsWith(".dylib") \ No newline at end of file + get() = endsWith(".dylib") || endsWith(".jnilib") diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/validation/ValidatedMacOSSigningSettings.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/validation/ValidatedMacOSSigningSettings.kt index 0f2c01b7b1..3ca82fb6a1 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/validation/ValidatedMacOSSigningSettings.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/validation/ValidatedMacOSSigningSettings.kt @@ -5,6 +5,7 @@ package org.jetbrains.compose.desktop.application.internal.validation +import org.gradle.api.Project import org.gradle.api.provider.Provider import org.jetbrains.compose.desktop.application.dsl.MacOSSigningSettings import org.jetbrains.compose.desktop.application.internal.ComposeProperties @@ -31,7 +32,8 @@ internal data class ValidatedMacOSSigningSettings( } internal fun MacOSSigningSettings.validate( - bundleIDProvider: Provider + bundleIDProvider: Provider, + project: Project ): ValidatedMacOSSigningSettings { check(currentOS == OS.MacOS) { ERR_WRONG_OS } @@ -41,10 +43,13 @@ internal fun MacOSSigningSettings.validate( ?: error(ERR_UNKNOWN_PREFIX) val signIdentity = this.identity.orNull ?: error(ERR_UNKNOWN_SIGN_ID) - val keychainFile = this.keychain.orNull?.let { File(it) } - if (keychainFile != null) { - check(keychainFile.exists()) { - "$ERR_PREFIX keychain is not an existing file: ${keychainFile.absolutePath}" + val keychainPath = this.keychain.orNull + val keychainFile = + listOf(project.file(keychainPath), project.rootProject.file(keychainPath)) + .firstOrNull { it.exists() } + if (keychainPath != null) { + check(keychainFile != null && keychainFile.exists()) { + "$ERR_PREFIX could not find the specified keychain: $keychainPath" } } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt index 6def4e1637..eb80b42a5c 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt @@ -22,10 +22,8 @@ import org.jetbrains.compose.desktop.application.internal.files.* import org.jetbrains.compose.desktop.application.internal.files.MacJarSignFileCopyingProcessor import org.jetbrains.compose.desktop.application.internal.files.fileHash import org.jetbrains.compose.desktop.application.internal.files.transformJar -import org.jetbrains.compose.desktop.application.internal.validation.ValidatedMacOSSigningSettings import org.jetbrains.compose.desktop.application.internal.validation.validate import java.io.* -import java.nio.file.Files import java.util.* import java.util.zip.ZipEntry import javax.inject.Inject @@ -169,6 +167,10 @@ abstract class AbstractJPackageTask @Inject constructor( @get:Optional internal val nonValidatedMacBundleID: Property = objects.nullableProperty() + @get:Input + @get:Optional + internal val macExtraPlistKeysRawXml: Property = objects.nullableProperty() + @get:Optional @get:Nested internal var nonValidatedMacSigningSettings: MacOSSigningSettings? = null @@ -176,7 +178,8 @@ abstract class AbstractJPackageTask @Inject constructor( private val macSigner: MacSigner? by lazy { val nonValidatedSettings = nonValidatedMacSigningSettings if (currentOS == OS.MacOS && nonValidatedSettings?.sign?.get() == true) { - val validatedSettings = nonValidatedSettings.validate(nonValidatedMacBundleID) + val validatedSettings = + nonValidatedSettings.validate(nonValidatedMacBundleID, project) MacSigner(validatedSettings, runExternalTool) } else null } @@ -185,7 +188,7 @@ abstract class AbstractJPackageTask @Inject constructor( protected val signDir: Provider = project.layout.buildDirectory.dir("compose/tmp/sign") @get:LocalState - protected val resourcesDir: Provider = project.layout.buildDirectory.dir("compose/tmp/resources") + protected val jpackageResources: Provider = project.layout.buildDirectory.dir("compose/tmp/resources") @get:LocalState protected val skikoDir: Provider = project.layout.buildDirectory.dir("compose/tmp/skiko") @@ -195,6 +198,31 @@ abstract class AbstractJPackageTask @Inject constructor( it.dir("libs") } + @get:Internal + private val packagedResourcesDir: Provider = libsDir.map { + it.dir("resources") + } + + @get:Internal + val appResourcesDir: DirectoryProperty = objects.directoryProperty() + + /** + * Gradle runtime verification fails, + * if InputDirectory is not null, but a directory does not exist. + * The directory might not exist, because prepareAppResources task + * does not create output directory if there are no resources. + * + * To work around this, appResourcesDir is used as a real property, + * but it is annotated as @Internal, so it ignored during inputs checking. + * This property is used only for inputs checking. + * It returns appResourcesDir value if the underlying directory exists. + */ + @Suppress("unused") + @get:InputDirectory + @get:Optional + internal val appResourcesDirInputDirHackForVerification: Provider + get() = appResourcesDir.map { it.takeIf { it.asFile.exists() } } + @get:Internal private val libsMappingFile: Provider = workingDir.map { it.file("libs-mapping.txt") @@ -204,11 +232,22 @@ abstract class AbstractJPackageTask @Inject constructor( private val libsMapping = FilesMapping() override fun makeArgs(tmpDir: File): MutableList = super.makeArgs(tmpDir).apply { + fun appDir(vararg pathParts: String): String { + /** For windows we need to pass '\\' to jpackage file, each '\' need to be escaped. + Otherwise '$APPDIR\resources' is passed to jpackage, + and '\r' is treated as a special character at run time. + */ + val separator = if (currentTarget.os == OS.Windows) "\\\\" else "/" + return listOf("${'$'}APPDIR", *pathParts).joinToString(separator) { it } + } + if (targetFormat == TargetFormat.AppImage || appImage.orNull == null) { // Args, that can only be used, when creating an app image or an installer w/o --app-image parameter cliArg("--input", libsDir) cliArg("--runtime-image", runtimeImage) - cliArg("--resource-dir", resourcesDir) + cliArg("--resource-dir", jpackageResources) + + javaOption("-D$APP_RESOURCES_DIR=${appDir(packagedResourcesDir.ioFile.name)}") val mappedJar = libsMapping[launcherMainJar.ioFile]?.singleOrNull() ?: error("Main jar was not processed correctly: ${launcherMainJar.ioFile}") @@ -225,12 +264,12 @@ abstract class AbstractJPackageTask @Inject constructor( cliArg("--arguments", "'$it'") } launcherJvmArgs.orNull?.forEach { - cliArg("--java-options", "'$it'") + javaOption(it) } - cliArg("--java-options", "'-Dskiko.library.path=${'$'}APPDIR'") + javaOption("-D$SKIKO_LIBRARY_PATH=${appDir()}") if (currentOS == OS.MacOS) { macDockName.orNull?.let { dockName -> - cliArg("--java-options", "'-Xdock:name=$dockName'") + javaOption("-Xdock:name=$dockName") } } } @@ -358,12 +397,29 @@ abstract class AbstractJPackageTask @Inject constructor( } } - fileOperations.delete(resourcesDir) - fileOperations.mkdir(resourcesDir) + // todo: incremental copy + val destResourcesDir = packagedResourcesDir.ioFile + fileOperations.delete(destResourcesDir) + fileOperations.mkdir(destResourcesDir) + val appResourcesDir = appResourcesDir.ioFileOrNull + if (appResourcesDir != null) { + for (file in appResourcesDir.walk()) { + val relPath = file.relativeTo(appResourcesDir).path + val destFile = destResourcesDir.resolve(relPath) + if (file.isDirectory) { + fileOperations.mkdir(destFile) + } else { + file.copyTo(destFile) + } + } + } + + fileOperations.delete(jpackageResources) + fileOperations.mkdir(jpackageResources) if (currentOS == OS.MacOS) { - InfoPlistBuilder() + InfoPlistBuilder(macExtraPlistKeysRawXml.orNull) .also { setInfoPlistValues(it) } - .writeToFile(resourcesDir.ioFile.resolve("Info.plist")) + .writeToFile(jpackageResources.ioFile.resolve("Info.plist")) } } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/preview/internal/configurePreview.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/preview/internal/configurePreview.kt index bed2520cbb..876669ef32 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/preview/internal/configurePreview.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/preview/internal/configurePreview.kt @@ -1,19 +1,36 @@ package org.jetbrains.compose.desktop.preview.internal import org.gradle.api.Project -import org.jetbrains.compose.desktop.application.dsl.Application -import org.jetbrains.compose.desktop.application.internal.javaHomeOrDefault -import org.jetbrains.compose.desktop.application.internal.provider +import org.jetbrains.compose.desktop.application.dsl.ConfigurationSource import org.jetbrains.compose.desktop.preview.tasks.AbstractConfigureDesktopPreviewTask +import org.jetbrains.compose.internal.* +import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType +import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget fun Project.initializePreview() { + plugins.withId(KOTLIN_MPP_PLUGIN_ID) { + mppExt.targets.all { target -> + if (target.platformType == KotlinPlatformType.jvm) { + val config = ConfigurationSource.KotlinMppTarget(target as KotlinJvmTarget) + registerConfigurePreviewTask(project, config, targetName = target.name) + } + } + } + plugins.withId(KOTLIN_JVM_PLUGIN_ID) { + val config = ConfigurationSource.GradleSourceSet(project.javaSourceSets.getByName("main")) + registerConfigurePreviewTask(project, config) + } } -internal fun AbstractConfigureDesktopPreviewTask.configureConfigureDesktopPreviewTask(app: Application) { - app._configurationSource?.let { configSource -> - dependsOn(configSource.jarTaskName) - previewClasspath = configSource.runtimeClasspath(project) - javaHome.set(provider { app.javaHomeOrDefault() }) - jvmArgs.set(provider { app.jvmArgs }) +private fun registerConfigurePreviewTask(project: Project, config: ConfigurationSource, targetName: String = "") { + project.tasks.register( + previewTaskName(targetName), + AbstractConfigureDesktopPreviewTask::class.java + ) { previewTask -> + previewTask.dependsOn(config.jarTask(project)) + previewTask.previewClasspath = config.runtimeClasspath(project) } -} \ No newline at end of file +} + +private fun previewTaskName(targetName: String) = + "configureDesktopPreview${targetName.capitalize()}" \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/preview/tasks/AbstractConfigureDesktopPreviewTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/preview/tasks/AbstractConfigureDesktopPreviewTask.kt index 4333b447da..6eab501556 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/preview/tasks/AbstractConfigureDesktopPreviewTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/preview/tasks/AbstractConfigureDesktopPreviewTask.kt @@ -7,6 +7,7 @@ import org.gradle.api.provider.Property import org.gradle.api.provider.Provider import org.gradle.api.tasks.* import org.jetbrains.compose.ComposeBuildConfig +import org.jetbrains.compose.desktop.application.internal.currentTarget import org.jetbrains.compose.desktop.application.internal.javaExecutable import org.jetbrains.compose.desktop.application.internal.notNullProperty import org.jetbrains.compose.desktop.tasks.AbstractComposeDesktopTask @@ -35,6 +36,11 @@ abstract class AbstractConfigureDesktopPreviewTask : AbstractComposeDesktopTask( internal val idePort: Provider = project.providers.gradleProperty("compose.desktop.preview.ide.port") + @get:InputFiles + internal val uiTooling = project.configurations.detachedConfiguration( + project.dependencies.create("org.jetbrains.compose.ui:ui-tooling-desktop:${ComposeBuildConfig.composeVersion}") + ).apply { isTransitive = false } + @get:InputFiles internal val hostClasspath = project.configurations.detachedConfiguration( project.dependencies.create("org.jetbrains.compose:preview-rpc:${ComposeBuildConfig.composeVersion}") @@ -44,9 +50,13 @@ abstract class AbstractConfigureDesktopPreviewTask : AbstractComposeDesktopTask( fun run() { val hostConfig = PreviewHostConfig( javaExecutable = javaExecutable(javaHome.get()), - hostClasspath = hostClasspath.files.pathString() + hostClasspath = hostClasspath.files.asSequence().pathString() ) - val previewClasspathString = previewClasspath.files.pathString() + val previewClasspathString = + (previewClasspath.files.asSequence() + + uiTooling.files.asSequence() + + tryGetSkikoRuntimeFilesIfNeeded().asSequence() + ).pathString() val gradleLogger = logger val previewLogger = GradlePreviewLoggerAdapter(gradleLogger) @@ -65,7 +75,40 @@ abstract class AbstractConfigureDesktopPreviewTask : AbstractComposeDesktopTask( } } - private fun Collection.pathString(): String = + private fun tryGetSkikoRuntimeFilesIfNeeded(): Collection { + try { + var hasSkikoJvm = false + var hasSkikoJvmRuntime = false + var skikoVersion: String? = null + for (file in previewClasspath.files) { + if (file.name.endsWith(".jar")) { + if (file.name.startsWith("skiko-jvm-runtime-")) { + hasSkikoJvmRuntime = true + continue + } else if (file.name.startsWith("skiko-jvm-")) { + hasSkikoJvm = true + skikoVersion = file.name + .removePrefix("skiko-jvm-") + .removeSuffix(".jar") + } + } + } + if (hasSkikoJvmRuntime) return emptyList() + + if (hasSkikoJvm && skikoVersion != null && skikoVersion.isNotBlank()) { + val skikoRuntimeConfig = project.configurations.detachedConfiguration( + project.dependencies.create("org.jetbrains.skiko:skiko-jvm-runtime-${currentTarget.id}:$skikoVersion") + ).apply { isTransitive = false } + return skikoRuntimeConfig.files + } + } catch (e: Exception) { + // OK + } + + return emptyList() + } + + private fun Sequence.pathString(): String = joinToString(File.pathSeparator) { it.absolutePath } private class GradlePreviewLoggerAdapter( diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/WarnAboutComposeWithSerialization.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/WarnAboutComposeWithSerialization.kt new file mode 100644 index 0000000000..8ca6c7f2e9 --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/WarnAboutComposeWithSerialization.kt @@ -0,0 +1,18 @@ +package org.jetbrains.compose.internal + +import org.gradle.api.Project + +internal fun Project.checkAndWarnAboutComposeWithSerialization() { + project.plugins.withId("org.jetbrains.kotlin.plugin.serialization") { + val warningMessage = """ + + >>> COMPOSE WARNING + >>> Project `${project.name}` has `compose` and `kotlinx.serialization` plugins applied! + >>> Consider using these plugins in separate modules to avoid compilation errors + >>> Check more details here: https://github.com/JetBrains/compose-jb/issues/738 + + """.trimIndent() + + logger.warn(warningMessage) + } +} diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/constants.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/constants.kt new file mode 100644 index 0000000000..1c8bd90320 --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/constants.kt @@ -0,0 +1,9 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.internal + +internal const val KOTLIN_MPP_PLUGIN_ID = "org.jetbrains.kotlin.multiplatform" +internal const val KOTLIN_JVM_PLUGIN_ID = "org.jetbrains.kotlin.jvm" \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/projectExtensions.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/projectExtensions.kt index 9a09e04fc4..7df1d4f07f 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/projectExtensions.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/projectExtensions.kt @@ -6,6 +6,10 @@ package org.jetbrains.compose.internal import org.gradle.api.Project +import org.gradle.api.plugins.JavaPluginConvention +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.api.tasks.SourceSetContainer +import org.gradle.util.GradleVersion import org.jetbrains.compose.ComposeExtension import org.jetbrains.compose.web.WebExtension import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension @@ -16,5 +20,15 @@ internal val Project.composeExt: ComposeExtension? internal val Project.webExt: WebExtension? get() = composeExt?.extensions?.findByType(WebExtension::class.java) -internal val Project.mppExt: KotlinMultiplatformExtension? - get() = extensions.findByType(KotlinMultiplatformExtension::class.java) \ No newline at end of file +internal val Project.mppExt: KotlinMultiplatformExtension + get() = mppExtOrNull ?: error("Could not find KotlinMultiplatformExtension ($project)") + +internal val Project.mppExtOrNull: KotlinMultiplatformExtension? + get() = extensions.findByType(KotlinMultiplatformExtension::class.java) + +internal val Project.javaSourceSets: SourceSetContainer + get() = if (GradleVersion.current() < GradleVersion.version("7.1")) { + convention.getPlugin(JavaPluginConvention::class.java).sourceSets + } else { + extensions.getByType(JavaPluginExtension::class.java).sourceSets + } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/web/WebExtension.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/web/WebExtension.kt index 079fac26ec..4f91e2b112 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/web/WebExtension.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/web/WebExtension.kt @@ -8,6 +8,7 @@ package org.jetbrains.compose.web import org.gradle.api.Project import org.gradle.api.plugins.ExtensionAware import org.jetbrains.compose.internal.mppExt +import org.jetbrains.compose.internal.mppExtOrNull import org.jetbrains.kotlin.gradle.plugin.KotlinTarget import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTarget @@ -45,7 +46,7 @@ abstract class WebExtension : ExtensionAware { } private fun defaultJsTargetsToConfigure(project: Project): Set { - val mppTargets = project.mppExt?.targets?.asMap?.values ?: emptySet() + val mppTargets = project.mppExtOrNull?.targets?.asMap?.values ?: emptySet() val jsIRTargets = mppTargets.filterIsInstanceTo(LinkedHashSet()) return if (jsIRTargets.size > 1) { diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/gradle/DesktopApplicationTest.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/gradle/DesktopApplicationTest.kt index b1f5132b2a..b8c7fa7b7e 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/gradle/DesktopApplicationTest.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/gradle/DesktopApplicationTest.kt @@ -315,4 +315,15 @@ class DesktopApplicationTest : GradlePluginTestBase() { } } } + + @Test + fun resources() = with(testProject(TestProjects.resources)) { + gradle(":run").build().checks { check -> + check.taskOutcome(":run", TaskOutcome.SUCCESS) + } + + gradle(":runDistributable").build().checks { check -> + check.taskOutcome(":runDistributable", TaskOutcome.SUCCESS) + } + } } diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/gradle/GradlePluginTest.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/gradle/GradlePluginTest.kt index 42e0e75cca..ee70c98400 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/gradle/GradlePluginTest.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/gradle/GradlePluginTest.kt @@ -6,11 +6,20 @@ package org.jetbrains.compose.gradle import org.gradle.testkit.runner.TaskOutcome +import org.jetbrains.compose.desktop.ui.tooling.preview.rpc.PreviewLogger +import org.jetbrains.compose.desktop.ui.tooling.preview.rpc.RemoteConnection +import org.jetbrains.compose.desktop.ui.tooling.preview.rpc.receiveConfigFromGradle import org.jetbrains.compose.test.GradlePluginTestBase import org.jetbrains.compose.test.TestKotlinVersion import org.jetbrains.compose.test.TestProjects import org.jetbrains.compose.test.checks import org.junit.jupiter.api.Test +import java.net.ServerSocket +import java.net.Socket +import java.net.SocketTimeoutException +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger +import kotlin.concurrent.thread class GradlePluginTest : GradlePluginTestBase() { @Test @@ -25,4 +34,103 @@ class GradlePluginTest : GradlePluginTestBase() { check.taskOutcome(":compileKotlinJs", TaskOutcome.SUCCESS) } } + + @Test + fun configurePreview() { + val isAlive = AtomicBoolean(true) + val receivedConfigCount = AtomicInteger(0) + val port = AtomicInteger(-1) + val connectionThread = thread { + val serverSocket = ServerSocket(0).apply { + soTimeout = 10_000 + } + port.set(serverSocket.localPort) + try { + while (isAlive.get()) { + try { + val socket = serverSocket.accept() + val connection = RemoteConnectionImpl(socket, TestPreviewLogger("SERVER")) + val previewConfig = connection.receiveConfigFromGradle() + if (previewConfig != null) { + receivedConfigCount.incrementAndGet() + } + } catch (e: Exception) { + if (!isAlive.get()) break + + if (e !is SocketTimeoutException) { + e.printStackTrace() + throw e + } + } + } + + } finally { + serverSocket.close() + } + } + + val startTimeNs = System.nanoTime() + while (port.get() <= 0) { + val elapsedTimeNs = System.nanoTime() - startTimeNs + val elapsedTimeMs = elapsedTimeNs / 1_000_000L + if (elapsedTimeMs > 10_000) { + error("Server socket initialization timeout!") + } + Thread.sleep(200) + } + + try { + testConfigureDesktopPreivewImpl(port.get()) + } finally { + isAlive.set(false) + connectionThread.interrupt() + connectionThread.join(5000) + } + + val expectedReceivedConfigCount = 2 + val actualReceivedConfigCount = receivedConfigCount.get() + check(actualReceivedConfigCount == 2) { + "Expected to receive $expectedReceivedConfigCount preview configs, got $actualReceivedConfigCount" + } + } + + private fun testConfigureDesktopPreivewImpl(port: Int) { + check(port > 0) { "Invalid port: $port" } + with(testProject(TestProjects.jvmPreview)) { + val portProperty = "-Pcompose.desktop.preview.ide.port=$port" + val previewTargetProperty = "-Pcompose.desktop.preview.target=PreviewKt.ExamplePreview" + val jvmTask = ":jvm:configureDesktopPreview" + gradle(jvmTask, portProperty, previewTargetProperty) + .build() + .checks { check -> + check.taskOutcome(jvmTask, TaskOutcome.SUCCESS) + } + + val mppTask = ":mpp:configureDesktopPreviewDesktop" + gradle(mppTask, portProperty, previewTargetProperty) + .build() + .checks { check -> + check.taskOutcome(mppTask, TaskOutcome.SUCCESS) + } + } + } + + private class TestPreviewLogger(private val prefix: String) : PreviewLogger() { + override val isEnabled: Boolean + get() = true + + override fun log(s: String) { + println("$prefix: $s") + } + } + + private fun RemoteConnectionImpl( + socket: Socket, logger: PreviewLogger + ): RemoteConnection { + val connectionClass = Class.forName("org.jetbrains.compose.desktop.ui.tooling.preview.rpc.RemoteConnectionImpl") + val constructor = connectionClass.constructors.first { + it.parameterCount == 3 + } + return constructor.newInstance(socket, logger, {}) as RemoteConnection + } } \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/GradleTestNameGenerator.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/GradleTestNameGenerator.kt index 5b011d127e..817aa82ed8 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/GradleTestNameGenerator.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/GradleTestNameGenerator.kt @@ -8,7 +8,7 @@ package org.jetbrains.compose.test import org.junit.jupiter.api.DisplayNameGenerator class GradleTestNameGenerator : DisplayNameGenerator.Standard() { - private val gradleVersion = "[Gradle '${TestProperties.gradleVersionForTests}']" + private val gradleVersion = TestProperties.gradleVersionForTests?.let { "[Gradle '$it']" } ?: "" override fun generateDisplayNameForClass(testClass: Class<*>?): String = super.generateDisplayNameForClass(testClass) + gradleVersion diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/TestKotlinVersion.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/TestKotlinVersion.kt index 8694aa6738..ceac76b168 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/TestKotlinVersion.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/TestKotlinVersion.kt @@ -8,6 +8,6 @@ package org.jetbrains.compose.test @Suppress("EnumEntryName") enum class TestKotlinVersion(val versionString: String) { // __KOTLIN_COMPOSE_VERSION__ - Default("1.5.10"), + Default("1.5.21"), V1_5_20("1.5.20") } \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/TestProjects.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/TestProjects.kt index e831eb1ba2..7fe5184de1 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/TestProjects.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/TestProjects.kt @@ -17,5 +17,7 @@ object TestProjects { const val defaultArgs = "application/defaultArgs" const val defaultArgsOverride = "application/defaultArgsOverride" const val unpackSkiko = "application/unpackSkiko" + const val resources = "application/resources" const val jsMpp = "misc/jsMpp" + const val jvmPreview = "misc/jvmPreview" } \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/TestProperties.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/TestProperties.kt index e3cff07842..4610bc3686 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/TestProperties.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/TestProperties.kt @@ -9,6 +9,6 @@ object TestProperties { val composeVersion: String get() = System.getProperty("compose.plugin.version")!! - val gradleVersionForTests: String - get() = System.getProperty("gradle.version.for.tests")!! + val gradleVersionForTests: String? + get() = System.getProperty("gradle.version.for.tests") } diff --git a/gradle-plugins/compose/src/test/test-projects/application/macOptions/Expected-Info.plist b/gradle-plugins/compose/src/test/test-projects/application/macOptions/Expected-Info.plist index 1f914cb8b0..c315c93bbe 100644 --- a/gradle-plugins/compose/src/test/test-projects/application/macOptions/Expected-Info.plist +++ b/gradle-plugins/compose/src/test/test-projects/application/macOptions/Expected-Info.plist @@ -32,5 +32,17 @@ true NSHighResolutionCapable true + + CFBundleURLTypes + + + CFBundleURLName + Exameple URL + CFBundleURLSchemes + + exampleUrl + + + diff --git a/gradle-plugins/compose/src/test/test-projects/application/macOptions/build.gradle b/gradle-plugins/compose/src/test/test-projects/application/macOptions/build.gradle index 0ceff664ba..d1c9437a9d 100644 --- a/gradle-plugins/compose/src/test/test-projects/application/macOptions/build.gradle +++ b/gradle-plugins/compose/src/test/test-projects/application/macOptions/build.gradle @@ -18,6 +18,19 @@ dependencies { implementation compose.desktop.currentOs } +def extraInfoPlistKeys = """ + CFBundleURLTypes + + + CFBundleURLName + Exameple URL + CFBundleURLSchemes + + exampleUrl + + + """ + compose.desktop { application { mainClass = "MainKt" @@ -25,6 +38,9 @@ compose.desktop { packageName = "TestPackage" macOS { dockName = "CustomDockName" + infoPlist { + extraKeysRawXml = extraInfoPlistKeys + } } } } diff --git a/gradle-plugins/compose/src/test/test-projects/application/macSign/build.gradle b/gradle-plugins/compose/src/test/test-projects/application/macSign/build.gradle index 2e13dd5860..92fce3f186 100644 --- a/gradle-plugins/compose/src/test/test-projects/application/macSign/build.gradle +++ b/gradle-plugins/compose/src/test/test-projects/application/macSign/build.gradle @@ -29,7 +29,7 @@ compose.desktop { signing { sign.set(true) identity.set("Compose Test") - keychain.set(project.file("compose.test.keychain").absolutePath) + keychain.set("compose.test.keychain") } } } diff --git a/gradle-plugins/compose/src/test/test-projects/application/resources/build.gradle b/gradle-plugins/compose/src/test/test-projects/application/resources/build.gradle new file mode 100644 index 0000000000..e4e8a01b88 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/resources/build.gradle @@ -0,0 +1,31 @@ +import org.jetbrains.compose.desktop.application.dsl.TargetFormat + +plugins { + id "org.jetbrains.kotlin.jvm" + id "org.jetbrains.compose" +} + +repositories { + google() + mavenCentral() + maven { + url "https://maven.pkg.jetbrains.space/public/p/compose/dev" + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib" + implementation compose.desktop.currentOs +} + +compose.desktop { + application { + mainClass = "MainKt" + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageVersion = "1.0.0" + + appResourcesRootDir.set(project.layout.projectDirectory.dir("resources")) + } + } +} diff --git a/gradle-plugins/compose/src/test/test-projects/application/resources/resources/common/common-resource.txt b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/common/common-resource.txt new file mode 100644 index 0000000000..09a3c6a27c --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/common/common-resource.txt @@ -0,0 +1 @@ +common resource \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/application/resources/resources/linux-arm64/target-specific-resource.txt b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/linux-arm64/target-specific-resource.txt new file mode 100644 index 0000000000..d5e56d78d6 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/linux-arm64/target-specific-resource.txt @@ -0,0 +1 @@ +linux-arm64 only resource \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/application/resources/resources/linux-x64/target-specific-resource.txt b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/linux-x64/target-specific-resource.txt new file mode 100644 index 0000000000..2a1dba9d8e --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/linux-x64/target-specific-resource.txt @@ -0,0 +1 @@ +linux-x64 only resource \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/application/resources/resources/linux/os-specific-resource.txt b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/linux/os-specific-resource.txt new file mode 100644 index 0000000000..a31ea557e1 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/linux/os-specific-resource.txt @@ -0,0 +1 @@ +linux only resource \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/application/resources/resources/macos-arm64/target-specific-resource.txt b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/macos-arm64/target-specific-resource.txt new file mode 100644 index 0000000000..120b561cc1 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/macos-arm64/target-specific-resource.txt @@ -0,0 +1 @@ +macos-arm64 only resource \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/application/resources/resources/macos-x64/target-specific-resource.txt b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/macos-x64/target-specific-resource.txt new file mode 100644 index 0000000000..386a1efa07 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/macos-x64/target-specific-resource.txt @@ -0,0 +1 @@ +macos-x64 only resource \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/application/resources/resources/macos/os-specific-resource.txt b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/macos/os-specific-resource.txt new file mode 100644 index 0000000000..676fbe9de2 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/macos/os-specific-resource.txt @@ -0,0 +1 @@ +macos only resource \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/application/resources/resources/windows-arm64/target-specific-resource.txt b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/windows-arm64/target-specific-resource.txt new file mode 100644 index 0000000000..b76ca2430e --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/windows-arm64/target-specific-resource.txt @@ -0,0 +1 @@ +windows-arm64 only resource \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/application/resources/resources/windows-x64/target-specific-resource.txt b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/windows-x64/target-specific-resource.txt new file mode 100644 index 0000000000..1de4b09cd9 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/windows-x64/target-specific-resource.txt @@ -0,0 +1 @@ +windows-x64 only resource \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/application/resources/resources/windows/os-specific-resource.txt b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/windows/os-specific-resource.txt new file mode 100644 index 0000000000..62794baeaf --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/resources/resources/windows/os-specific-resource.txt @@ -0,0 +1 @@ +windows only resource \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/application/resources/settings.gradle b/gradle-plugins/compose/src/test/test-projects/application/resources/settings.gradle new file mode 100644 index 0000000000..8d7ab43b40 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/resources/settings.gradle @@ -0,0 +1,11 @@ +pluginManagement { + plugins { + id 'org.jetbrains.kotlin.jvm' version 'KOTLIN_VERSION_PLACEHOLDER' + id 'org.jetbrains.compose' version 'COMPOSE_VERSION_PLACEHOLDER' + } + repositories { + mavenLocal() + gradlePluginPortal() + } +} +rootProject.name = "simple" \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/application/resources/src/main/kotlin/main.kt b/gradle-plugins/compose/src/test/test-projects/application/resources/src/main/kotlin/main.kt new file mode 100644 index 0000000000..a69630f8ce --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/resources/src/main/kotlin/main.kt @@ -0,0 +1,50 @@ +import java.io.File + +fun main() { + checkContent("common-resource.txt", "common resource") + checkContent("os-specific-resource.txt", "$currentOS only resource") + checkContent("target-specific-resource.txt", "$currentTarget only resource") +} + +fun checkContent(actualFileName: String, expectedContent: String) { + val file = composeAppResource(actualFileName) + val actualContent = file.readText().trim() + check(actualContent == expectedContent) { + """ + Actual: '$actualContent' + Expected: '$expectedContent' + """.trimIndent() + } +} + +fun composeAppResource(path: String): File = + composeAppResourceDir.resolve(path) + +private val composeAppResourceDir: File by lazy { + val property = "compose.application.resources.dir" + val path = System.getProperty(property) ?: error("System property '$property' is not set!") + File(path) +} + +internal val currentTarget by lazy { + "$currentOS-$currentArch" +} + +internal val currentOS: String by lazy { + val os = System.getProperty("os.name") + when { + os.equals("Mac OS X", ignoreCase = true) -> "macos" + os.startsWith("Win", ignoreCase = true) -> "windows" + os.startsWith("Linux", ignoreCase = true) -> "linux" + else -> error("Unknown OS name: $os") + } +} + +internal val currentArch by lazy { + val osArch = System.getProperty("os.arch") + when (osArch) { + "x86_64", "amd64" -> "x64" + "aarch64" -> "arm64" + else -> error("Unsupported OS arch: $osArch") + } +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/build.gradle b/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/build.gradle new file mode 100644 index 0000000000..e5a360f3a0 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/build.gradle @@ -0,0 +1,10 @@ +subprojects { + repositories { + mavenLocal() + mavenCentral() + maven { + url 'https://maven.pkg.jetbrains.space/public/p/compose/dev' + } + google() + } +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/jvm/build.gradle b/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/jvm/build.gradle new file mode 100644 index 0000000000..75721fd9c7 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/jvm/build.gradle @@ -0,0 +1,10 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' + id 'org.jetbrains.compose' +} + +dependencies { + implementation 'org.jetbrains.kotlin:kotlin-stdlib' + implementation compose.uiTooling + implementation compose.desktop.currentOs +} \ No newline at end of file diff --git a/idea-plugin/examples/desktop-project/src/main/kotlin/preview.kt b/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/jvm/src/main/kotlin/preview.kt similarity index 100% rename from idea-plugin/examples/desktop-project/src/main/kotlin/preview.kt rename to gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/jvm/src/main/kotlin/preview.kt diff --git a/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/mpp/build.gradle b/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/mpp/build.gradle new file mode 100644 index 0000000000..884f35288f --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/mpp/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'org.jetbrains.kotlin.multiplatform' + id 'org.jetbrains.compose' +} + +kotlin { + jvm('desktop') {} + + sourceSets { + commonMain.dependencies { + api compose.runtime + api compose.foundation + api compose.material + api compose.uiTooling + } + desktopMain.dependencies { + implementation compose.desktop.currentOs + } + } +} diff --git a/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/mpp/src/commonMain/kotlin/composable.kt b/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/mpp/src/commonMain/kotlin/composable.kt new file mode 100644 index 0000000000..370b617b84 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/mpp/src/commonMain/kotlin/composable.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +import androidx.compose.material.Text +import androidx.compose.material.Button +import androidx.compose.runtime.* + +@Composable +fun ExampleComposable() { + var text by remember { mutableStateOf("Hello, World!") } + + Button(onClick = { + text = "Hello, $platformName!" + }) { + Text(text) + } +} + +expect val platformName: String \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/mpp/src/desktopMain/kotlin/preview.kt b/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/mpp/src/desktopMain/kotlin/preview.kt new file mode 100644 index 0000000000..d601d04369 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/mpp/src/desktopMain/kotlin/preview.kt @@ -0,0 +1,11 @@ +import androidx.compose.runtime.Composable +import androidx.compose.desktop.ui.tooling.preview.Preview + +@Preview +@Composable +fun ExamplePreview() { + ExampleComposable() +} + +actual val platformName: String + get() = "Desktop" \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/settings.gradle b/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/settings.gradle new file mode 100644 index 0000000000..2db23b4ddc --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/settings.gradle @@ -0,0 +1,16 @@ +pluginManagement { + plugins { + id 'org.jetbrains.kotlin.multiplatform' version 'KOTLIN_VERSION_PLACEHOLDER' + id 'org.jetbrains.kotlin.jvm' version 'KOTLIN_VERSION_PLACEHOLDER' + id 'org.jetbrains.compose' version 'COMPOSE_VERSION_PLACEHOLDER' + } + repositories { + mavenLocal() + gradlePluginPortal() + maven { + url 'https://maven.pkg.jetbrains.space/public/p/compose/dev' + } + } +} +rootProject.name = 'jvmPreview' +include(':jvm', ':mpp') \ No newline at end of file diff --git a/gradle-plugins/gradle.properties b/gradle-plugins/gradle.properties index 456860be35..c1419cc009 100644 --- a/gradle-plugins/gradle.properties +++ b/gradle-plugins/gradle.properties @@ -6,7 +6,7 @@ kotlin.code.style=official # unless overridden by COMPOSE_GRADLE_PLUGIN_COMPOSE_VERSION env var. # # __LATEST_COMPOSE_RELEASE_VERSION__ -compose.version=0.4.0 +compose.version=0.5.0-build262 compose.with.web=false # A version of Gradle plugin, that will be published, diff --git a/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/PreviewListener.kt b/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/PreviewListener.kt new file mode 100644 index 0000000000..cce79d1f65 --- /dev/null +++ b/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/PreviewListener.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.desktop.ui.tooling.preview.rpc + +interface PreviewListener { + fun onNewBuildRequest() + fun onFinishedBuild(success: Boolean) + fun onNewRenderRequest(previewRequest: FrameRequest) + fun onRenderedFrame(frame: RenderedFrame) + fun onIncompatibleProtocolVersions(versionServer: Int, versionClient: Int) +} + +open class PreviewListenerBase : PreviewListener { + override fun onNewBuildRequest() {} + override fun onFinishedBuild(success: Boolean) {} + + override fun onNewRenderRequest(previewRequest: FrameRequest) {} + override fun onRenderedFrame(frame: RenderedFrame) {} + + override fun onIncompatibleProtocolVersions(versionServer: Int, versionClient: Int) {} +} + +class CompositePreviewListener : PreviewListener { + private val listeners = arrayListOf() + + override fun onNewBuildRequest() { + forEachListener { it.onNewBuildRequest() } + } + + override fun onFinishedBuild(success: Boolean) { + forEachListener { it.onFinishedBuild(success) } + } + + override fun onNewRenderRequest(previewRequest: FrameRequest) { + forEachListener { it.onNewRenderRequest(previewRequest) } + } + + override fun onRenderedFrame(frame: RenderedFrame) { + forEachListener { it.onRenderedFrame(frame) } + } + + override fun onIncompatibleProtocolVersions(versionServer: Int, versionClient: Int) { + forEachListener { it.onIncompatibleProtocolVersions(versionServer, versionClient) } + } + + @Synchronized + fun addListener(listener: PreviewListener) { + listeners.add(listener) + } + + @Synchronized + private fun forEachListener(fn: (PreviewListener) -> Unit) { + listeners.forEach(fn) + } +} diff --git a/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/PreviewManager.kt b/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/PreviewManager.kt index 7ceeae5aeb..80f8d8ccba 100644 --- a/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/PreviewManager.kt +++ b/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/PreviewManager.kt @@ -29,6 +29,7 @@ data class FrameConfig(val width: Int, val height: Int, val scale: Double?) { } data class FrameRequest( + val id: Long, val composableFqName: String, val frameConfig: FrameConfig ) @@ -47,7 +48,9 @@ private data class RunningPreview( get() = connection.isAlive && process.isAlive } -class PreviewManagerImpl(private val onNewFrame: (RenderedFrame) -> Unit) : PreviewManager { +class PreviewManagerImpl( + private val previewListener: PreviewListener = PreviewListenerBase() +) : PreviewManager { private val log = PrintStreamLogger("SERVER") private val previewSocket = newServerSocket() private val gradleCallbackSocket = newServerSocket() @@ -59,8 +62,9 @@ class PreviewManagerImpl(private val onNewFrame: (RenderedFrame) -> Unit) : Prev private val previewClasspath = AtomicReference(null) private val previewFqName = AtomicReference(null) private val previewFrameConfig = AtomicReference(null) - private val frameRequest = AtomicReference(null) - private val shouldRequestFrame = AtomicBoolean(false) + private val inProcessRequest = AtomicReference(null) + private val processedRequest = AtomicReference(null) + private val userRequestCount = AtomicLong(0) private val runningPreview = AtomicReference(null) private val threads = arrayListOf() @@ -84,7 +88,7 @@ class PreviewManagerImpl(private val onNewFrame: (RenderedFrame) -> Unit) : Prev if (previewConfig != null && runningPreview?.isAlive != true) { val process = startPreviewProcess(previewConfig) val connection = tryAcceptConnection(previewSocket, "PREVIEW") - connection?.receiveAttach { + connection?.receiveAttach(listener = previewListener) { this.runningPreview.set(RunningPreview(connection, process)) } } @@ -97,14 +101,12 @@ class PreviewManagerImpl(private val onNewFrame: (RenderedFrame) -> Unit) : Prev val frameConfig = previewFrameConfig.get() if (classpath != null && frameConfig != null && fqName != null) { - val request = FrameRequest(fqName, frameConfig) - if (shouldRequestFrame.get() && frameRequest.get() == null) { - if (shouldRequestFrame.compareAndSet(true, false)) { - if (frameRequest.compareAndSet(null, request)) { - sendPreviewRequest(classpath, request) - } else { - shouldRequestFrame.compareAndSet(false, true) - } + val request = FrameRequest(userRequestCount.get(), fqName, frameConfig) + val prevRequest = processedRequest.get() + if (inProcessRequest.get() == null && request != prevRequest) { + if (inProcessRequest.compareAndSet(null, request)) { + previewListener.onNewRenderRequest(request) + sendPreviewRequest(classpath, request) } } } @@ -114,10 +116,11 @@ class PreviewManagerImpl(private val onNewFrame: (RenderedFrame) -> Unit) : Prev private val receivePreviewResponseThread = repeatWhileAliveThread("receivePreviewResponse") { withLivePreviewConnection { receiveFrame { renderedFrame -> - frameRequest.get()?.let { request -> - frameRequest.compareAndSet(request, null) + inProcessRequest.get()?.let { request -> + processedRequest.set(request) + inProcessRequest.compareAndSet(request, null) } - onNewFrame(renderedFrame) + previewListener.onRenderedFrame(renderedFrame) } } } @@ -125,13 +128,14 @@ class PreviewManagerImpl(private val onNewFrame: (RenderedFrame) -> Unit) : Prev private val gradleCallbackThread = repeatWhileAliveThread("gradleCallback") { tryAcceptConnection(gradleCallbackSocket, "GRADLE_CALLBACK")?.let { connection -> while (isAlive.get() && connection.isAlive) { - connection.receiveConfigFromGradle( - onPreviewClasspath = { previewClasspath.set(it) }, - onPreviewHostConfig = { previewHostConfig.set(it) }, - onPreviewFqName = { previewFqName.set(it) } - ) - shouldRequestFrame.set(true) - sendPreviewRequestThread.interrupt() + val config = connection.receiveConfigFromGradle() + if (config != null) { + previewClasspath.set(config.previewClasspath) + previewFqName.set(config.previewFqName) + previewHostConfig.set(config.previewHostConfig) + userRequestCount.incrementAndGet() + sendPreviewRequestThread.interrupt() + } } } } @@ -191,7 +195,6 @@ class PreviewManagerImpl(private val onNewFrame: (RenderedFrame) -> Unit) : Prev override fun updateFrameConfig(frameConfig: FrameConfig) { previewFrameConfig.set(frameConfig) - shouldRequestFrame.set(true) sendPreviewRequestThread.interrupt() } diff --git a/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/RemoteConnection.kt b/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/RemoteConnection.kt index 249510f5da..692cbc75be 100644 --- a/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/RemoteConnection.kt +++ b/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/RemoteConnection.kt @@ -39,6 +39,7 @@ abstract class RemoteConnection : AutoCloseable { } } +// Constructor is also used in GradlePluginTest#configurePreview via reflection internal class RemoteConnectionImpl( private val socket: Socket, private val log: PreviewLogger, diff --git a/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/RemotePreviewHost.kt b/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/RemotePreviewHost.kt index f11f144fd0..cd02d330ae 100644 --- a/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/RemotePreviewHost.kt +++ b/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/RemotePreviewHost.kt @@ -126,7 +126,7 @@ internal class PreviewHost(private val log: PreviewLogger, connection: RemoteCon val possibleCandidates = previewFacade.methods.filter { it.name == "render" } throw RuntimeException("Could not find method '$signature'. Possible candidates: \n${possibleCandidates.joinToString("\n") { "* ${it}" }}", e) } - val (fqName, frameConfig) = request + val (id, fqName, frameConfig) = request val scaledWidth = frameConfig.scaledWidth val scaledHeight = frameConfig.scaledHeight val scale = frameConfig.scale diff --git a/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/RenderedFrame.kt b/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/RenderedFrame.kt index 838c7f9cfe..93c04aa5c4 100644 --- a/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/RenderedFrame.kt +++ b/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/RenderedFrame.kt @@ -5,6 +5,11 @@ package org.jetbrains.compose.desktop.ui.tooling.preview.rpc +import java.awt.Dimension +import java.awt.image.BufferedImage +import java.io.ByteArrayInputStream +import javax.imageio.ImageIO + data class RenderedFrame( val bytes: ByteArray, val width: Int, @@ -29,4 +34,10 @@ data class RenderedFrame( result = 31 * result + height return result } + + val image: BufferedImage + get() = ByteArrayInputStream(bytes).use { ImageIO.read(it) } + + val dimension: Dimension + get() = Dimension(width, height) } \ No newline at end of file diff --git a/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/commands.kt b/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/commands.kt index deba6ab06b..bd12ec1828 100644 --- a/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/commands.kt +++ b/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/commands.kt @@ -9,12 +9,19 @@ import java.net.URLDecoder import java.net.URLEncoder internal fun RemoteConnection.sendAttach() { - sendCommand(Command.Type.ATTACH) + sendCommand(Command.Type.ATTACH, PROTOCOL_VERSION.toString()) } -internal fun RemoteConnection.receiveAttach(fn: () -> Unit) { - receiveCommand { (type, _) -> +internal fun RemoteConnection.receiveAttach( + listener: PreviewListener? = null, + fn: () -> Unit +) { + receiveCommand { (type, args) -> if (type == Command.Type.ATTACH) { + val version = args.firstOrNull()?.toIntOrNull() ?: 0 + if (PROTOCOL_VERSION != version) { + listener?.onIncompatibleProtocolVersions(PROTOCOL_VERSION, version) + } fn() } } @@ -50,29 +57,46 @@ fun RemoteConnection.sendConfigFromGradle( sendUtf8StringData(previewFqName) } -internal fun RemoteConnection.receiveConfigFromGradle( - onPreviewClasspath: (String) -> Unit, - onPreviewFqName: (String) -> Unit, - onPreviewHostConfig: (PreviewHostConfig) -> Unit -) { +data class ConfigFromGradle( + val previewClasspath: String, + val previewFqName: String, + val previewHostConfig: PreviewHostConfig +) + +fun RemoteConnection.receiveConfigFromGradle(): ConfigFromGradle? { + var previewClasspath: String? = null + var previewFqName: String? = null + var previewHostConfig: PreviewHostConfig? = null + receiveCommand { (type, args) -> - when (type) { - Command.Type.PREVIEW_CLASSPATH -> - receiveUtf8StringData { onPreviewClasspath(it) } - Command.Type.PREVIEW_FQ_NAME -> - receiveUtf8StringData { onPreviewFqName(it) } - Command.Type.PREVIEW_CONFIG -> { - val javaExecutable = URLDecoder.decode(args[0], Charsets.UTF_8) - receiveUtf8StringData { hostClasspath -> - val config = PreviewHostConfig(javaExecutable = javaExecutable, hostClasspath = hostClasspath) - onPreviewHostConfig(config) - } - } - else -> { - // todo - } + check(type == Command.Type.PREVIEW_CONFIG) { + "Expected ${Command.Type.PREVIEW_CONFIG}, got $type" + } + val javaExecutable = URLDecoder.decode(args[0], Charsets.UTF_8) + receiveUtf8StringData { hostClasspath -> + previewHostConfig = PreviewHostConfig(javaExecutable = javaExecutable, hostClasspath = hostClasspath) } } + receiveCommand { (type, _) -> + check(type == Command.Type.PREVIEW_CLASSPATH) { + "Expected ${Command.Type.PREVIEW_CLASSPATH}, got $type" + } + receiveUtf8StringData { previewClasspath = it } + } + receiveCommand { (type, _) -> + check(type == Command.Type.PREVIEW_FQ_NAME) { + "Expected ${Command.Type.PREVIEW_FQ_NAME}, got $type" + } + receiveUtf8StringData { previewFqName = it } + } + + return if (previewClasspath != null && previewFqName != null && previewHostConfig != null) { + ConfigFromGradle( + previewClasspath = previewClasspath!!, + previewFqName = previewFqName!!, + previewHostConfig = previewHostConfig!! + ) + } else null } internal fun RemoteConnection.sendPreviewRequest( @@ -81,9 +105,9 @@ internal fun RemoteConnection.sendPreviewRequest( ) { sendCommand(Command.Type.PREVIEW_CLASSPATH) sendData(previewClasspath.toByteArray(Charsets.UTF_8)) - val (fqName, frameConfig) = request + val (id, fqName, frameConfig) = request val (w, h, scale) = frameConfig - val args = arrayListOf(fqName, w.toString(), h.toString()) + val args = arrayListOf(fqName, id.toString(), w.toString(), h.toString()) if (scale != null) { val scaleLong = java.lang.Double.doubleToRawLongBits(scale) args.add(scaleLong.toString()) @@ -102,15 +126,17 @@ internal fun RemoteConnection.receivePreviewRequest( } Command.Type.FRAME_REQUEST -> { val fqName = args.getOrNull(0) - val w = args.getOrNull(1)?.toIntOrNull() - val h = args.getOrNull(2)?.toIntOrNull() - val scale = args.getOrNull(3)?.toLongOrNull()?.let { java.lang.Double.longBitsToDouble(it) } + val id = args.getOrNull(1)?.toLongOrNull() + val w = args.getOrNull(2)?.toIntOrNull() + val h = args.getOrNull(3)?.toIntOrNull() + val scale = args.getOrNull(4)?.toLongOrNull()?.let { java.lang.Double.longBitsToDouble(it) } if ( fqName != null && fqName.isNotEmpty() + && id != null && w != null && w > 0 && h != null && h > 0 ) { - onFrameRequest(FrameRequest(fqName, FrameConfig(width = w, height = h, scale = scale))) + onFrameRequest(FrameRequest(id, fqName, FrameConfig(width = w, height = h, scale = scale))) } } else -> { diff --git a/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/protocolVersion.kt b/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/protocolVersion.kt new file mode 100644 index 0000000000..c91b9b2852 --- /dev/null +++ b/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/protocolVersion.kt @@ -0,0 +1,8 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.desktop.ui.tooling.preview.rpc + +const val PROTOCOL_VERSION = 1 \ No newline at end of file diff --git a/idea-plugin/build.gradle.kts b/idea-plugin/build.gradle.kts index 48c8a4da20..742d9e7e58 100644 --- a/idea-plugin/build.gradle.kts +++ b/idea-plugin/build.gradle.kts @@ -3,14 +3,14 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompile plugins { id("java") id("org.jetbrains.kotlin.jvm") version "1.5.10" - id("org.jetbrains.intellij") version "0.7.2" + id("org.jetbrains.intellij") version "1.1.2" id("org.jetbrains.changelog") version "1.1.2" } -fun properties(key: String) = project.findProperty(key).toString() +val projectProperties = ProjectProperties(project) group = "org.jetbrains.compose.desktop.ide" -version = properties("deploy.version") +version = projectProperties.deployVersion repositories { mavenCentral() @@ -21,15 +21,17 @@ dependencies { } intellij { - pluginName = "Compose Multiplatform IDE Support" - type = properties("platform.type") - version = properties("platform.version") - downloadSources = properties("platform.download.sources").toBoolean() + pluginName.set("Compose Multiplatform IDE Support") + type.set(projectProperties.platformType) + version.set(projectProperties.platformVersion) + downloadSources.set(projectProperties.platformDownloadSources) - setPlugins( - "java", - "com.intellij.gradle", - "org.jetbrains.kotlin" + plugins.set( + listOf( + "java", + "com.intellij.gradle", + "org.jetbrains.kotlin" + ) ) } @@ -49,17 +51,32 @@ tasks { } publishPlugin { - token(System.getenv("IDE_PLUGIN_PUBLISH_TOKEN")) - channels(properties("plugin.channels")) + token.set(System.getenv("IDE_PLUGIN_PUBLISH_TOKEN")) + channels.set(projectProperties.pluginChannels) } patchPluginXml { - sinceBuild(properties("plugin.since.build")) - untilBuild(properties("plugin.until.build")) + sinceBuild.set(projectProperties.pluginSinceBuild) + untilBuild.set(projectProperties.pluginUntilBuild) } runPluginVerifier { - ideVersions(properties("plugin.verifier.ide.versions")) - downloadDirectory("${project.buildDir}/pluginVerifier/ides") + ideVersions.set(projectProperties.pluginVerifierIdeVersions) } } + +class ProjectProperties(private val project: Project) { + val deployVersion get() = stringProperty("deploy.version") + val platformType get() = stringProperty("platform.type") + val platformVersion get() = stringProperty("platform.version") + val platformDownloadSources get() = stringProperty("platform.download.sources").toBoolean() + val pluginChannels get() = listProperty("plugin.channels") + val pluginSinceBuild get() = stringProperty("plugin.since.build") + val pluginUntilBuild get() = stringProperty("plugin.until.build") + val pluginVerifierIdeVersions get() = listProperty("plugin.verifier.ide.versions") + + private fun stringProperty(key: String): String = + project.findProperty(key)!!.toString() + private fun listProperty(key: String): List = + stringProperty(key).split(",").map { it.trim() } +} diff --git a/idea-plugin/examples/desktop-project/README.md b/idea-plugin/examples/desktop-project/README.md deleted file mode 100644 index 64d40addab..0000000000 --- a/idea-plugin/examples/desktop-project/README.md +++ /dev/null @@ -1,5 +0,0 @@ -1. Run from `idea-plugin`: -``` -./gradlew runIde -``` -2. Open `idea-plugin/examples/desktop-project` with the test IDE. \ No newline at end of file diff --git a/idea-plugin/examples/desktop-project/build.gradle.kts b/idea-plugin/examples/desktop-project/build.gradle.kts deleted file mode 100644 index 7c929bd8ca..0000000000 --- a/idea-plugin/examples/desktop-project/build.gradle.kts +++ /dev/null @@ -1,24 +0,0 @@ -import org.jetbrains.compose.compose - -plugins { - // __KOTLIN_COMPOSE_VERSION__ - kotlin("jvm") version "1.5.10" - // __LATEST_COMPOSE_RELEASE_VERSION__ - id("org.jetbrains.compose") version "0.5.0-build229" -} - -repositories { - mavenCentral() - maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") -} - -dependencies { - implementation(compose.uiTooling) - implementation(compose.desktop.currentOs) -} - -compose.desktop { - application { - mainClass = "MainKt" - } -} diff --git a/idea-plugin/examples/desktop-project/gradle.properties b/idea-plugin/examples/desktop-project/gradle.properties deleted file mode 100644 index 87ec412e67..0000000000 --- a/idea-plugin/examples/desktop-project/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 -kotlin.code.style=official -#org.gradle.unsafe.configuration-cache=true -#org.gradle.unsafe.configuration-cache-problems=warn diff --git a/idea-plugin/examples/desktop-project/settings.gradle.kts b/idea-plugin/examples/desktop-project/settings.gradle.kts deleted file mode 100644 index 781ae9381e..0000000000 --- a/idea-plugin/examples/desktop-project/settings.gradle.kts +++ /dev/null @@ -1,6 +0,0 @@ -pluginManagement { - repositories { - gradlePluginPortal() - maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") - } -} \ No newline at end of file diff --git a/idea-plugin/examples/desktop-project/.gitignore b/idea-plugin/examples/simple-preview-example/.gitignore similarity index 100% rename from idea-plugin/examples/desktop-project/.gitignore rename to idea-plugin/examples/simple-preview-example/.gitignore diff --git a/idea-plugin/examples/simple-preview-example/README.md b/idea-plugin/examples/simple-preview-example/README.md new file mode 100644 index 0000000000..e8bca9ebb7 --- /dev/null +++ b/idea-plugin/examples/simple-preview-example/README.md @@ -0,0 +1,5 @@ +1. Run from `idea-plugin`: +``` +./gradlew runIde +``` +2. Open the project with the test IDE. \ No newline at end of file diff --git a/idea-plugin/examples/simple-preview-example/build.gradle.kts b/idea-plugin/examples/simple-preview-example/build.gradle.kts new file mode 100644 index 0000000000..2f642cb193 --- /dev/null +++ b/idea-plugin/examples/simple-preview-example/build.gradle.kts @@ -0,0 +1,22 @@ +buildscript { + repositories { + mavenLocal() + mavenCentral() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + } + dependencies { + // __LATEST_COMPOSE_RELEASE_VERSION__ + classpath("org.jetbrains.compose:compose-gradle-plugin:1.0.0-alpha1") + // __KOTLIN_COMPOSE_VERSION__ + classpath(kotlin("gradle-plugin", version = "1.5.21")) + } +} + +subprojects { + repositories { + mavenLocal() + mavenCentral() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + google() + } +} diff --git a/idea-plugin/examples/simple-preview-example/gradle.properties b/idea-plugin/examples/simple-preview-example/gradle.properties new file mode 100644 index 0000000000..2c8ed06fb6 --- /dev/null +++ b/idea-plugin/examples/simple-preview-example/gradle.properties @@ -0,0 +1,3 @@ +kotlin.code.style=official +#org.gradle.unsafe.configuration-cache=true +#org.gradle.unsafe.configuration-cache-problems=warn \ No newline at end of file diff --git a/idea-plugin/examples/desktop-project/gradle/wrapper/gradle-wrapper.jar b/idea-plugin/examples/simple-preview-example/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from idea-plugin/examples/desktop-project/gradle/wrapper/gradle-wrapper.jar rename to idea-plugin/examples/simple-preview-example/gradle/wrapper/gradle-wrapper.jar diff --git a/idea-plugin/examples/desktop-project/gradle/wrapper/gradle-wrapper.properties b/idea-plugin/examples/simple-preview-example/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from idea-plugin/examples/desktop-project/gradle/wrapper/gradle-wrapper.properties rename to idea-plugin/examples/simple-preview-example/gradle/wrapper/gradle-wrapper.properties diff --git a/idea-plugin/examples/desktop-project/gradlew b/idea-plugin/examples/simple-preview-example/gradlew similarity index 100% rename from idea-plugin/examples/desktop-project/gradlew rename to idea-plugin/examples/simple-preview-example/gradlew diff --git a/idea-plugin/examples/desktop-project/gradlew.bat b/idea-plugin/examples/simple-preview-example/gradlew.bat similarity index 100% rename from idea-plugin/examples/desktop-project/gradlew.bat rename to idea-plugin/examples/simple-preview-example/gradlew.bat diff --git a/idea-plugin/examples/simple-preview-example/mpp-jvm/build.gradle.kts b/idea-plugin/examples/simple-preview-example/mpp-jvm/build.gradle.kts new file mode 100644 index 0000000000..132deb883d --- /dev/null +++ b/idea-plugin/examples/simple-preview-example/mpp-jvm/build.gradle.kts @@ -0,0 +1,25 @@ +import org.jetbrains.compose.compose + +plugins { + kotlin("multiplatform") + id("org.jetbrains.compose") +} + +kotlin { + jvm("desktop") + + sourceSets { + named("commonMain") { + dependencies { + api(compose.runtime) + api(compose.foundation) + api(compose.material) + } + } + named("desktopMain") { + dependencies { + implementation(compose.desktop.currentOs) + } + } + } +} \ No newline at end of file diff --git a/idea-plugin/examples/simple-preview-example/mpp-jvm/src/commonMain/kotlin/App.kt b/idea-plugin/examples/simple-preview-example/mpp-jvm/src/commonMain/kotlin/App.kt new file mode 100644 index 0000000000..788fc3940e --- /dev/null +++ b/idea-plugin/examples/simple-preview-example/mpp-jvm/src/commonMain/kotlin/App.kt @@ -0,0 +1,15 @@ +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.* + +@Composable +fun App() { + MaterialTheme { + Button(onClick = {}) { + Text("Hello, ${getPlatformName()}!") + } + } +} + +expect fun getPlatformName(): String \ No newline at end of file diff --git a/idea-plugin/examples/simple-preview-example/mpp-jvm/src/desktopMain/kotlin/DesktopApp.kt b/idea-plugin/examples/simple-preview-example/mpp-jvm/src/desktopMain/kotlin/DesktopApp.kt new file mode 100644 index 0000000000..0be86dcc17 --- /dev/null +++ b/idea-plugin/examples/simple-preview-example/mpp-jvm/src/desktopMain/kotlin/DesktopApp.kt @@ -0,0 +1,10 @@ +import androidx.compose.runtime.* +import androidx.compose.desktop.ui.tooling.preview.Preview + +actual fun getPlatformName(): String = "Desktop" + +@Preview +@Composable +fun DesktopAppPreview() { + App() +} \ No newline at end of file diff --git a/idea-plugin/examples/simple-preview-example/pure-jvm/build.gradle.kts b/idea-plugin/examples/simple-preview-example/pure-jvm/build.gradle.kts new file mode 100644 index 0000000000..4a21377d49 --- /dev/null +++ b/idea-plugin/examples/simple-preview-example/pure-jvm/build.gradle.kts @@ -0,0 +1,10 @@ +import org.jetbrains.compose.compose + +plugins { + kotlin("jvm") + id("org.jetbrains.compose") +} + +dependencies { + implementation(compose.desktop.currentOs) +} \ No newline at end of file diff --git a/idea-plugin/examples/simple-preview-example/pure-jvm/src/main/kotlin/preview.kt b/idea-plugin/examples/simple-preview-example/pure-jvm/src/main/kotlin/preview.kt new file mode 100644 index 0000000000..9c8d883b3e --- /dev/null +++ b/idea-plugin/examples/simple-preview-example/pure-jvm/src/main/kotlin/preview.kt @@ -0,0 +1,16 @@ +import androidx.compose.material.Text +import androidx.compose.material.Button +import androidx.compose.runtime.* +import androidx.compose.desktop.ui.tooling.preview.Preview + +@Preview +@Composable +fun ExamplePreview() { + var text by remember { mutableStateOf("Hello, World!") } + + Button(onClick = { + text = "Hello, Desktop!" + }) { + Text(text) + } +} \ No newline at end of file diff --git a/idea-plugin/examples/simple-preview-example/settings.gradle.kts b/idea-plugin/examples/simple-preview-example/settings.gradle.kts new file mode 100644 index 0000000000..3e91a0b422 --- /dev/null +++ b/idea-plugin/examples/simple-preview-example/settings.gradle.kts @@ -0,0 +1 @@ +include(":mpp-jvm", ":pure-jvm") \ No newline at end of file diff --git a/idea-plugin/gradle.properties b/idea-plugin/gradle.properties index 4e028d58d5..077072f910 100644 --- a/idea-plugin/gradle.properties +++ b/idea-plugin/gradle.properties @@ -4,12 +4,12 @@ kotlin.stdlib.default.dependency = false deploy.version=0.1-SNAPSHOT -plugin.channels=alpha +plugin.channels=snapshots plugin.since.build=203 plugin.until.build=212.* ## See https://jb.gg/intellij-platform-builds-list for available build versions. plugin.verifier.ide.versions=2020.3.2, 2021.1 platform.type=IC -platform.version=2020.3.2 +platform.version=2021.1 platform.download.sources=true diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/ConfigurePreviewTaskNameCache.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/ConfigurePreviewTaskNameCache.kt new file mode 100644 index 0000000000..83948e2c0b --- /dev/null +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/ConfigurePreviewTaskNameCache.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.desktop.ide.preview + +import com.intellij.openapi.externalSystem.model.DataNode +import com.intellij.openapi.externalSystem.model.ProjectKeys +import com.intellij.openapi.externalSystem.model.project.ModuleData +import com.intellij.openapi.externalSystem.service.project.ProjectDataManager +import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil +import com.intellij.openapi.module.Module +import com.intellij.openapi.project.Project +import com.intellij.util.concurrency.annotations.RequiresReadLock +import org.jetbrains.kotlin.idea.configuration.KotlinTargetData +import org.jetbrains.plugins.gradle.settings.GradleSettings +import org.jetbrains.plugins.gradle.util.GradleConstants + +internal val DEFAULT_CONFIGURE_PREVIEW_TASK_NAME = "configureDesktopPreview" + +internal interface ConfigurePreviewTaskNameProvider { + @RequiresReadLock + fun configurePreviewTaskNameOrNull(module: Module): String? +} + +internal class ConfigurePreviewTaskNameProviderImpl : ConfigurePreviewTaskNameProvider { + @RequiresReadLock + override fun configurePreviewTaskNameOrNull(module: Module): String? { + val modulePath = ExternalSystemApiUtil.getExternalProjectPath(module) ?: return null + val moduleNode = moduleDataNodeOrNull(module.project, modulePath) + if (moduleNode != null) { + val target = ExternalSystemApiUtil.getChildren(moduleNode, KotlinTargetData.KEY).singleOrNull() + if (target != null) { + return previewTaskName(target.data.externalName) + } + } + + return null + } + + private fun previewTaskName(targetName: String = "") = + "$DEFAULT_CONFIGURE_PREVIEW_TASK_NAME${targetName.capitalize()}" + + private fun moduleDataNodeOrNull(project: Project, modulePath: String): DataNode? { + val projectDataManager = ProjectDataManager.getInstance() + for (settings in GradleSettings.getInstance(project).linkedProjectsSettings) { + val projectInfo = projectDataManager.getExternalProjectData(project, GradleConstants.SYSTEM_ID, settings.externalProjectPath) + val projectNode = projectInfo?.externalProjectStructure ?: continue + val moduleNodes = ExternalSystemApiUtil.getChildren(projectNode, ProjectKeys.MODULE) + for (moduleNode in moduleNodes) { + val externalProjectPath = moduleNode.data.linkedExternalProjectPath + if (externalProjectPath == modulePath) { + return moduleNode + } + } + } + return null + } +} + +internal class ConfigurePreviewTaskNameCache( + private val provider: ConfigurePreviewTaskNameProvider +) : ConfigurePreviewTaskNameProvider { + private var cachedModuleId: String? = null + private var cachedTaskName: String? = null + + @RequiresReadLock + override fun configurePreviewTaskNameOrNull(module: Module): String? { + val externalProjectPath = ExternalSystemApiUtil.getExternalProjectPath(module) ?: return null + val moduleId = "$externalProjectPath#${module.name}" + + synchronized(this) { + if (moduleId == cachedModuleId) return cachedTaskName + } + + val taskName = provider.configurePreviewTaskNameOrNull(module) + synchronized(this) { + cachedTaskName = taskName + cachedModuleId = moduleId + } + return taskName + } + + fun invalidate() { + synchronized(this) { + cachedModuleId = null + cachedTaskName = null + } + } +} \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewActions.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewActions.kt new file mode 100644 index 0000000000..93851ff38c --- /dev/null +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewActions.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.desktop.ide.preview + +import com.intellij.execution.executors.DefaultRunExecutor +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.application.ReadAction +import com.intellij.openapi.components.service +import com.intellij.openapi.externalSystem.model.execution.ExternalSystemTaskExecutionSettings +import com.intellij.openapi.externalSystem.service.execution.ProgressExecutionMode +import com.intellij.openapi.externalSystem.task.TaskCallback +import com.intellij.openapi.externalSystem.util.ExternalSystemUtil.runTask +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.UserDataHolderBase +import com.intellij.openapi.wm.ToolWindowManager +import org.jetbrains.plugins.gradle.settings.GradleSettings +import org.jetbrains.plugins.gradle.util.GradleConstants +import javax.swing.SwingUtilities + +class RunPreviewAction( + private val previewLocation: PreviewLocation +) : AnAction({ "Show non-interactive preview" }, PreviewIcons.RUN_PREVIEW) { + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + buildPreviewViaGradle(project, previewLocation) + } +} + +internal const val PREVIEW_EDITOR_TOOLBAR_GROUP_ID = "Compose.Desktop.Preview.Editor.Toolbar" + +class RefreshOrRunPreviewAction : AnAction(PreviewIcons.COMPOSE) { + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + val previewLocation = ReadAction.compute { + val editor = e.dataContext.getData(CommonDataKeys.EDITOR) + if (editor != null) { + e.presentation.isEnabled = false + parentPreviewAtCaretOrNull(editor) + } else null + } + if (previewLocation != null) { + buildPreviewViaGradle(project, previewLocation) + } + } +} + +private fun buildPreviewViaGradle(project: Project, previewLocation: PreviewLocation) { + val previewToolWindow = ToolWindowManager.getInstance(project).getToolWindow("Desktop Preview") + previewToolWindow?.setAvailable(true) + + val gradleVmOptions = GradleSettings.getInstance(project).gradleVmOptions + val settings = ExternalSystemTaskExecutionSettings() + settings.executionName = "Preview: ${previewLocation.fqName}" + settings.externalProjectPath = previewLocation.modulePath + settings.taskNames = listOf(previewLocation.taskName) + settings.vmOptions = gradleVmOptions + settings.externalSystemIdString = GradleConstants.SYSTEM_ID.id + val previewService = project.service() + val gradleCallbackPort = previewService.gradleCallbackPort + settings.scriptParameters = + listOf( + "-Pcompose.desktop.preview.target=${previewLocation.fqName}", + "-Pcompose.desktop.preview.ide.port=$gradleCallbackPort" + ).joinToString(" ") + SwingUtilities.invokeLater { + ToolWindowManager.getInstance(project).getToolWindow("Desktop Preview")?.activate { + previewService.buildStarted() + } + } + runTask( + settings, + DefaultRunExecutor.EXECUTOR_ID, + project, + GradleConstants.SYSTEM_ID, + object : TaskCallback { + override fun onSuccess() { + previewService.buildFinished(success = true) + } + override fun onFailure() { + previewService.buildFinished(success = false) + } + }, + ProgressExecutionMode.IN_BACKGROUND_ASYNC, + false, + UserDataHolderBase() + ) +} diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewFloatingToolbarProvider.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewFloatingToolbarProvider.kt new file mode 100644 index 0000000000..f029ea8a68 --- /dev/null +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewFloatingToolbarProvider.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.desktop.ide.preview + +import com.intellij.openapi.Disposable +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.application.ReadAction +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.event.CaretEvent +import com.intellij.openapi.editor.event.CaretListener +import com.intellij.openapi.editor.toolbar.floating.AbstractFloatingToolbarProvider +import com.intellij.openapi.editor.toolbar.floating.FloatingToolbarComponent +import com.intellij.openapi.editor.toolbar.floating.FloatingToolbarComponentImpl +import com.intellij.openapi.project.Project +import com.intellij.util.concurrency.AppExecutorUtil + +class PreviewFloatingToolbarProvider : AbstractFloatingToolbarProvider(PREVIEW_EDITOR_TOOLBAR_GROUP_ID) { + override val autoHideable = false + override val priority: Int = 100 + + // todo: disable if not in Compose JVM module + override fun register(toolbar: FloatingToolbarComponent, parentDisposable: Disposable) { + try { + // todo: use provided data context once 2020.3 is no longer supported + val toolbarClass = FloatingToolbarComponentImpl::class.java + val getDataMethod = toolbarClass.getMethod("getData", String::class.java) + val editor = getDataMethod.invoke(toolbar, CommonDataKeys.EDITOR.name) as? Editor ?: return + registerComponent(toolbar, editor, parentDisposable) + } catch (e: Exception) { + LOG.error(e) + } + } + + override fun register(dataContext: DataContext, component: FloatingToolbarComponent, parentDisposable: Disposable) { + val editor = dataContext.getData(CommonDataKeys.EDITOR) ?: return + registerComponent(component, editor, parentDisposable) + } + + private fun registerComponent( + component: FloatingToolbarComponent, + editor: Editor, + parentDisposable: Disposable + ) { + val project = editor.project ?: return + val listener = PreviewEditorToolbarVisibilityUpdater(component, project, editor) + editor.caretModel.addCaretListener(listener, parentDisposable) + } +} + +internal class PreviewEditorToolbarVisibilityUpdater( + private val toolbar: FloatingToolbarComponent, + private val project: Project, + private val editor: Editor +) : CaretListener { + override fun caretPositionChanged(event: CaretEvent) { + ReadAction.nonBlocking { updateVisibility() } + .inSmartMode(project) + .submit(AppExecutorUtil.getAppExecutorService()) + } + + private fun updateVisibility() { + val parentPreviewFun = parentPreviewAtCaretOrNull(editor) + if (parentPreviewFun != null) { + toolbar.scheduleShow() + } else { + toolbar.scheduleHide() + } + } +} \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewIcons.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewIcons.kt index 6efc761aa3..acdba102f1 100644 --- a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewIcons.kt +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewIcons.kt @@ -11,4 +11,5 @@ object PreviewIcons { private fun load(path: String) = IconLoader.getIcon(path, PreviewIcons::class.java) val COMPOSE = load("/icons/compose/compose.svg") + val RUN_PREVIEW = load("/icons/compose/runPreview.svg") } \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewLocation.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewLocation.kt new file mode 100644 index 0000000000..ceb50191a6 --- /dev/null +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewLocation.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.desktop.ide.preview + +import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil +import com.intellij.openapi.roots.ProjectFileIndex +import com.intellij.util.concurrency.annotations.RequiresReadLock +import org.jetbrains.kotlin.idea.debugger.getService +import org.jetbrains.kotlin.psi.KtNamedFunction + +data class PreviewLocation(val fqName: String, val modulePath: String, val taskName: String) + +@RequiresReadLock +internal fun KtNamedFunction.asPreviewFunctionOrNull(): PreviewLocation? { + if (!isValidComposablePreviewFunction()) return null + + val fqName = composePreviewFunctionFqn() + val module = ProjectFileIndex.getInstance(project).getModuleForFile(containingFile.virtualFile) + if (module == null || module.isDisposed) return null + + val service = project.getService() + val previewTaskName = service.configurePreviewTaskNameOrNull(module) ?: DEFAULT_CONFIGURE_PREVIEW_TASK_NAME + val modulePath = ExternalSystemApiUtil.getExternalProjectPath(module) ?: return null + return PreviewLocation(fqName = fqName, modulePath = modulePath, taskName = previewTaskName) +} \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewPanel.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewPanel.kt index ce440cb78c..0087a2d610 100644 --- a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewPanel.kt +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewPanel.kt @@ -37,4 +37,7 @@ internal class PreviewPanel : JPanel() { repaint() } + + override fun getPreferredSize(): Dimension? = + imageDimension ?: super.getPreferredSize() } \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewRootLogger.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewRootLogger.kt new file mode 100644 index 0000000000..300ead3ad0 --- /dev/null +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewRootLogger.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.desktop.ide.preview + +import com.intellij.openapi.diagnostic.Logger + +val LOG = Logger.getInstance(PreviewRootLogger::class.java) +private class PreviewRootLogger \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewRunConfigurationProducer.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewRunConfigurationProducer.kt deleted file mode 100644 index 66730e14b5..0000000000 --- a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewRunConfigurationProducer.kt +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (C) 2019 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 org.jetbrains.compose.desktop.ide.preview - -import com.intellij.execution.actions.ConfigurationContext -import com.intellij.execution.actions.ConfigurationFromContext -import com.intellij.execution.actions.LazyRunConfigurationProducer -import com.intellij.execution.configurations.ConfigurationFactory -import com.intellij.openapi.components.service -import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil -import com.intellij.openapi.util.Ref -import com.intellij.psi.PsiElement -import org.jetbrains.compose.common.modulePath -import org.jetbrains.kotlin.psi.KtNamedFunction -import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType -import org.jetbrains.plugins.gradle.service.execution.GradleExternalTaskConfigurationType -import org.jetbrains.plugins.gradle.service.execution.GradleRunConfiguration - -/** - * Producer of [ComposePreviewRunConfiguration] for `@Composable` functions annotated with [PREVIEW_ANNOTATION_FQN]. The configuration - * created is initially named after the `@Composable` function, and its fully qualified name is properly set in the configuration. - * - * The [ConfigurationContext] where the [ComposePreviewRunConfiguration] is created from can be any descendant of the `@Composable` function - * in the PSI tree, such as its annotations, function name or even the keyword "fun". - * - * Based on com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer from AOSP - * with modifications - */ -class PreviewRunConfigurationProducer : LazyRunConfigurationProducer() { - override fun getConfigurationFactory(): ConfigurationFactory = - GradleExternalTaskConfigurationType.getInstance().factory - - override fun isConfigurationFromContext( - configuration: GradleRunConfiguration, - context: ConfigurationContext - ): Boolean { - val composeFunction = context.containingComposePreviewFunction() ?: return false - return configuration.run { - name == runConfigurationNameFor(composeFunction) - && settings.externalProjectPath == context.modulePath() - && settings.taskNames.singleOrNull() == configureDesktopPreviewTaskName - && settings.scriptParameters.split(" ").containsAll( - runConfigurationScriptParameters(composeFunction.composePreviewFunctionFqn(), context.port) - ) - } - } - - override fun setupConfigurationFromContext( - configuration: GradleRunConfiguration, - context: ConfigurationContext, - sourceElement: Ref - ): Boolean { - val composeFunction = context.containingComposePreviewFunction() ?: return false - // todo: temporary configuration? - configuration.apply { - name = runConfigurationNameFor(composeFunction) - settings.taskNames.add(configureDesktopPreviewTaskName) - settings.externalProjectPath = ExternalSystemApiUtil.getExternalProjectPath(context.location?.module) - settings.scriptParameters = - runConfigurationScriptParameters(composeFunction.composePreviewFunctionFqn(), context.port) - .joinToString(" ") - } - - return true - } -} - -private val configureDesktopPreviewTaskName = "configureDesktopPreview" - -private fun runConfigurationNameFor(function: KtNamedFunction): String = - "Compose Preview: ${function.name!!}" - -private fun runConfigurationScriptParameters(target: String, idePort: Int): List = - listOf( - "-Pcompose.desktop.preview.target=$target", - "-Pcompose.desktop.preview.ide.port=${idePort}" - ) - -private val ConfigurationContext.port: Int - get() = project.service().gradleCallbackPort - -private fun KtNamedFunction.composePreviewFunctionFqn() = "${getClassName()}.${name}" - -private fun ConfigurationContext.containingComposePreviewFunction() = - psiLocation?.let { location -> location.getNonStrictParentOfType()?.takeIf { it.isValidComposePreview() } } \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewRunLineMarkerContributor.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewRunLineMarkerContributor.kt index 508deb2c69..a8aefef758 100644 --- a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewRunLineMarkerContributor.kt +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewRunLineMarkerContributor.kt @@ -16,10 +16,11 @@ package org.jetbrains.compose.desktop.ide.preview -import com.intellij.execution.lineMarker.ExecutorAction import com.intellij.execution.lineMarker.RunLineMarkerContributor +import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil import com.intellij.psi.PsiElement import com.intellij.psi.impl.source.tree.LeafPsiElement +import org.jetbrains.kotlin.idea.util.projectStructure.module import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.psi.KtNamedFunction @@ -34,14 +35,13 @@ class PreviewRunLineMarkerContributor : RunLineMarkerContributor() { if (element !is LeafPsiElement) return null if (element.node.elementType != KtTokens.IDENTIFIER) return null - val parent = element.parent - return when { - parent is KtNamedFunction && parent.isValidComposePreview() -> { - val actions = arrayOf(ExecutorAction.getActions(0).first()) + return when (val parent = element.parent) { + is KtNamedFunction -> { + val previewFunction = parent.asPreviewFunctionOrNull() ?: return null + val actions = arrayOf(RunPreviewAction(previewFunction)) Info(PreviewIcons.COMPOSE, actions) { PreviewMessages.runPreview(parent.name!!) } } else -> null } } } - diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewStateService.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewStateService.kt index 71b29ec985..1beba8abb8 100644 --- a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewStateService.kt +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewStateService.kt @@ -5,59 +5,145 @@ package org.jetbrains.compose.desktop.ide.preview +import com.intellij.notification.NotificationGroupManager +import com.intellij.notification.NotificationType import com.intellij.openapi.Disposable import com.intellij.openapi.components.Service -import org.jetbrains.compose.desktop.ui.tooling.preview.rpc.FrameConfig -import org.jetbrains.compose.desktop.ui.tooling.preview.rpc.PreviewManager -import org.jetbrains.compose.desktop.ui.tooling.preview.rpc.PreviewManagerImpl -import java.awt.Dimension -import java.io.ByteArrayInputStream -import javax.imageio.ImageIO +import com.intellij.openapi.externalSystem.model.task.* +import com.intellij.openapi.externalSystem.service.notification.ExternalSystemProgressNotificationManager +import com.intellij.openapi.module.Module +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Disposer +import com.intellij.ui.components.JBLoadingPanel +import com.intellij.util.concurrency.annotations.RequiresReadLock +import org.jetbrains.compose.desktop.ui.tooling.preview.rpc.* +import org.jetbrains.kotlin.idea.framework.GRADLE_SYSTEM_ID import javax.swing.JComponent import javax.swing.event.AncestorEvent import javax.swing.event.AncestorListener @Service -class PreviewStateService : Disposable { - private var myPanel: PreviewPanel? = null - private val previewManager: PreviewManager = PreviewManagerImpl { frame -> - ByteArrayInputStream(frame.bytes).use { input -> - val image = ImageIO.read(input) - myPanel?.previewImage(image, Dimension(frame.width, frame.height)) - } - } +class PreviewStateService(private val myProject: Project) : Disposable { + private val previewListener = CompositePreviewListener() + private val previewManager: PreviewManager = PreviewManagerImpl(previewListener) val gradleCallbackPort: Int get() = previewManager.gradleCallbackPort + private val configurePreviewTaskNameCache = + ConfigurePreviewTaskNameCache(ConfigurePreviewTaskNameProviderImpl()) - private val myListener = object : AncestorListener { - private fun updateFrameSize(c: JComponent) { - val frameConfig = FrameConfig( - width = c.width, - height = c.height, - scale = null - ) - previewManager.updateFrameConfig(frameConfig) - } + init { + val projectRefreshListener = ConfigurePreviewTaskNameCacheInvalidator(configurePreviewTaskNameCache) + ExternalSystemProgressNotificationManager.getInstance() + .addNotificationListener(projectRefreshListener, myProject) + } - override fun ancestorAdded(event: AncestorEvent) { - updateFrameSize(event.component) - } + @RequiresReadLock + internal fun configurePreviewTaskNameOrNull(module: Module): String? = + configurePreviewTaskNameCache.configurePreviewTaskNameOrNull(module) - override fun ancestorRemoved(event: AncestorEvent) { - } + override fun dispose() { + previewManager.close() + configurePreviewTaskNameCache.invalidate() + } - override fun ancestorMoved(event: AncestorEvent) { - updateFrameSize(event.component) - } + internal fun registerPreviewPanels( + previewPanel: PreviewPanel, + loadingPanel: JBLoadingPanel + ) { + val previewResizeListener = PreviewResizeListener(previewManager) + previewPanel.addAncestorListener(previewResizeListener) + Disposer.register(this) { previewPanel.removeAncestorListener(previewResizeListener) } + + previewListener.addListener(PreviewPanelUpdater(previewPanel)) + previewListener.addListener(LoadingPanelUpdater(loadingPanel)) + previewListener.addListener(object : PreviewListenerBase() { + private val reported = hashSetOf>() + + override fun onIncompatibleProtocolVersions(versionServer: Int, versionClient: Int) { + if (reported.add(versionServer to versionClient)) { + NotificationGroupManager.getInstance() + .getNotificationGroup("Compose MPP Notifications") + .createNotification("Compose Desktop Preview may be incompatible " + + "with provided Compose Gradle plugin. " + + "Please use matching versions.", + NotificationType.ERROR) + .notify(myProject) + } + } + }) } - override fun dispose() { - myPanel?.removeAncestorListener(myListener) - previewManager.close() + internal fun buildStarted() { + previewListener.onNewBuildRequest() } - internal fun registerPreviewPanel(panel: PreviewPanel) { - myPanel = panel - panel.addAncestorListener(myListener) + internal fun buildFinished(success: Boolean) { + previewListener.onFinishedBuild(success) } } + +private class PreviewResizeListener(private val previewManager: PreviewManager) : AncestorListener { + private fun updateFrameSize(c: JComponent) { + val frameConfig = FrameConfig( + width = c.width, + height = c.height, + scale = c.graphicsConfiguration.defaultTransform.scaleX + ) + previewManager.updateFrameConfig(frameConfig) + } + + override fun ancestorAdded(event: AncestorEvent) { + updateFrameSize(event.component) + + } + + override fun ancestorRemoved(event: AncestorEvent) { + } + + override fun ancestorMoved(event: AncestorEvent) { + updateFrameSize(event.component) + } +} + +private class PreviewPanelUpdater(private val panel: PreviewPanel) : PreviewListenerBase() { + override fun onRenderedFrame(frame: RenderedFrame) { + panel.previewImage(frame.image, frame.dimension) + } +} + +private class LoadingPanelUpdater(private val panel: JBLoadingPanel) : PreviewListenerBase() { + override fun onNewBuildRequest() { + panel.setLoadingText("Building project") + panel.startLoading() + } + + override fun onFinishedBuild(success: Boolean) { + panel.stopLoading() + } + + override fun onNewRenderRequest(previewRequest: FrameRequest) { + panel.setLoadingText("Rendering preview") + panel.startLoading() + } + + override fun onRenderedFrame(frame: RenderedFrame) { + panel.stopLoading() + } +} + +// ExternalSystemTaskNotificationListenerAdapter is used, +// because ExternalSystemTaskNotificationListener interface's API +// was changed between 2020.3 and 2021.1, so a direct implementation +// would not work with both 2020.3 and 2021.1 +private class ConfigurePreviewTaskNameCacheInvalidator( + private val configurePreviewTaskNameCache: ConfigurePreviewTaskNameCache +) : ExternalSystemTaskNotificationListenerAdapter(null) { + override fun onStart(id: ExternalSystemTaskId, workingDir: String?) { + if ( + id.projectSystemId == GRADLE_SYSTEM_ID && + id.type == ExternalSystemTaskType.RESOLVE_PROJECT + ) { + configurePreviewTaskNameCache.invalidate() + } + } +} \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewToolWindow.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewToolWindow.kt index cdd53d6300..b75fd4f6b6 100644 --- a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewToolWindow.kt +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewToolWindow.kt @@ -9,6 +9,8 @@ import com.intellij.openapi.project.DumbAware import com.intellij.openapi.project.Project import com.intellij.openapi.wm.ToolWindow import com.intellij.openapi.wm.ToolWindowFactory +import com.intellij.ui.components.JBLoadingPanel +import java.awt.BorderLayout class PreviewToolWindow : ToolWindowFactory, DumbAware { override fun isApplicable(project: Project): Boolean { @@ -16,11 +18,21 @@ class PreviewToolWindow : ToolWindowFactory, DumbAware { return true } + override fun init(toolWindow: ToolWindow) { + toolWindow.setIcon(PreviewIcons.COMPOSE) + } + override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { toolWindow.contentManager.let { content -> val panel = PreviewPanel() - content.addContent(content.factory.createContent(panel, null, false)) - project.service().registerPreviewPanel(panel) + val loadingPanel = JBLoadingPanel(BorderLayout(), project) + loadingPanel.add(panel, BorderLayout.CENTER) + content.addContent(content.factory.createContent(loadingPanel, null, false)) + project.service().registerPreviewPanels(panel, loadingPanel) } } + + // don't show the toolwindow until a preview is requested + override fun shouldBeAvailable(project: Project): Boolean = + false } \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/editorUtils.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/editorUtils.kt new file mode 100644 index 0000000000..3e4de4242e --- /dev/null +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/editorUtils.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.desktop.ide.preview + +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiManager +import com.intellij.testFramework.LightVirtualFile +import com.intellij.util.concurrency.annotations.RequiresReadLock +import org.jetbrains.kotlin.idea.KotlinFileType +import org.jetbrains.kotlin.psi.KtNamedFunction + +@RequiresReadLock +internal fun parentPreviewAtCaretOrNull(editor: Editor): PreviewLocation? { + val caretModel = editor.caretModel + val psiFile = kotlinPsiFile(editor) + if (psiFile != null) { + var node = psiFile.findElementAt(caretModel.offset) + while (node != null) { + val previewFunction = (node as? KtNamedFunction)?.asPreviewFunctionOrNull() + if (previewFunction != null) { + return previewFunction + } + node = node.parent + } + } + + return null +} + +private fun kotlinPsiFile(editor: Editor): PsiFile? { + val project = editor.project ?: return null + val documentManager = FileDocumentManager.getInstance() + val file = documentManager.getFile(editor.document) + return if (file != null && file.fileType is KotlinFileType) { + PsiManager.getInstance(project).findFile(file) + } else null +} \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/locationUtils.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/locationUtils.kt index 5906832b52..c802e6369d 100644 --- a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/locationUtils.kt +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/locationUtils.kt @@ -16,7 +16,11 @@ package org.jetbrains.compose.desktop.ide.preview +import com.intellij.openapi.roots.ProjectRootModificationTracker +import com.intellij.psi.util.CachedValueProvider +import com.intellij.psi.util.CachedValuesManager import com.intellij.psi.util.parentOfType +import com.intellij.util.concurrency.annotations.RequiresReadLock import org.jetbrains.kotlin.asJava.findFacadeClass import org.jetbrains.kotlin.builtins.KotlinBuiltIns import org.jetbrains.kotlin.descriptors.ClassKind @@ -28,28 +32,22 @@ import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode internal const val DESKTOP_PREVIEW_ANNOTATION_FQN = "androidx.compose.desktop.ui.tooling.preview.Preview" +internal const val COMPOSABLE_FQ_NAME = "androidx.compose.runtime.Composable" /** * Utils based on functions from AOSP, taken from * tools/adt/idea/compose-designer/src/com/android/tools/idea/compose/preview/util/PreviewElement.kt */ -/** - * Whether this function is properly annotated with [PREVIEW_ANNOTATION_FQN] and is defined in a valid location. - * - * @see [isValidPreviewLocation] - */ -internal fun KtNamedFunction.isValidComposePreview() = - isValidPreviewLocation() && annotationEntries.any { it.fqNameMatches(DESKTOP_PREVIEW_ANNOTATION_FQN) } - /** * Returns whether a `@Composable` [PREVIEW_ANNOTATION_FQN] is defined in a valid location, which can be either: * 1. Top-level functions * 2. Non-nested functions defined in top-level classes that have a default (no parameter) constructor * */ -internal fun KtNamedFunction.isValidPreviewLocation(): Boolean { +private fun KtNamedFunction.isValidPreviewLocation(): Boolean { if (valueParameters.size > 0) return false + if (receiverTypeReference != null) return false if (isTopLevel) return true @@ -109,4 +107,41 @@ private fun KtAnnotationEntry.fqNameMatches(fqName: String): Boolean { private fun KtAnnotationEntry.getQualifiedName(): String? = analyze(BodyResolveMode.PARTIAL).get(BindingContext.ANNOTATION, this)?.fqName?.asString() +internal fun KtNamedFunction.composePreviewFunctionFqn() = "${getClassName()}.${name}" + +@RequiresReadLock +internal fun KtNamedFunction.isValidComposablePreviewFunction(): Boolean { + fun isValidComposablePreviewImpl(): Boolean { + if (!isValidPreviewLocation()) return false + + var hasComposableAnnotation = false + var hasPreviewAnnotation = false + val annotationIt = annotationEntries.iterator() + while (annotationIt.hasNext() && !(hasComposableAnnotation && hasPreviewAnnotation)) { + val annotation = annotationIt.next() + hasComposableAnnotation = hasComposableAnnotation || annotation.fqNameMatches(COMPOSABLE_FQ_NAME) + hasPreviewAnnotation = hasPreviewAnnotation || annotation.fqNameMatches(DESKTOP_PREVIEW_ANNOTATION_FQN) + } + + return hasComposableAnnotation && hasPreviewAnnotation + } + + return CachedValuesManager.getCachedValue(this) { + cachedResult(isValidComposablePreviewImpl()) + } +} + +// based on AndroidComposePsiUtils.kt from AOSP +internal fun KtNamedFunction.isComposableFunction(): Boolean { + return CachedValuesManager.getCachedValue(this) { + cachedResult(annotationEntries.any { it.fqNameMatches(COMPOSABLE_FQ_NAME) }) + } +} +private fun KtNamedFunction.cachedResult(value: T) = + CachedValueProvider.Result.create( + // TODO: see if we can handle alias imports without ruining performance. + value, + this.containingKtFile, + ProjectRootModificationTracker.getInstance(project) + ) \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/inspections/ComposeSuppressor.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/inspections/ComposeSuppressor.kt new file mode 100644 index 0000000000..04407d2f02 --- /dev/null +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/inspections/ComposeSuppressor.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2020 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 org.jetbrains.compose.inspections + +import com.intellij.codeInspection.InspectionSuppressor +import com.intellij.codeInspection.SuppressQuickFix +import com.intellij.psi.PsiElement +import org.jetbrains.compose.desktop.ide.preview.isComposableFunction +import org.jetbrains.kotlin.idea.KotlinLanguage +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.KtNamedFunction + +/** + * Suppress inspection that require composable function names to start with a lower case letter. + */ +class ComposeSuppressor : InspectionSuppressor { + override fun isSuppressedFor(element: PsiElement, toolId: String): Boolean { + return toolId == "FunctionName" && + element.language == KotlinLanguage.INSTANCE && + element.node.elementType == KtTokens.IDENTIFIER && + element.parent.let { it is KtNamedFunction && it.isComposableFunction() } + } + + override fun getSuppressActions(element: PsiElement?, toolId: String): Array { + return SuppressQuickFix.EMPTY_ARRAY + } +} + diff --git a/idea-plugin/src/main/resources/META-INF/plugin.xml b/idea-plugin/src/main/resources/META-INF/plugin.xml index ca24c7598c..9c1c8fcb9f 100644 --- a/idea-plugin/src/main/resources/META-INF/plugin.xml +++ b/idea-plugin/src/main/resources/META-INF/plugin.xml @@ -24,8 +24,6 @@ - + + + + + + + + + + + + diff --git a/idea-plugin/src/main/resources/icons/compose/runPreview.svg b/idea-plugin/src/main/resources/icons/compose/runPreview.svg new file mode 100644 index 0000000000..e6f910728c --- /dev/null +++ b/idea-plugin/src/main/resources/icons/compose/runPreview.svg @@ -0,0 +1,4 @@ + + + + diff --git a/templates/desktop-template/build.gradle.kts b/templates/desktop-template/build.gradle.kts index 1557e31860..26f1814bb0 100644 --- a/templates/desktop-template/build.gradle.kts +++ b/templates/desktop-template/build.gradle.kts @@ -3,17 +3,22 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat plugins { // __KOTLIN_COMPOSE_VERSION__ - kotlin("jvm") version "1.5.10" + kotlin("jvm") version "1.5.21" // __LATEST_COMPOSE_RELEASE_VERSION__ - id("org.jetbrains.compose") version (System.getenv("COMPOSE_TEMPLATE_COMPOSE_VERSION") ?: "0.5.0-build226") + id("org.jetbrains.compose") version (System.getenv("COMPOSE_TEMPLATE_COMPOSE_VERSION") ?: "1.0.0-alpha1") } repositories { mavenCentral() maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + google() } dependencies { + // Note, if you develop a library, you should use compose.desktop.common. + // compose.desktop.currentOs should be used in launcher-sourceSet + // (in a separate module for demo project and in testMain). + // With compose.desktop.common you will also lose @Preview functionality implementation(compose.desktop.currentOs) } @@ -22,6 +27,7 @@ compose.desktop { mainClass = "MainKt" nativeDistributions { + appResourcesRootDir.set(project.layout.projectDirectory.dir("xxx")) targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) packageName = "KotlinJvmComposeDesktopApplication" packageVersion = "1.0.0" diff --git a/templates/desktop-template/gradle/wrapper/gradle-wrapper.properties b/templates/desktop-template/gradle/wrapper/gradle-wrapper.properties index 7665b0fa93..05679dc3c1 100644 --- a/templates/desktop-template/gradle/wrapper/gradle-wrapper.properties +++ b/templates/desktop-template/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/templates/desktop-template/src/main/kotlin/main.kt b/templates/desktop-template/src/main/kotlin/main.kt index 85d8b04296..5a7386dd8a 100644 --- a/templates/desktop-template/src/main/kotlin/main.kt +++ b/templates/desktop-template/src/main/kotlin/main.kt @@ -1,16 +1,21 @@ -import androidx.compose.desktop.Window -import androidx.compose.material.Text +import androidx.compose.desktop.DesktopMaterialTheme +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.material.Button -import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application -fun main() = Window { +@Composable +@Preview +fun App() { var text by remember { mutableStateOf("Hello, World!") } - MaterialTheme { + DesktopMaterialTheme { Button(onClick = { text = "Hello, Desktop!" }) { @@ -18,3 +23,9 @@ fun main() = Window { } } } + +fun main() = application { + Window(onCloseRequest = ::exitApplication) { + App() + } +} diff --git a/templates/multiplatform-template/android/build.gradle.kts b/templates/multiplatform-template/android/build.gradle.kts index a0a284cc06..31c84584d8 100644 --- a/templates/multiplatform-template/android/build.gradle.kts +++ b/templates/multiplatform-template/android/build.gradle.kts @@ -22,5 +22,5 @@ android { dependencies { implementation(project(":common")) - implementation("androidx.activity:activity-compose:1.3.0-alpha03") + implementation("androidx.activity:activity-compose:1.3.0") } \ No newline at end of file diff --git a/templates/multiplatform-template/android/src/main/java/com/myapplication/MainActivity.kt b/templates/multiplatform-template/android/src/main/java/com/myapplication/MainActivity.kt index 52cb3575e1..ea85703371 100644 --- a/templates/multiplatform-template/android/src/main/java/com/myapplication/MainActivity.kt +++ b/templates/multiplatform-template/android/src/main/java/com/myapplication/MainActivity.kt @@ -4,13 +4,16 @@ import App import android.os.Bundle import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity +import androidx.compose.material.MaterialTheme class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { - App() + MaterialTheme { + App() + } } } } \ No newline at end of file diff --git a/templates/multiplatform-template/build.gradle.kts b/templates/multiplatform-template/build.gradle.kts index d608a85cf3..e3a349a9cd 100644 --- a/templates/multiplatform-template/build.gradle.kts +++ b/templates/multiplatform-template/build.gradle.kts @@ -1,18 +1,18 @@ buildscript { // __LATEST_COMPOSE_RELEASE_VERSION__ - val composeVersion = System.getenv("COMPOSE_TEMPLATE_COMPOSE_VERSION") ?: "0.5.0-build226" + val composeVersion = System.getenv("COMPOSE_TEMPLATE_COMPOSE_VERSION") ?: "1.0.0-alpha1" repositories { - google() mavenCentral() maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + google() } dependencies { classpath("org.jetbrains.compose:compose-gradle-plugin:$composeVersion") classpath("com.android.tools.build:gradle:4.0.1") // __KOTLIN_COMPOSE_VERSION__ - classpath(kotlin("gradle-plugin", version = "1.5.10")) + classpath(kotlin("gradle-plugin", version = "1.5.21")) } } diff --git a/templates/multiplatform-template/common/build.gradle.kts b/templates/multiplatform-template/common/build.gradle.kts index ec7c4db090..4edeb03b78 100644 --- a/templates/multiplatform-template/common/build.gradle.kts +++ b/templates/multiplatform-template/common/build.gradle.kts @@ -16,12 +16,14 @@ kotlin { api(compose.runtime) api(compose.foundation) api(compose.material) + // Needed only for preview. + implementation(compose.preview) } } named("androidMain") { dependencies { - api("androidx.appcompat:appcompat:1.3.0-beta01") - api("androidx.core:core-ktx:1.3.1") + api("androidx.appcompat:appcompat:1.3.1") + api("androidx.core:core-ktx:1.6.0") } } } @@ -48,4 +50,4 @@ android { res.srcDirs("src/androidMain/res") } } -} +} \ No newline at end of file diff --git a/templates/multiplatform-template/common/src/commonMain/kotlin/App.kt b/templates/multiplatform-template/common/src/commonMain/kotlin/App.kt index 71dba63610..8595c8b72e 100644 --- a/templates/multiplatform-template/common/src/commonMain/kotlin/App.kt +++ b/templates/multiplatform-template/common/src/commonMain/kotlin/App.kt @@ -1,18 +1,19 @@ import androidx.compose.material.Button -import androidx.compose.material.MaterialTheme import androidx.compose.material.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue @Composable fun App() { var text by remember { mutableStateOf("Hello, World!") } - MaterialTheme { - Button(onClick = { - text = "Hello, ${getPlatformName()}" - }) { - Text(text) - } + Button(onClick = { + text = "Hello, ${getPlatformName()}" + }) { + Text(text) } } diff --git a/templates/multiplatform-template/common/src/desktopMain/kotlin/DesktopApp.kt b/templates/multiplatform-template/common/src/desktopMain/kotlin/DesktopApp.kt index f3c8941cf5..fb85b30443 100644 --- a/templates/multiplatform-template/common/src/desktopMain/kotlin/DesktopApp.kt +++ b/templates/multiplatform-template/common/src/desktopMain/kotlin/DesktopApp.kt @@ -1 +1,10 @@ -actual fun getPlatformName(): String = "Desktop" \ No newline at end of file +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.runtime.Composable + +actual fun getPlatformName(): String = "Desktop" + +@Preview +@Composable +fun AppPreview() { + App() +} \ No newline at end of file diff --git a/templates/multiplatform-template/desktop/src/jvmMain/kotlin/main.kt b/templates/multiplatform-template/desktop/src/jvmMain/kotlin/main.kt index c810ea3d8b..d60069e49a 100644 --- a/templates/multiplatform-template/desktop/src/jvmMain/kotlin/main.kt +++ b/templates/multiplatform-template/desktop/src/jvmMain/kotlin/main.kt @@ -1,5 +1,11 @@ -import androidx.compose.desktop.Window +import androidx.compose.desktop.DesktopMaterialTheme +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application -fun main() = Window { - App() +fun main() = application { + Window(onCloseRequest = ::exitApplication) { + DesktopMaterialTheme { + App() + } + } } \ No newline at end of file diff --git a/templates/multiplatform-template/gradle/wrapper/gradle-wrapper.properties b/templates/multiplatform-template/gradle/wrapper/gradle-wrapper.properties index 7665b0fa93..f371643eed 100644 --- a/templates/multiplatform-template/gradle/wrapper/gradle-wrapper.properties +++ b/templates/multiplatform-template/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/templates/web-template/build.gradle.kts b/templates/web-template/build.gradle.kts index 2bf2a6f974..57e88a1176 100644 --- a/templates/web-template/build.gradle.kts +++ b/templates/web-template/build.gradle.kts @@ -3,14 +3,15 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat plugins { // __KOTLIN_COMPOSE_VERSION__ - kotlin("multiplatform") version "1.5.10" + kotlin("multiplatform") version "1.5.21" // __LATEST_COMPOSE_RELEASE_VERSION__ - id("org.jetbrains.compose") version ("0.5.0-build228") + id("org.jetbrains.compose") version ("1.0.0-alpha1") } repositories { mavenCentral() maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + google() } kotlin { diff --git a/templates/web-template/src/main/kotlin/main.kt b/templates/web-template/src/main/kotlin/main.kt index 40b8da8a84..333561ab9d 100644 --- a/templates/web-template/src/main/kotlin/main.kt +++ b/templates/web-template/src/main/kotlin/main.kt @@ -1,11 +1,28 @@ +import androidx.compose.runtime.* +import org.jetbrains.compose.web.dom.Button import org.jetbrains.compose.web.dom.Div import org.jetbrains.compose.web.dom.Text import org.jetbrains.compose.web.renderComposable fun main() { renderComposable(rootElementId = "root") { - Div { - Text("This is a template!") + Body() + } +} + +@Composable +fun Body() { + var counter by remember { mutableStateOf(0) } + Div { + Text("Clicked: ${counter}") + } + Button( + attrs = { + onClick { _ -> + counter++ + } } + ) { + Text("Click") } } diff --git a/tutorials/Desktop_Components/README.md b/tutorials/Desktop_Components/README.md index c467913c64..8dbb5b19a5 100644 --- a/tutorials/Desktop_Components/README.md +++ b/tutorials/Desktop_Components/README.md @@ -9,7 +9,6 @@ In this tutorial, we will show you how to use desktop-specific components of Com You can apply scrollbars to scrollable components. The scrollbar and scrollable components share a common state to synchronize with each other. For example, `VerticalScrollbar` can be attached to `Modifier.verticalScroll`, and `LazyColumnFor` and `HorizontalScrollbar` can be attached to `Modifier.horizontalScroll` and `LazyRowFor`. ```kotlin -import androidx.compose.desktop.Window import androidx.compose.foundation.HorizontalScrollbar import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.background @@ -31,47 +30,49 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.WindowState +import androidx.compose.ui.window.singleWindowApplication + +fun main() = singleWindowApplication( + title = "Scrollbars", + state = WindowState(width = 250.dp, height = 400.dp) +) { + Box( + modifier = Modifier.fillMaxSize() + .background(color = Color(180, 180, 180)) + .padding(10.dp) + ) { + val stateVertical = rememberScrollState(0) + val stateHorizontal = rememberScrollState(0) -fun main() { - Window(title = "Scrollbars", size = IntSize(250, 400)) { Box( - modifier = Modifier.fillMaxSize() - .background(color = Color(180, 180, 180)) - .padding(10.dp) + modifier = Modifier + .fillMaxSize() + .verticalScroll(stateVertical) + .padding(end = 12.dp, bottom = 12.dp) + .horizontalScroll(stateHorizontal) ) { - val stateVertical = rememberScrollState(0) - val stateHorizontal = rememberScrollState(0) - - Box( - modifier = Modifier - .fillMaxSize() - .verticalScroll(stateVertical) - .padding(end = 12.dp, bottom = 12.dp) - .horizontalScroll(stateHorizontal) - ) { - Column { - for (item in 0..30) { - TextBox("Item #$item") - if (item < 30) { - Spacer(modifier = Modifier.height(5.dp)) - } + Column { + for (item in 0..30) { + TextBox("Item #$item") + if (item < 30) { + Spacer(modifier = Modifier.height(5.dp)) } } } - VerticalScrollbar( - modifier = Modifier.align(Alignment.CenterEnd) - .fillMaxHeight(), - adapter = rememberScrollbarAdapter(stateVertical) - ) - HorizontalScrollbar( - modifier = Modifier.align(Alignment.BottomStart) - .fillMaxWidth() - .padding(end = 12.dp), - adapter = rememberScrollbarAdapter(stateHorizontal) - ) } + VerticalScrollbar( + modifier = Modifier.align(Alignment.CenterEnd) + .fillMaxHeight(), + adapter = rememberScrollbarAdapter(stateVertical) + ) + HorizontalScrollbar( + modifier = Modifier.align(Alignment.BottomStart) + .fillMaxWidth() + .padding(end = 12.dp), + adapter = rememberScrollbarAdapter(stateHorizontal) + ) } } @@ -89,14 +90,13 @@ fun TextBox(text: String = "Item") { } ``` -![Scrollbars](scrollbars.gif) +Scrollbars ## Lazy scrollable components with Scrollbar You can use scrollbars with lazy scrollable components, for example, `LazyColumn`. ```kotlin -import androidx.compose.desktop.Window import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -114,11 +114,17 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import androidx.compose.ui.window.rememberWindowState -fun main() { - Window(title = "Scrollbars", size = IntSize(250, 400)) { +fun main() = application { + Window( + onCloseRequest = ::exitApplication, + title = "Scrollbars", + state = rememberWindowState(width = 250.dp, height = 400.dp) + ) { LazyScrollable() } } @@ -162,15 +168,14 @@ fun TextBox(text: String = "Item") { } ``` -![Lazy component](lazy_scrollbar.gif) +Lazy component ## Theme applying -Scrollbars support themes to change their appearance. The example below shows how to use the `DesktopTheme` appearance for the scrollbar. +Scrollbars support themes to change their appearance. The example below shows how to use the `DesktopMaterialTheme` appearance for the scrollbar. ```kotlin -import androidx.compose.desktop.DesktopTheme -import androidx.compose.desktop.Window +import androidx.compose.desktop.DesktopMaterialTheme import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxHeight @@ -185,44 +190,47 @@ import androidx.compose.material.Text import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.layout.Column import androidx.compose.foundation.verticalScroll -import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.graphics.Color import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import androidx.compose.ui.window.rememberWindowState -fun main() { - Window(title = "Scrollbars", size = IntSize(280, 400)) { - MaterialTheme { - DesktopTheme { - Box( - modifier = Modifier.fillMaxSize() - .background(color = Color(180, 180, 180)) - .padding(10.dp) - ) { - val state = rememberScrollState(0) +fun main() = application { + Window( + onCloseRequest = ::exitApplication, + title = "Scrollbars", + state = rememberWindowState(width = 250.dp, height = 400.dp) + ) { + DesktopMaterialTheme { + Box( + modifier = Modifier.fillMaxSize() + .background(color = Color(180, 180, 180)) + .padding(10.dp) + ) { + val state = rememberScrollState(0) - Column( - modifier = Modifier - .verticalScroll(state) - .fillMaxSize() - .padding(end = 12.dp) - ) { - for (item in 0..30) { - TextBox("Item #$item") - if (item < 30) { - Spacer(modifier = Modifier.height(5.dp)) - } + Column( + modifier = Modifier + .verticalScroll(state) + .fillMaxSize() + .padding(end = 12.dp) + ) { + for (item in 0..30) { + TextBox("Item #$item") + if (item < 30) { + Spacer(modifier = Modifier.height(5.dp)) } } - VerticalScrollbar( - modifier = Modifier.align(Alignment.CenterEnd) - .fillMaxHeight(), - adapter = rememberScrollbarAdapter(state) - ) } + VerticalScrollbar( + modifier = Modifier.align(Alignment.CenterEnd) + .fillMaxHeight(), + adapter = rememberScrollbarAdapter(state) + ) } } } @@ -242,7 +250,7 @@ fun TextBox(text: String = "Item") { } ``` -![Themed scrollbar](themed_scrollbar.gif) +Themed scrollbar ## Tooltips @@ -250,12 +258,12 @@ fun TextBox(text: String = "Item") { You can add tooltip to any components using `BoxWithTooltip`. Basically `BoxWithTooltip` is a `Box` with the ability to show a tooltip, and has the same arguments and behavior as `Box`. The main arguments of the `BoxWithTooltip` function: - tooltip - composable content representing tooltip + - tooltipPlacement - describes how to place tooltip. You can specify an anchor (the mouse cursor or the component), an offset and an alignment - delay - time delay in milliseconds after which the tooltip will be shown (default is 500 ms) - - offset - tooltip offset, the default position of the tooltip is under the mouse cursor ```kotlin -import androidx.compose.desktop.Window import androidx.compose.foundation.BoxWithTooltip +import androidx.compose.foundation.TooltipPlacement import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -265,41 +273,54 @@ import androidx.compose.material.Button import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.DpOffset -import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import androidx.compose.ui.window.rememberWindowState -fun main() = Window(title = "Tooltip Example", size = IntSize(300, 300)) { - val buttons = listOf("Button A", "Button B", "Button C", "Button D", "Button E", "Button F") - Column(Modifier.fillMaxSize(), Arrangement.spacedBy(5.dp)) { - buttons.forEachIndexed { index, name -> - // wrap button in BoxWithTooltip - BoxWithTooltip( - modifier = Modifier.padding(start = 40.dp), - tooltip = { - // composable tooltip content - Surface( - modifier = Modifier.shadow(4.dp), - color = Color(255, 255, 210), - shape = RoundedCornerShape(4.dp) - ) { - Text( - text = "Tooltip for ${name}", - modifier = Modifier.padding(10.dp) - ) - } - }, - delay = 600, // in milliseconds - offset = if (index % 2 == 0) DpOffset(-16.dp, 0.dp) else DpOffset.Zero // tooltip offset - ) { - Button(onClick = {}) { Text(text = name) } +@OptIn(ExperimentalComposeUiApi::class) +fun main() = application { + Window( + onCloseRequest = ::exitApplication, + title = "Tooltip Example", + state = rememberWindowState(width = 300.dp, height = 300.dp) + ) { + val buttons = listOf("Button A", "Button B", "Button C", "Button D", "Button E", "Button F") + Column(Modifier.fillMaxSize(), Arrangement.spacedBy(5.dp)) { + buttons.forEachIndexed { index, name -> + // wrap button in BoxWithTooltip + BoxWithTooltip( + modifier = Modifier.padding(start = 40.dp), + tooltip = { + // composable tooltip content + Surface( + modifier = Modifier.shadow(4.dp), + color = Color(255, 255, 210), + shape = RoundedCornerShape(4.dp) + ) { + Text( + text = "Tooltip for ${name}", + modifier = Modifier.padding(10.dp) + ) + } + }, + delay = 600, // in milliseconds + tooltipPlacement = TooltipPlacement.CursorPoint( + alignment = Alignment.BottomEnd, + offset = if (index % 2 == 0) DpOffset(-16.dp, 0.dp) else DpOffset.Zero // tooltip offset + ) + ) { + Button(onClick = {}) { Text(text = name) } + } } } } } ``` -![Tooltip](tooltips.gif) \ No newline at end of file +Tooltip diff --git a/tutorials/Getting_Started/README.md b/tutorials/Getting_Started/README.md index 41dc638485..9ae95577a7 100644 --- a/tutorials/Getting_Started/README.md +++ b/tutorials/Getting_Started/README.md @@ -1,9 +1,9 @@ -# Getting Started with Compose for Desktop +# Getting Started with Compose Multiplatform ## What is covered In this tutorial we will create a simple desktop UI application -using the Compose UI framework. +using Compose Multiplatform UI framework. ## Prerequisites @@ -12,7 +12,7 @@ and so any of these platforms can be used for this tutorial. The following software has to be preinstalled: * JDK 11 or later - * IntelliJ IDEA Community Edition or Ultimate Edition 20.2 or later (other editors could be used, but we assume you are using IntelliJ IDEA in this tutorial) + * IntelliJ IDEA Community Edition or Ultimate Edition 2020.3 or later (other editors could be used, but we assume you are using IntelliJ IDEA in this tutorial) ## Creating a new project @@ -24,19 +24,27 @@ capable to create a Compose application automatically. Note that JDK must be at least JDK 11, and to use the native distribution packaging JDK 15 or later must be used. -![Create new project 1](screen3.png) +Create new project 1 -![Create new project 2](screen4.png) +Create new project 2 -![Create new project 3](screen5.png) +Create new project 3 + +### IDE plugin + +Compose Multiplatform [IDEA plugin](https://plugins.jetbrains.com/plugin/16541-compose-multiplatform-ide-support) +can simplify compose development by adding support for `@Preview` annotation on argument-less +`@Composable` functions. One could see how particular composable function looks like +directly in IDE panel. This plugin could also be discovered via plugins marketplace, +just search for "Compose Multiplatform". ### Update the wizard plugin -The Сompose plugin version used in the wizard above may be not the last. Update the version of the plugin to the latest available by editing the `build.gradle.kts` file, finding and updating the version information as shown below. In this example the latest version of the plugin was 0.5.0-build225 and a compatible version of kotlin was 1.5.10. For the latest versions, see the [latest versions](https://github.com/JetBrains/compose-jb/releases) site and the [Kotlin](https://kotlinlang.org/) site. +The Сompose plugin version used in the wizard above may be not the last. Update the version of the plugin to the latest available by editing the `build.gradle.kts` file, finding and updating the version information as shown below. In this example the latest version of the plugin was 1.0.0-alpha1 and a compatible version of kotlin was 1.5.21. For the latest versions, see the [latest versions](https://github.com/JetBrains/compose-jb/releases) site and the [Kotlin](https://kotlinlang.org/) site. ``` plugins { - kotlin("jvm") version "1.5.10" - id("org.jetbrains.compose") version "0.5.0-build225" + kotlin("jvm") version "1.5.21" + id("org.jetbrains.compose") version "1.0.0-alpha1" } ``` @@ -71,13 +79,14 @@ Then create `build.gradle.kts` with the following content: import org.jetbrains.compose.compose plugins { - kotlin("jvm") version "1.5.10" - id("org.jetbrains.compose") version "0.5.0-build225" + kotlin("jvm") version "1.5.21" + id("org.jetbrains.compose") version "1.0.0-alpha1" } repositories { mavenCentral() maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + google() } dependencies { @@ -92,7 +101,6 @@ compose.desktop { ``` Then create `src/main/kotlin/main.kt` and put the following code in there: ```kotlin -import androidx.compose.desktop.Window import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -103,24 +111,32 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp - -fun main() = Window(title = "Compose for Desktop", size = IntSize(300, 300)) { - val count = remember { mutableStateOf(0) } - MaterialTheme { - Column(Modifier.fillMaxSize(), Arrangement.spacedBy(5.dp)) { - Button(modifier = Modifier.align(Alignment.CenterHorizontally), - onClick = { - count.value++ - }) { - Text(if (count.value == 0) "Hello World" else "Clicked ${count.value}!") - } - Button(modifier = Modifier.align(Alignment.CenterHorizontally), - onClick = { - count.value = 0 +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import androidx.compose.ui.window.rememberWindowState + +fun main() = application { + Window( + onCloseRequest = ::exitApplication, + title = "Compose for Desktop", + state = rememberWindowState(width = 300.dp, height = 300.dp) + ) { + val count = remember { mutableStateOf(0) } + MaterialTheme { + Column(Modifier.fillMaxSize(), Arrangement.spacedBy(5.dp)) { + Button(modifier = Modifier.align(Alignment.CenterHorizontally), + onClick = { + count.value++ }) { - Text("Reset") + Text(if (count.value == 0) "Hello World" else "Clicked ${count.value}!") + } + Button(modifier = Modifier.align(Alignment.CenterHorizontally), + onClick = { + count.value = 0 + }) { + Text("Reset") + } } } } @@ -130,13 +146,17 @@ fun main() = Window(title = "Compose for Desktop", size = IntSize(300, 300)) { Open `build.gradle.kts` as a project in IntelliJ IDEA. -![New project](screen1.png) +New project After you download the Compose for Desktop dependencies from the Maven repositories your new project is ready to go. Open the Gradle toolbar on the right, and select `sample/Tasks/compose desktop/run`. The first run may take some time, but afterwards the following dialog will be shown: -![Application running](screen2.gif) +Application running You can click on the button several times and see that the application reacts and updates the UI. + +Running and debugging the `main()` function using run gutter is also supported. + +Application running diff --git a/tutorials/Getting_Started/screen6.png b/tutorials/Getting_Started/screen6.png new file mode 100644 index 0000000000..8d9dcf0b48 Binary files /dev/null and b/tutorials/Getting_Started/screen6.png differ diff --git a/tutorials/Image_And_Icons_Manipulations/README.md b/tutorials/Image_And_Icons_Manipulations/README.md index ad88fd293b..b5b8d046a0 100755 --- a/tutorials/Image_And_Icons_Manipulations/README.md +++ b/tutorials/Image_And_Icons_Manipulations/README.md @@ -6,128 +6,180 @@ In this tutorial we will show you how to work with images using Compose for Desk ## Loading images from resources -Using images from application resources is very simple. Suppose we have a PNG image that is placed in the `resources/images` directory in our project. For this tutorial we will use the image sample: +Using images from application resources is very simple. Suppose we have a PNG image that is placed in the `resources` directory in our project. For this tutorial we will use the image sample: -![Sample](sample.png) +Sample ```kotlin -import androidx.compose.desktop.Window import androidx.compose.foundation.Image import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.ui.Modifier -import androidx.compose.ui.res.imageResource - -fun main() { - Window { - Image( - bitmap = imageResource("images/sample.png"), // ImageBitmap - contentDescription = "Sample", - modifier = Modifier.fillMaxSize() - ) - } +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.window.singleWindowApplication + +fun main() = singleWindowApplication { + Image( + painter = painterResource("sample.png"), // ImageBitmap + contentDescription = "Sample", + modifier = Modifier.fillMaxSize() + ) } ``` -![Resources](image_from_resources.png) +`painterResource` supports raster (BMP, GIF, HEIF, ICO, JPEG, PNG, WBMP, WebP) and vector formats (SVG, [XML vector drawable](https://developer.android.com/guide/topics/graphics/vector-drawable-resources)). + +Resources -## Loading images from device storage +## Loading images from device storage asynchronously -To create an `ImageBitmap` from a loaded image stored in the device memory you can use `org.jetbrains.skija.Image`: +To load an image stored in the device memory you can use `loadImageBitmap`, `loadSvgPainter` or `loadXmlImageVector`. The example below shows how to use them to load an image asynchronously. ```kotlin -import androidx.compose.desktop.Window import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.asImageBitmap -import org.jetbrains.skija.Image +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.loadImageBitmap +import androidx.compose.ui.res.loadSvgPainter +import androidx.compose.ui.res.loadXmlImageVector +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.singleWindowApplication +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.xml.sax.InputSource import java.io.File - -fun main() { - Window { - val image = remember { imageFromFile(File("sample.png")) } - Image( - bitmap = image, +import java.io.IOException + +fun main() = singleWindowApplication { + val density = LocalDensity.current + Column { + AsyncImage( + load = { loadImageBitmap(File("sample.png")) }, + painterFor = { remember { BitmapPainter(it) } }, contentDescription = "Sample", - modifier = Modifier.fillMaxSize() + modifier = Modifier.width(200.dp) + ) + AsyncImage( + load = { loadSvgPainter(File("idea-logo.svg"), density) }, + painterFor = { it }, + contentDescription = "Idea logo", + contentScale = ContentScale.FillWidth, + modifier = Modifier.width(200.dp) + ) + AsyncImage( + load = { loadXmlImageVector(File("compose-logo.xml"), density) }, + painterFor = { rememberVectorPainter(it) }, + contentDescription = "Compose logo", + contentScale = ContentScale.FillWidth, + modifier = Modifier.width(200.dp) ) } } -fun imageFromFile(file: File): ImageBitmap { - return Image.makeFromEncoded(file.readBytes()).asImageBitmap() +@Composable +fun AsyncImage( + load: suspend () -> T, + painterFor: @Composable (T) -> Painter, + contentDescription: String, + modifier: Modifier = Modifier, + contentScale: ContentScale = ContentScale.Fit, +) { + val image: T? by produceState(null) { + value = withContext(Dispatchers.IO) { + try { + load() + } catch (e: IOException) { + e.printStackTrace() + null + } + } + } + + if (image != null) { + Image( + painter = painterFor(image!!), + contentDescription = contentDescription, + contentScale = contentScale, + modifier = modifier + ) + } } + +fun loadImageBitmap(file: File): ImageBitmap = + file.inputStream().buffered().use(::loadImageBitmap) + +fun loadSvgPainter(file: File, density: Density): Painter = + file.inputStream().buffered().use { loadSvgPainter(it, density) } + +fun loadXmlImageVector(file: File, density: Density): ImageVector = + file.inputStream().buffered().use { loadXmlImageVector(InputSource(it), density) } ``` -![Storage](image_from_resources.png) +Storage + +[PNG](sample.png) + +[SVG](../../artwork/idea-logo.svg) + +[XML vector drawable](../../artwork/compose-logo.xml) ## Drawing raw image data using native canvas You may want to draw raw image data, in which case you can use `Canvas` and` drawIntoCanvas`. ```kotlin -import androidx.compose.desktop.Window import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.drawscope.drawIntoCanvas -import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.res.loadImageBitmap +import androidx.compose.ui.res.useResource +import androidx.compose.ui.window.singleWindowApplication import org.jetbrains.skija.Bitmap import org.jetbrains.skija.ColorAlphaType -import org.jetbrains.skija.IRect import org.jetbrains.skija.ImageInfo -import org.jetbrains.skija.Image -import java.awt.image.BufferedImage -import java.io.File -import javax.imageio.ImageIO -fun main() { - Window { - val bitmap = remember { bitmapFromByteArray() } - Canvas( - modifier = Modifier.fillMaxSize() - ) { - drawIntoCanvas { canvas -> - canvas.nativeCanvas.drawImageRect( - Image.makeFromBitmap(bitmap), - IRect(0, 0, bitmap.getWidth(), bitmap.getHeight()).toRect() - ) - } +private val sample = useResource("sample.png", ::loadImageBitmap) + +fun main() = singleWindowApplication { + val bitmap = remember { bitmapFromByteArray(sample.getBytes(), sample.width, sample.height) } + Canvas( + modifier = Modifier.fillMaxSize() + ) { + drawIntoCanvas { canvas -> + canvas.drawImage(bitmap, Offset.Zero, Paint()) } } } -fun bitmapFromByteArray(): Bitmap { - var image: BufferedImage? = null - try { - image = ImageIO.read(File("sample.png")) - } catch (e: Exception) { - // image file does not exist - } - - if (image == null) { - image = BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB) - } - val pixels = getBytes(image) // assume we only have raw pixels - - // allocate and fill skija Bitmap +fun bitmapFromByteArray(pixels: ByteArray, width: Int, height: Int): ImageBitmap { val bitmap = Bitmap() - bitmap.allocPixels(ImageInfo.makeS32(image.width, image.height, ColorAlphaType.PREMUL)) - bitmap.installPixels(bitmap.getImageInfo(), pixels, (image.width * 4).toLong()) - - return bitmap + bitmap.allocPixels(ImageInfo.makeS32(width, height, ColorAlphaType.PREMUL)) + bitmap.installPixels(bitmap.imageInfo, pixels, (width * 4).toLong()) + return bitmap.asImageBitmap() } // creating byte array from BufferedImage -private fun getBytes(image: BufferedImage): ByteArray { - val width = image.width - val height = image.height - +private fun ImageBitmap.getBytes(): ByteArray { val buffer = IntArray(width * height) - image.getRGB(0, 0, width, height, buffer, 0, width) + readPixels(buffer) val pixels = ByteArray(width * height * 4) @@ -146,250 +198,67 @@ private fun getBytes(image: BufferedImage): ByteArray { } ``` -![Drawing raw images](draw_image_into_canvas.png) +Drawing raw images ## Setting the application window icon -You have 2 ways to set icon for window: -1. Via parameter in `Window` function (or in `AppWindow` constructor) +You can set the icon for the window via parameter in the `Window` function. + +Note that to change the icon on the taskbar on some OS (macOs), you should change icon in [build.gradle](https://github.com/JetBrains/compose-jb/tree/sync/2021-07-23/tutorials/Native_distributions_and_local_execution#app-icon) ```kotlin -import androidx.compose.desktop.Window -import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.asImageBitmap -import org.jetbrains.skija.Image -import java.awt.image.BufferedImage -import java.io.ByteArrayOutputStream -import java.io.File -import javax.imageio.ImageIO +import androidx.compose.ui.draw.paint +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application -fun main() { - val image = getWindowIcon() +fun main() = application { + val icon = painterResource("sample.png") Window( - icon = image + onCloseRequest = ::exitApplication, + icon = icon ) { - val imageAsset = remember { asImageAsset(image) } - Image( - bitmap = imageAsset, - contentDescription = "Icon", - modifier = Modifier.fillMaxSize() - ) - } -} - -fun getWindowIcon(): BufferedImage { - var image: BufferedImage? = null - try { - image = ImageIO.read(File("sample.png")) - } catch (e: Exception) { - // image file does not exist - } - - if (image == null) { - image = BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB) - } - - return image -} - -fun asImageAsset(image: BufferedImage): ImageBitmap { - val baos = ByteArrayOutputStream() - ImageIO.write(image, "png", baos) - - return Image.makeFromEncoded(baos.toByteArray()).asImageBitmap() -} -``` - -2. Using `setIcon()` method - -```kotlin -import androidx.compose.desktop.AppManager -import androidx.compose.desktop.Window -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.asImageBitmap -import org.jetbrains.skija.Image -import java.awt.image.BufferedImage -import java.io.ByteArrayOutputStream -import java.io.File -import javax.imageio.ImageIO - -fun main() { - val image = getWindowIcon() - Window { - val imageAsset = remember { asImageAsset(image) } - Image( - bitmap = imageAsset, - contentDescription = "Icon", - modifier = Modifier.fillMaxSize() - ) + Box(Modifier.paint(icon).fillMaxSize()) } - - AppManager.focusedWindow?.setIcon(image) -} - -fun getWindowIcon(): BufferedImage { - var image: BufferedImage? = null - try { - image = ImageIO.read(File("sample.png")) - } catch (e: Exception) { - // image file does not exist - } - - if (image == null) { - image = BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB) - } - - return image -} - -fun asImageAsset(image: BufferedImage): ImageBitmap { - val baos = ByteArrayOutputStream() - ImageIO.write(image, "png", baos) - - return Image.makeFromEncoded(baos.toByteArray()).asImageBitmap() } ``` -![Window icon](window_icon.png) +Window icon ## Setting the application tray icon You can create a tray icon for your application: ```kotlin -import androidx.compose.desktop.AppManager -import androidx.compose.desktop.Window import androidx.compose.foundation.Image import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.window.v1.MenuItem -import androidx.compose.ui.window.v1.Tray -import org.jetbrains.skija.Image -import java.awt.image.BufferedImage -import java.io.ByteArrayOutputStream -import java.io.File -import javax.imageio.ImageIO - -fun main() { - val image = getWindowIcon() - Window { - DisposableEffect(Unit) { - val tray = Tray().apply { - icon(getWindowIcon()) - menu( - MenuItem( - name = "Quit App", - onClick = { AppManager.exit() } - ) - ) - } - onDispose { - tray.remove() - } +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.window.Tray +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application + +fun main() = application { + val icon = painterResource("sample.png") + + Tray( + icon = icon, + menu = { + Item("Quit App", onClick = ::exitApplication) } + ) - val imageAsset = asImageAsset(image) + Window(onCloseRequest = ::exitApplication, icon = icon) { Image( - bitmap = imageAsset, + painter = icon, contentDescription = "Icon", modifier = Modifier.fillMaxSize() ) } - - val current = AppManager.focusedWindow - if (current != null) { - current.setIcon(image) - } -} - -fun getWindowIcon(): BufferedImage { - var image: BufferedImage? = null - try { - image = ImageIO.read(File("sample.png")) - } catch (e: Exception) { - // image file does not exist - } - - if (image == null) { - image = BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB) - } - - return image -} - -fun asImageAsset(image: BufferedImage): ImageBitmap { - val baos = ByteArrayOutputStream() - ImageIO.write(image, "png", baos) - - return Image.makeFromEncoded(baos.toByteArray()).asImageBitmap() -} -``` - -![Tray icon](tray_icon.png) - -## Loading SVG images -Suppose we have an SVG image placed in the `resources/images` directory in our project. - -[SVG](../../artwork/idea-logo.svg) - -```kotlin -import androidx.compose.desktop.Window -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.svgResource - -fun main() { - Window { - Image( - painter = svgResource("images/idea-logo.svg"), - contentDescription = "Idea logo", - modifier = Modifier.fillMaxSize() - ) - } -} -``` -![Loading XML vector images](loading_svg_images.png) - -## Loading XML vector images -Compose for Desktop supports XML vector images. -XML vector images come from the world of [Android](https://developer.android.com/guide/topics/graphics/vector-drawable-resources). -We implemented it on the desktop so we can use common resources in a cross-platform application. - -SVG files can be converted to XML with [Android Studio](https://developer.android.com/studio/write/vector-asset-studio#svg) or with [third-party tools](https://www.google.com/search?q=svg+to+xml). -Suppose we have an XML image placed in the `resources/images` directory in our project. - -[SVG example](../../artwork/compose-logo.svg) - -[Converted XML](compose-logo.xml) - -```kotlin -import androidx.compose.desktop.Window -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.vectorXmlResource - -fun main() { - Window { - Image( - imageVector = vectorXmlResource("images/compose-logo.xml"), - contentDescription = "Compose logo", - modifier = Modifier.fillMaxSize() - ) - } } ``` -![Loading XML vector images](loading_xml_vector_images.png) +Tray icon \ No newline at end of file diff --git a/tutorials/Image_And_Icons_Manipulations/image_from_resources2.png b/tutorials/Image_And_Icons_Manipulations/image_from_resources2.png new file mode 100644 index 0000000000..243ad6b9e3 Binary files /dev/null and b/tutorials/Image_And_Icons_Manipulations/image_from_resources2.png differ diff --git a/tutorials/Image_And_Icons_Manipulations/loading_svg_images.png b/tutorials/Image_And_Icons_Manipulations/loading_svg_images.png deleted file mode 100644 index b27c48b670..0000000000 Binary files a/tutorials/Image_And_Icons_Manipulations/loading_svg_images.png and /dev/null differ diff --git a/tutorials/Image_And_Icons_Manipulations/loading_xml_vector_images.png b/tutorials/Image_And_Icons_Manipulations/loading_xml_vector_images.png deleted file mode 100644 index 2c716a45b9..0000000000 Binary files a/tutorials/Image_And_Icons_Manipulations/loading_xml_vector_images.png and /dev/null differ diff --git a/tutorials/Image_And_Icons_Manipulations/window_icon.png b/tutorials/Image_And_Icons_Manipulations/window_icon.png index d4089bf8b0..7dd2db0120 100755 Binary files a/tutorials/Image_And_Icons_Manipulations/window_icon.png and b/tutorials/Image_And_Icons_Manipulations/window_icon.png differ diff --git a/tutorials/Keyboard/README.md b/tutorials/Keyboard/README.md index ee94c9abe6..e22d940846 100644 --- a/tutorials/Keyboard/README.md +++ b/tutorials/Keyboard/README.md @@ -22,25 +22,27 @@ It works the same as Compose for Android, for details see [API Reference](https: The most common use case is to define keyboard handlers for active controls like `TextField`. You can use both `onKeyEvent` and `onPreviewKeyEvent` but the last one is usually preferable to define shortcuts while it guarantees you that key events will not be consumed by children components. Here is an example: ```kotlin -import androidx.compose.desktop.Window import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material.Text import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text import androidx.compose.material.TextField +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.key.* -import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.unit.dp -import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.isCtrlPressed +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.singleWindowApplication @OptIn(ExperimentalComposeUiApi::class) -fun main() = Window(title = "Compose for Desktop", size = IntSize(300, 300)) { +fun main() = singleWindowApplication { MaterialTheme { var consumedText by remember { mutableStateOf(0) } var text by remember { mutableStateOf("") } @@ -51,12 +53,12 @@ fun main() = Window(title = "Compose for Desktop", size = IntSize(300, 300)) { onValueChange = { text = it }, modifier = Modifier.onPreviewKeyEvent { when { - (it.isMetaPressed && it.key == Key.Minus) -> { + (it.isCtrlPressed && it.key == Key.Minus) -> { consumedText -= text.length text = "" true } - (it.isMetaPressed && it.key == Key.Equals) -> { + (it.isCtrlPressed && it.key == Key.Equals) -> { consumedText += text.length text = "" true @@ -70,19 +72,15 @@ fun main() = Window(title = "Compose for Desktop", size = IntSize(300, 300)) { } ``` +Note the annotation `@OptIn(ExperimentalComposeUiApi::class)`. Some keys related APIs are still an experimental feature of Compose, and later API changes are possible. So it requires the use of a special annotation to emphasize the experimental nature of the code. -Note the annotation `@OptIn(ExperimentalKeyInput::class)`. Some keys related APIs are still an experimental feature of Compose, and later API changes are possible. So it requires the use of a special annotation to emphasize the experimental nature of the code. - -![keyInputFilter](keyInputFilter.gif) +keyInputFilter ## Window-scoped events -`LocalAppWindow` instances have a `keyboard` property. It is possible to use it to define keyboard event handlers that are always active in the current window. You also can get window instance for popups. Again, you possibly want to use `onPreviewKeyEvent` here to intercept events. Here is an example: +`Window`,`singleWindowApplication` and `Dialog` functions have a `onPreviewKeyEvent` and a `onKeyEvent` properties. It is possible to use them to define keyboard event handlers that are always active in the current window. You possibly want to use `onPreviewKeyEvent` here to intercept events. Here is an example: ``` kotlin -import androidx.compose.desktop.AppWindow -import androidx.compose.desktop.LocalAppWindow -import androidx.compose.desktop.Window import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -91,32 +89,41 @@ import androidx.compose.material.Button import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.input.key.Key -import androidx.compose.ui.input.key.isMetaPressed +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.isCtrlPressed import androidx.compose.ui.input.key.isShiftPressed import androidx.compose.ui.input.key.key -import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.input.key.type import androidx.compose.ui.unit.dp -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.singleWindowApplication + +private var cleared by mutableStateOf(false) @OptIn(ExperimentalComposeUiApi::class) -fun main() = Window(title = "Compose for Desktop", size = IntSize(300, 300)) { - MaterialTheme { - var cleared by remember { mutableStateOf(false) } - LocalAppWindow.current.keyboard.onKeyEvent = { - if (it.isMetaPressed && it.isShiftPressed && it.key == Key.C) { - cleared = true - true - } else { - false - } +fun main() = singleWindowApplication( + onKeyEvent = { + if ( + it.isCtrlPressed && + it.isShiftPressed && + it.key == Key.C && + it.type == KeyEventType.KeyDown + ) { + cleared = true + true + } else { + false } - + } +) { + MaterialTheme { if (cleared) { Text("The App was cleared!") } else { @@ -128,28 +135,32 @@ fun main() = Window(title = "Compose for Desktop", size = IntSize(300, 300)) { @OptIn(ExperimentalComposeUiApi::class) @Composable fun App() { + var isDialogOpen by remember { mutableStateOf(false) } + + if (isDialogOpen) { + Dialog( + onCloseRequest = { isDialogOpen = false }, + onPreviewKeyEvent = { + if (it.key == Key.Escape && it.type == KeyEventType.KeyDown) { + isDialogOpen = false + true + } else { + false + } + }) { + Text("I'm dialog!") + } + } + Column(Modifier.fillMaxSize(), Arrangement.spacedBy(5.dp)) { Button( modifier = Modifier.padding(4.dp), - onClick = { - AppWindow(size = IntSize(200, 200)).also { window -> - window.keyboard.onPreviewKeyEvent = { - if (it.key == Key.Escape) { - window.close() - true - } else { - false - } - } - }.show { - Text("I'm popup!") - } - } + onClick = { isDialogOpen = true } ) { - Text("Open popup") + Text("Open dialog") } } } ``` -![window_keyboard](window_keyboard.gif) +window_keyboard diff --git a/tutorials/Mouse_Events/README.md b/tutorials/Mouse_Events/README.md index 69411100d1..2c76118bb4 100644 --- a/tutorials/Mouse_Events/README.md +++ b/tutorials/Mouse_Events/README.md @@ -13,25 +13,28 @@ Click listeners are available in both Compose on Android and Compose for Desktop so code like this will work on both platforms: ```kotlin -import androidx.compose.desktop.Window import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material.Text +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.singleWindowApplication -fun main() = Window(title = "Compose for Desktop", size = IntSize(400, 400)) { - var count = remember { mutableStateOf(0) } +fun main() = singleWindowApplication { + var count by remember { mutableStateOf(0) } Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxWidth()) { - var text = remember { mutableStateOf("Click magenta box!") } + var text by remember { mutableStateOf("Click magenta box!") } Column { @OptIn(ExperimentalFoundationApi::class) Box( @@ -41,23 +44,23 @@ fun main() = Window(title = "Compose for Desktop", size = IntSize(400, 400)) { .fillMaxHeight(0.2f) .combinedClickable( onClick = { - text.value = "Click! ${count.value++}" + text = "Click! ${count++}" }, onDoubleClick = { - text.value = "Double click! ${count.value++}" + text = "Double click! ${count++}" }, onLongClick = { - text.value = "Long click! ${count.value++}" + text = "Long click! ${count++}" } ) ) - Text(text = text.value, fontSize = 40.sp) + Text(text = text, fontSize = 40.sp) } } } ``` -![Application running](mouse_click.gif) +Application running ### Mouse move listeners @@ -66,132 +69,132 @@ the following code will only work with Compose for Desktop. Let's create a window and install a pointer move filter on it that changes the background color according to the mouse pointer position: ```kotlin -import androidx.compose.desktop.Window import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerMoveFilter -import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.window.singleWindowApplication -fun main() = Window(title = "Compose for Desktop", size = IntSize(400, 400)) { - var color = remember { mutableStateOf(Color(0, 0, 0)) } +fun main() = singleWindowApplication { + var color by remember { mutableStateOf(Color(0, 0, 0)) } Box( - modifier = Modifier - .wrapContentSize(Alignment.Center) - .fillMaxSize() - .background(color = color.value) - .pointerMoveFilter( - onMove = { - color.value = Color(it.x.toInt() % 256, it.y.toInt() % 256, 0) - false - } - ) + modifier = Modifier + .wrapContentSize(Alignment.Center) + .fillMaxSize() + .background(color = color) + .pointerMoveFilter( + onMove = { + color = Color(it.x.toInt() % 256, it.y.toInt() % 256, 0) + false + } + ) ) } ``` -![Application running](mouse_move.gif) +Application running ### Mouse enter listeners Compose for Desktop also supports pointer enter and exit handlers, like this: ```kotlin -import androidx.compose.desktop.Window import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material.Text +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerMoveFilter import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.singleWindowApplication -fun main() = Window(title = "Compose for Desktop", size = IntSize(400, 400)) { - Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { +fun main() = singleWindowApplication { + Column( + Modifier.background(Color.White), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { repeat(10) { index -> - var active = remember { mutableStateOf(false) } + var active by remember { mutableStateOf(false) } Text( modifier = Modifier .fillMaxWidth() - .background(color = if (active.value) Color.Green else Color.White) + .background(color = if (active) Color.Green else Color.White) .pointerMoveFilter( onEnter = { - active.value = true + active = true false }, onExit = { - active.value = false + active = false false } ), fontSize = 30.sp, - fontStyle = if (active.value) FontStyle.Italic else FontStyle.Normal, + fontStyle = if (active) FontStyle.Italic else FontStyle.Normal, text = "Item $index" ) } } } ``` -![Application running](mouse_enter.gif) +Application running ### Mouse right/middle clicks and keyboard modifiers -While first-class support for pointer type-specific data, like pressed mouse buttons, is still in development in Compose, there is an available raw AWT mouse event object in Compose for Desktop, that can be used as a workaround when you need advanced functionality. +Compose for Desktop contains desktop-only `Modifier.mouseClickable`, where data about pressed mouse buttons and keyboard modifiers is available. This is an experimental API, which means that it's likely to be changed before release. ```kotlin -import androidx.compose.desktop.Window -import androidx.compose.foundation.gestures.forEachGesture -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.ExperimentalDesktopApi +import androidx.compose.foundation.mouseClickable import androidx.compose.material.Text +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.input.pointer.AwaitPointerEventScope -import androidx.compose.ui.input.pointer.PointerEvent -import androidx.compose.ui.input.pointer.changedToDown -import androidx.compose.ui.input.pointer.consumeDownChange -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.unit.IntSize -import java.awt.event.MouseEvent - -fun main() = Window(title = "Compose for Desktop", size = IntSize(400, 400)) { - var lastEvent by remember { mutableStateOf(null) } - Column { - Text( - text = "Custom button", - modifier = Modifier.pointerInput(Unit) { - forEachGesture { - awaitPointerEventScope { - lastEvent = awaitEventFirstDown().also { - it.changes.forEach { it.consumeDownChange() } - }.mouseEvent - } +import androidx.compose.ui.window.singleWindowApplication + +@OptIn(ExperimentalDesktopApi::class) +fun main() = singleWindowApplication { + var clickableText by remember { mutableStateOf("Click me!") } + + Text( + modifier = Modifier.mouseClickable( + onClick = { + clickableText = buildString { + append("Buttons pressed:\n") + append("primary: ${buttons.isPrimaryPressed}\t") + append("secondary: ${buttons.isSecondaryPressed}\t") + append("tertiary: ${buttons.isTertiaryPressed}\t") + + append("\n\nKeyboard modifiers pressed:\n") + + append("alt: ${keyboardModifiers.isAltPressed}\t") + append("ctrl: ${keyboardModifiers.isCtrlPressed}\t") + append("meta: ${keyboardModifiers.isMetaPressed}\t") + append("shift: ${keyboardModifiers.isShiftPressed}\t") } } - ) - Text("Mouse event: ${lastEvent?.paramString()}") - } - -} - -private suspend fun AwaitPointerEventScope.awaitEventFirstDown(): PointerEvent { - var event: PointerEvent - do { - event = awaitPointerEvent() - } while ( - !event.changes.all { it.changedToDown() } + ), + text = clickableText ) - return event } ``` -![Application running](mouse_event.gif) \ No newline at end of file +Application running + +If you need more information about events there is an available raw AWT mouse event object in `mouseEvent` property of `PointerEvent` \ No newline at end of file diff --git a/tutorials/Mouse_Events/mouse_click.gif b/tutorials/Mouse_Events/mouse_click.gif index af4a909ef6..c9eaa6b08e 100644 Binary files a/tutorials/Mouse_Events/mouse_click.gif and b/tutorials/Mouse_Events/mouse_click.gif differ diff --git a/tutorials/Mouse_Events/mouse_enter.gif b/tutorials/Mouse_Events/mouse_enter.gif index f0f1e17228..cfc625f2b1 100644 Binary files a/tutorials/Mouse_Events/mouse_enter.gif and b/tutorials/Mouse_Events/mouse_enter.gif differ diff --git a/tutorials/Mouse_Events/mouse_event.gif b/tutorials/Mouse_Events/mouse_event.gif index 0cde3989a5..e2b73febbf 100644 Binary files a/tutorials/Mouse_Events/mouse_event.gif and b/tutorials/Mouse_Events/mouse_event.gif differ diff --git a/tutorials/Mouse_Events/mouse_move.gif b/tutorials/Mouse_Events/mouse_move.gif index 7ba17915e9..1ad33e4ed2 100644 Binary files a/tutorials/Mouse_Events/mouse_move.gif and b/tutorials/Mouse_Events/mouse_move.gif differ diff --git a/tutorials/Native_distributions_and_local_execution/README.md b/tutorials/Native_distributions_and_local_execution/README.md index f910c194eb..93756e9e69 100755 --- a/tutorials/Native_distributions_and_local_execution/README.md +++ b/tutorials/Native_distributions_and_local_execution/README.md @@ -116,7 +116,7 @@ The following formats available for the supported operating systems: By default, Apple does not allow users to execute unsigned applications downloaded from the internet. Users attempting to run such applications will be faced with an error like this: -![](attrs-error.png) + See [our tutorial](/tutorials/Signing_and_notarization_on_macOS/README.md) on how to sign and notarize your application. @@ -256,6 +256,61 @@ compose.desktop { } ``` +## Packaging resources + +There are multiple ways to package and load resources with Compose for Desktop. + +### JVM resource loading + +Since Compose for Desktop uses JVM platform, you can load resources from a jar file using `java.lang.Class` API. Put a file under `src/main/resources`, +then access it using [Class::getResource](https://docs.oracle.com/en/java/javase/15/docs/api/java.base/java/lang/Class.html#getResource(java.lang.String)) +or [Class::getResourceAsStream](https://docs.oracle.com/en/java/javase/15/docs/api/java.base/java/lang/Class.html#getResourceAsStream(java.lang.String)). + +### Adding files to packaged application + +In some cases putting and reading resources from jar files might be inconvenient. +Or you may want to include a target specific asset (e.g. a file, that is included only +into a macOS package, but not into a Windows one). + +Compose Gradle plugin can be configured to put additional +resource files under an installation directory. + +To do so, specify a root resource directory via DSL: +``` +compose.desktop { + application { + mainClass = "MainKt" + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageVersion = "1.0.0" + + appResourcesRootDir.set(project.layout.projectDirectory.dir("resources")) + } + } +} +``` +In the example above a root resource directory is set to `/resources`. + +Compose Gradle plugin will include all files under the following subdirectories: +1. Files from `/common` will be included into all packages. +2. Files from `/` will be included only into packages for +a specific OS. Possible values for `` are: `windows`, `macos`, `linux`. +3. Files from `/-` will be included only into packages for + a specific combination of OS and CPU architecture. Possible values for `` are: `x64` and `arm64`. +For example, files from `/macos-arm64` will be included only into packages built for Apple Silicon +Macs. + +Included resources can be accessed via `compose.application.resources.dir` system property: +``` +import java.io.File + +val resourcesDir = File(System.getProperty("compose.application.resources.dir")) + +fun main() { + println(resourcesDir.resolve("resource.txt").readText()) +} +``` + ## Customizing content The plugin can configure itself, when either `org.jetbrains.kotlin.jvm` or `org.jetbrains.kotlin.multiplatform` plugins @@ -370,6 +425,7 @@ The following platform-specific options are available (see the section `Specifying package version` for details); * `pkgPackageVersion = "PKG_VERSION"` — a pkg-specific package version (see the section `Specifying package version` for details); + * `infoPlist` — see the section `Customizing Info.plist on macOS` for details; * Windows: * `console = true` adds a console launcher for the application; * `dirChooser = true` enables customizing the installation path during installation; @@ -383,7 +439,7 @@ The following platform-specific options are available (see the section `Specifying package version` for details); * `exePackageVersion = "EXE_VERSION"` — a pkg-specific package version (see the section `Specifying package version` for details); - + ## App icon The app icon needs to be provided in OS-specific formats: @@ -408,3 +464,81 @@ compose.desktop { } } ``` + +## Customizing Info.plist on macOS + +We aim to support important platform-specific customization use-cases via declarative DSL. +However, the provided DSL is not enough sometimes. If you need to specify `Info.plist` +values, that are not modeled in the DSL, you can work around by specifying a piece +of raw XML, that will be appended to the application's `Info.plist`. + +### Example: deep linking into macOS apps + +1. Specify a custom URL scheme: +``` kotlin +// build.gradle.kts +compose.desktop { + application { + mainClass = "MainKt" + nativeDistributions { + targetFormats(TargetFormat.Dmg) + packageName = "Deep Linking Example App" + macOS { + bundleID = "org.jetbrains.compose.examples.deeplinking" + infoPlist { + extraKeysRawXml = macExtraPlistKeys + } + } + } + } +} + +val macExtraPlistKeys: String + get() = """ + CFBundleURLTypes + + + CFBundleURLName + Example deep link + CFBundleURLSchemes + + compose + + + + """ +""" +``` + +2. Use `java.awt.Desktop` to set up a URI handler: +``` kotlin +// src/main/main.kt + +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.window.singleWindowApplication +import java.awt.Desktop + +fun main() { + var text by mutableStateOf("Hello, World!") + + try { + Desktop.getDesktop().setOpenURIHandler { event -> + text = "Open URI: " + event.uri + } + } catch (e: UnsupportedOperationException) { + println("setOpenURIHandler is unsupported") + } + + singleWindowApplication { + MaterialTheme { + Text(text) + } + } +} +``` +3. Run `./gradlew runDistributable`. +4. Links like `compose://foo/bar` are now redirected from a browser to your application. diff --git a/tutorials/Navigation/README.md b/tutorials/Navigation/README.md index 9306d8d22e..336a696c9f 100644 --- a/tutorials/Navigation/README.md +++ b/tutorials/Navigation/README.md @@ -289,21 +289,21 @@ Application and Root initialisation: ``` kotlin import androidx.compose.desktop.DesktopTheme -import androidx.compose.desktop.Window import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.window.singleWindowApplication import com.arkivanov.decompose.extensions.compose.jetbrains.rememberRootComponent -fun main() { - Window("Navigation tutorial") { - Surface(modifier = Modifier.fillMaxSize()) { - MaterialTheme { - DesktopTheme { - RootUi(root()) // Render the Root and its children - } +fun main() = singleWindowApplication( + title = "Navigation tutorial" +) { + Surface(modifier = Modifier.fillMaxSize()) { + MaterialTheme { + DesktopTheme { + RootUi(root()) // Render the Root and its children } } } diff --git a/tutorials/Swing_Integration/README.md b/tutorials/Swing_Integration/README.md index 34bea20745..9cf8ad61a8 100644 --- a/tutorials/Swing_Integration/README.md +++ b/tutorials/Swing_Integration/README.md @@ -9,34 +9,31 @@ In this tutorial, we will show you how to use ComposePanel and SwingPanel in you ComposePanel lets you create a UI using Compose for Desktop in a Swing-based UI. To achieve this you need to create an instance of ComposePanel, add it to your Swing layout, and describe the composition inside `setContent`. You may also need to clear the CFD application events via `AppManager.setEvents`. ```kotlin -import androidx.compose.desktop.AppManager -import androidx.compose.desktop.ComposePanel import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.width -import androidx.compose.material.Text import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Button import androidx.compose.material.Surface +import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Alignment -import androidx.compose.ui.graphics.Color import androidx.compose.ui.Modifier +import androidx.compose.ui.awt.ComposePanel +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import java.awt.BorderLayout import java.awt.Dimension -import java.awt.event.ActionEvent -import java.awt.event.ActionListener -import javax.swing.JFrame import javax.swing.JButton +import javax.swing.JFrame import javax.swing.SwingUtilities import javax.swing.WindowConstants @@ -44,17 +41,7 @@ val northClicks = mutableStateOf(0) val westClicks = mutableStateOf(0) val eastClicks = mutableStateOf(0) -fun main() { - // explicitly clear the application events - AppManager.setEvents( - onAppStart = null, - onAppExit = null, - onWindowsEmpty = null - ) - SwingComposeWindow() -} - -fun SwingComposeWindow() = SwingUtilities.invokeLater { +fun main() = SwingUtilities.invokeLater { val window = JFrame() // creating ComposePanel @@ -62,10 +49,9 @@ fun SwingComposeWindow() = SwingUtilities.invokeLater { window.defaultCloseOperation = WindowConstants.EXIT_ON_CLOSE window.title = "SwingComposeWindow" - - window.contentPane.add(actionButton("NORTH", { northClicks.value++ }), BorderLayout.NORTH) - window.contentPane.add(actionButton("WEST", { westClicks.value++ }), BorderLayout.WEST) - window.contentPane.add(actionButton("EAST", { eastClicks.value++ }), BorderLayout.EAST) + window.contentPane.add(actionButton("NORTH", action = { northClicks.value++ }), BorderLayout.NORTH) + window.contentPane.add(actionButton("WEST", action = { westClicks.value++ }), BorderLayout.WEST) + window.contentPane.add(actionButton("EAST", action = { eastClicks.value++ }), BorderLayout.EAST) window.contentPane.add( actionButton( text = "SOUTH/REMOVE COMPOSE", @@ -85,19 +71,14 @@ fun SwingComposeWindow() = SwingUtilities.invokeLater { } window.setSize(800, 600) - window.setVisible(true) + window.isVisible = true } -fun actionButton(text: String, action: (() -> Unit)? = null): JButton { +fun actionButton(text: String, action: () -> Unit): JButton { val button = JButton(text) - button.setToolTipText("Tooltip for $text button.") - button.setPreferredSize(Dimension(100, 100)) - button.addActionListener(object : ActionListener { - public override fun actionPerformed(e: ActionEvent) { - action?.invoke() - } - }) - + button.toolTipText = "Tooltip for $text button." + button.preferredSize = Dimension(100, 100) + button.addActionListener { action() } return button } @@ -145,15 +126,13 @@ fun Counter(text: String, counter: MutableState) { } ``` -![IntegrationWithSwing](screenshot.png) +IntegrationWithSwing -## Adding a Swing component to CFD composition using SwingPanel. +## Adding a Swing component to CFD composition using SwingPanel SwingPanel lets you create a UI using Swing in a Compose-based UI. To achieve this you need to create Swing `JComponent` in the `factory` parameter of `SwingPanel`. ```kotlin -import androidx.compose.desktop.SwingPanel -import androidx.compose.desktop.Window import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -169,56 +148,53 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.awt.SwingPanel import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.singleWindowApplication import java.awt.Component -import java.awt.Dimension -import java.awt.event.ActionEvent -import java.awt.event.ActionListener import javax.swing.BoxLayout import javax.swing.JButton import javax.swing.JPanel -fun main() { - Window { - val counter = remember { mutableStateOf(0) } +fun main() = singleWindowApplication { + val counter = remember { mutableStateOf(0) } - val inc: () -> Unit = { counter.value++ } - val dec: () -> Unit = { counter.value-- } + val inc: () -> Unit = { counter.value++ } + val dec: () -> Unit = { counter.value-- } - Box( - modifier = Modifier.fillMaxWidth().height(60.dp).padding(top = 20.dp), - contentAlignment = Alignment.Center - ) { - Text("Counter: ${counter.value}") - } - - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center + Box( + modifier = Modifier.fillMaxWidth().height(60.dp).padding(top = 20.dp), + contentAlignment = Alignment.Center + ) { + Text("Counter: ${counter.value}") + } + + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier.padding(top = 80.dp, bottom = 20.dp) ) { - Column( - modifier = Modifier.padding(top = 80.dp, bottom = 20.dp) - ) { - Button("1. Compose Button: increment", inc) - Spacer(modifier = Modifier.height(20.dp)) - - SwingPanel( - background = Color.White, - modifier = Modifier.size(270.dp, 90.dp), - factory = { - JPanel().apply { - setLayout(BoxLayout(this, BoxLayout.Y_AXIS)) - add(actionButton("1. Swing Button: decrement", dec)) - add(actionButton("2. Swing Button: decrement", dec)) - add(actionButton("3. Swing Button: decrement", dec)) - } + Button("1. Compose Button: increment", inc) + Spacer(modifier = Modifier.height(20.dp)) + + SwingPanel( + background = Color.White, + modifier = Modifier.size(270.dp, 90.dp), + factory = { + JPanel().apply { + layout = BoxLayout(this, BoxLayout.Y_AXIS) + add(actionButton("1. Swing Button: decrement", dec)) + add(actionButton("2. Swing Button: decrement", dec)) + add(actionButton("3. Swing Button: decrement", dec)) } - ) + } + ) - Spacer(modifier = Modifier.height(20.dp)) - Button("2. Compose Button: increment", inc) - } + Spacer(modifier = Modifier.height(20.dp)) + Button("2. Compose Button: increment", inc) } } } @@ -235,18 +211,14 @@ fun Button(text: String = "", action: (() -> Unit)? = null) { fun actionButton( text: String, - action: (() -> Unit)? = null + action: () -> Unit ): JButton { val button = JButton(text) - button.setAlignmentX(Component.CENTER_ALIGNMENT) - button.addActionListener(object : ActionListener { - public override fun actionPerformed(e: ActionEvent) { - action?.invoke() - } - }) + button.alignmentX = Component.CENTER_ALIGNMENT + button.addActionListener { action() } return button } ``` -![IntegrationWithSwing](swing_panel.gif) +IntegrationWithSwing diff --git a/tutorials/Tab_Navigation/README.md b/tutorials/Tab_Navigation/README.md new file mode 100644 index 0000000000..4f72904f2b --- /dev/null +++ b/tutorials/Tab_Navigation/README.md @@ -0,0 +1,312 @@ +# Tabbing navigation and keyboard focus + +## What is covered + +In this tutorial, we will show you how to use tabbing navigation between components via keyboard shortcuts `tab` and `shift + tab`. + +## Default `Next/Previous` tabbing navigation + +By default, `Next/Previous` tabbed navigation moves focus in composition order (in order of appearance), to see how this works, we can use some of the components that are already focusable by default:`TextField`, `OutlinedTextField`, `BasicTextField`, `CircularProgressIndicator`, `LinearProgressIndicator`. + +```kotlin +import androidx.compose.ui.window.application +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.WindowState +import androidx.compose.ui.window.WindowSize +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.Spacer +import androidx.compose.material.OutlinedTextField +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +fun main() = application { + Window( + state = WindowState(size = WindowSize(350.dp, 500.dp)), + onCloseRequest = ::exitApplication + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier.padding(50.dp) + ) { + for (x in 1..5) { + val text = remember { mutableStateOf("") } + OutlinedTextField( + value = text.value, + singleLine = true, + onValueChange = { text.value = it } + ) + Spacer(modifier = Modifier.height(20.dp)) + } + } + } + } +} +``` + +default-tab-nav + +To make a non-focusable component focusable, you need to apply `Modifier.focusable()` modifier to the component. + +```kotlin +import androidx.compose.ui.window.application +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.WindowState +import androidx.compose.ui.window.WindowSize +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.foundation.focusable +import androidx.compose.foundation.interaction.collectIsFocusedAsState +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.Spacer +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.lerp +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.type +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.onPreviewKeyEvent + +fun main() = application { + Window( + state = WindowState(size = WindowSize(350.dp, 450.dp)), + onCloseRequest = ::exitApplication + ) { + MaterialTheme( + colors = MaterialTheme.colors.copy( + primary = Color(10, 132, 232), + secondary = Color(150, 232, 150) + ) + ) { + val clicks = remember { mutableStateOf(0) } + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier.padding(40.dp) + ) { + Text(text = "Clicks: ${clicks.value}") + Spacer(modifier = Modifier.height(20.dp)) + for (x in 1..5) { + FocusableButton("Button $x", { clicks.value++ }) + Spacer(modifier = Modifier.height(20.dp)) + } + } + } + } + } +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun FocusableButton( + text: String = "", + onClick: () -> Unit = {}, + size: IntSize = IntSize(200, 35) +) { + val keyPressedState = remember { mutableStateOf(false) } + val interactionSource = remember { MutableInteractionSource() } + val colors = ButtonDefaults.buttonColors( + backgroundColor = if (interactionSource.collectIsFocusedAsState().value) { + if (keyPressedState.value) + lerp(MaterialTheme.colors.secondary, Color(64, 64, 64), 0.3f) + else + MaterialTheme.colors.secondary + } else { + MaterialTheme.colors.primary + } + ) + Button( + onClick = onClick, + interactionSource = interactionSource, + modifier = Modifier.size(size.width.dp, size.height.dp) + .onPreviewKeyEvent { + if ( + it.key == Key.Enter || + it.key == Key.Spacebar + ) { + when (it.type) { + KeyEventType.KeyDown -> { + keyPressedState.value = true + } + KeyEventType.KeyUp -> { + keyPressedState.value = false + onClick.invoke() + } + } + } + false + } + .focusable(interactionSource = interactionSource), + colors = colors + ) { + Text(text = text) + } +} +``` + +focusable-buttons + +## Custom ordering +To move focus in custom order we need to create a `FocusRequester` and apply the `Modifier.focusOrder` modifier to each component you want to navigate. + +- `FocusRequester` sends requests to change focus. +- `Modifier.focusOrder` is used to specify a custom focus traversal order. + +In the example below, we simply create a `FocusRequester` list and create text fields for each `FocusRequester` in the list. Each text field sends a focus request to the previous and next text field in the list when using the `shift + tab` or `tab` keyboard shortcut in reverse order. + +```kotlin +import androidx.compose.ui.window.application +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.WindowState +import androidx.compose.ui.window.WindowSize +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.Spacer +import androidx.compose.material.OutlinedTextField +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusOrder +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +fun main() = application { + Window( + state = WindowState(size = WindowSize(350.dp, 500.dp)), + onCloseRequest = ::exitApplication + ) { + val itemsList = remember { List(5) { FocusRequester() } } + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier.padding(50.dp) + ) { + itemsList.forEachIndexed { index, item -> + val text = remember { mutableStateOf("") } + OutlinedTextField( + value = text.value, + singleLine = true, + onValueChange = { text.value = it }, + modifier = Modifier.focusOrder(item) { + // reverse order + next = if (index - 1 < 0) itemsList.last() else itemsList[index - 1] + previous = if (index + 1 == itemsList.size) itemsList.first() else itemsList[index + 1] + } + ) + Spacer(modifier = Modifier.height(20.dp)) + } + } + } + } +} +``` + +reverse-order + +## Making component focused + +To make a component focused, we need to create a `FocusRequester` and apply the `Modifier.focusRequester` modifier to the component you want to focus on. With `FocusRequester`, we can request focus, as in the example below: + +```kotlin +import androidx.compose.ui.window.application +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.WindowState +import androidx.compose.ui.window.WindowSize +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.Spacer +import androidx.compose.material.Button +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Text +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +fun main() = application { + Window( + state = WindowState(size = WindowSize(350.dp, 450.dp)), + onCloseRequest = ::exitApplication + ) { + val buttonFocusRequester = remember { FocusRequester() } + val textFieldFocusRequester = remember { FocusRequester() } + val focusState = remember { mutableStateOf(false) } + val text = remember { mutableStateOf("") } + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier.padding(50.dp) + ) { + Button( + onClick = { + focusState.value = !focusState.value + if (focusState.value) { + textFieldFocusRequester.requestFocus() + } else { + buttonFocusRequester.requestFocus() + } + }, + modifier = Modifier.fillMaxWidth() + .focusRequester(buttonFocusRequester) + .focusable() + ) { + Text(text = "Focus switcher") + } + Spacer(modifier = Modifier.height(20.dp)) + OutlinedTextField( + value = text.value, + singleLine = true, + onValueChange = { text.value = it }, + modifier = Modifier + .focusRequester(textFieldFocusRequester) + ) + } + } + } +} +``` + +reverse-order \ No newline at end of file diff --git a/tutorials/Tab_Navigation/default-tab-nav.gif b/tutorials/Tab_Navigation/default-tab-nav.gif new file mode 100644 index 0000000000..6ecf2900db Binary files /dev/null and b/tutorials/Tab_Navigation/default-tab-nav.gif differ diff --git a/tutorials/Tab_Navigation/focus-switcher.gif b/tutorials/Tab_Navigation/focus-switcher.gif new file mode 100644 index 0000000000..6b56649595 Binary files /dev/null and b/tutorials/Tab_Navigation/focus-switcher.gif differ diff --git a/tutorials/Tab_Navigation/focusable-button.gif b/tutorials/Tab_Navigation/focusable-button.gif new file mode 100644 index 0000000000..b63ca41dcd Binary files /dev/null and b/tutorials/Tab_Navigation/focusable-button.gif differ diff --git a/tutorials/Tab_Navigation/reverse-order.gif b/tutorials/Tab_Navigation/reverse-order.gif new file mode 100644 index 0000000000..b044399080 Binary files /dev/null and b/tutorials/Tab_Navigation/reverse-order.gif differ diff --git a/tutorials/Tray_Notifications_MenuBar/README.md b/tutorials/Tray_Notifications_MenuBar/README.md deleted file mode 100755 index 582d132445..0000000000 --- a/tutorials/Tray_Notifications_MenuBar/README.md +++ /dev/null @@ -1,311 +0,0 @@ -# Menu, tray, notifications - -## What is covered - -In this tutorial we'll show you how to work with the system tray, create an application menu bar and a window-specific menu bar, and send system notifications using Compose for Desktop. - -## Tray - -You can add an application icon to the system tray. You can also send notifications to the user using the system tray. There are 3 types of notification: - -1. notify - simple notification -2. warn - warning notification -3. error - error notification - -```kotlin -import androidx.compose.desktop.AppManager -import androidx.compose.desktop.Window -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material.Text -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.window.v1.MenuItem -import androidx.compose.ui.window.v1.Tray -import java.awt.Color -import java.awt.image.BufferedImage - -fun main() { - val count = mutableStateOf(0) - Window( - icon = getMyAppIcon() - ) { - DisposableEffect(Unit) { - val tray = Tray().apply { - icon(getTrayIcon()) - menu( - MenuItem( - name = "Increment value", - onClick = { - count.value++ - } - ), - MenuItem( - name = "Send notification", - onClick = { - notify("Notification", "Message from MyApp!") - } - ), - MenuItem( - name = "Exit", - onClick = { - AppManager.exit() - } - ) - ) - } - onDispose { - tray.remove() - } - } - - // content - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Text(text = "Value: ${count.value}") - } - } -} - -fun getMyAppIcon(): BufferedImage { - val size = 256 - val image = BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB) - val graphics = image.createGraphics() - graphics.setColor(Color.green) - graphics.fillOval(size / 4, 0, size / 2, size) - graphics.setColor(Color.blue) - graphics.fillOval(0, size / 4, size, size / 2) - graphics.setColor(Color.red) - graphics.fillOval(size / 4, size / 4, size / 2, size / 2) - graphics.dispose() - return image -} - -fun getTrayIcon(): BufferedImage { - val size = 256 - val image = BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB) - val graphics = image.createGraphics() - graphics.setColor(Color.orange) - graphics.fillOval(0, 0, size, size) - graphics.dispose() - return image -} -``` - -![Tray](tray.gif) - -## Notifier -You can send system notifications with Notifier without using the system tray. -Notifier also has 3 types of notification: - -1. notify - simple notification -2. warn - warning notification -3. error - error notification - -```kotlin -import androidx.compose.desktop.Window -import androidx.compose.foundation.layout.Column -import androidx.compose.material.Text -import androidx.compose.material.Button -import androidx.compose.ui.window.Notifier -import java.awt.Color -import java.awt.image.BufferedImage - -fun main() { - val message = "Some message!" - val notifier = Notifier() - Window( - icon = getMyAppIcon() - ) { - Column { - Button(onClick = { notifier.notify("Notification.", message) }) { - Text(text = "Notify") - } - Button(onClick = { notifier.warn("Warning.", message) }) { - Text(text = "Warning") - } - Button(onClick = { notifier.error("Error.", message) }) { - Text(text = "Error") - } - } - } -} - -fun getMyAppIcon() : BufferedImage { - val size = 256 - val image = BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB) - val graphics = image.createGraphics() - graphics.setColor(Color.green) - graphics.fillOval(size / 4, 0, size / 2, size) - graphics.setColor(Color.blue) - graphics.fillOval(0, size / 4, size, size / 2) - graphics.setColor(Color.red) - graphics.fillOval(size / 4, size / 4, size / 2, size / 2) - graphics.dispose() - return image -} -``` - -![Notifier](notifier.gif) - -## MenuBar - -MenuBar is used to create and customize the common context menu of the application or a particular window. -To create a common context menu for all the application windows, you need to configure the AppManager. - -```kotlin -import androidx.compose.desktop.AppManager -import androidx.compose.desktop.Window -import androidx.compose.material.Text -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.Modifier -import androidx.compose.ui.window.v1.KeyStroke -import androidx.compose.ui.window.v1.MenuItem -import androidx.compose.ui.window.v1.Menu -import androidx.compose.ui.window.v1.MenuBar - -@OptIn(ExperimentalComposeUiApi::class) -fun main() { - // To use Apple global menu. - System.setProperty("apple.laf.useScreenMenuBar", "true") - - val action = mutableStateOf("Last action: None") - - AppManager.setMenu( - MenuBar( - Menu( - name = "Actions", - MenuItem( - name = "About", - onClick = { action.value = "Last action: About (Command + I)" }, - shortcut = KeyStroke(Key.I) - ), - MenuItem( - name = "Exit", - onClick = { AppManager.exit() }, - shortcut = KeyStroke(Key.X) - ) - ), - Menu( - name = "File", - MenuItem( - name = "Copy", - onClick = { action.value = "Last action: Copy (Command + C)" }, - shortcut = KeyStroke(Key.C) - ), - MenuItem( - name = "Paste", - onClick = { action.value = "Last action: Paste (Command + V)" }, - shortcut = KeyStroke(Key.V) - ) - ) - ) - ) - - Window { - // content - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Text(text = action.value) - } - } -} -``` - -![Application MenuBar](app_menubar.gif) - -You can create a MenuBar for a specific window, and have the other windows use the defined MenuBar. - -```kotlin -import androidx.compose.desktop.AppManager -import androidx.compose.desktop.Window -import androidx.compose.material.Text -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material.Button -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.Modifier -import androidx.compose.ui.window.v1.KeyStroke -import androidx.compose.ui.window.v1.MenuItem -import androidx.compose.ui.window.v1.Menu -import androidx.compose.ui.window.v1.MenuBar -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.IntSize - -@OptIn(ExperimentalComposeUiApi::class) -fun main() { - // To use Apple global menu. - System.setProperty("apple.laf.useScreenMenuBar", "true") - - val action = mutableStateOf("Last action: None") - - Window( - menuBar = MenuBar( - Menu( - name = "Actions", - MenuItem( - name = "About", - onClick = { action.value = "Last action: About (Command + I)" }, - shortcut = KeyStroke(Key.I) - ), - MenuItem( - name = "Exit", - onClick = { AppManager.exit() }, - shortcut = KeyStroke(Key.X) - ) - ), - Menu( - name = "File", - MenuItem( - name = "Copy", - onClick = { action.value = "Last action: Copy (Command + C)" }, - shortcut = KeyStroke(Key.C) - ), - MenuItem( - name = "Paste", - onClick = { action.value = "Last action: Paste (Command + V)" }, - shortcut = KeyStroke(Key.V) - ) - ) - ) - ) { - // content - Button( - onClick = { - Window( - title = "Another window", - size = IntSize(350, 200), - location = IntOffset(100, 100), - centered = false - ) { - - } - } - ) { - Text(text = "New window") - } - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Text(text = action.value) - } - } -} -``` - -![Window MenuBar](window_menubar.gif) diff --git a/tutorials/Tray_Notifications_MenuBar/app_menubar.gif b/tutorials/Tray_Notifications_MenuBar/app_menubar.gif deleted file mode 100644 index 6f186df333..0000000000 Binary files a/tutorials/Tray_Notifications_MenuBar/app_menubar.gif and /dev/null differ diff --git a/tutorials/Tray_Notifications_MenuBar/notifier.gif b/tutorials/Tray_Notifications_MenuBar/notifier.gif deleted file mode 100644 index 9ab6638f77..0000000000 Binary files a/tutorials/Tray_Notifications_MenuBar/notifier.gif and /dev/null differ diff --git a/tutorials/Tray_Notifications_MenuBar/tray.gif b/tutorials/Tray_Notifications_MenuBar/tray.gif deleted file mode 100644 index 4f42bcb2a1..0000000000 Binary files a/tutorials/Tray_Notifications_MenuBar/tray.gif and /dev/null differ diff --git a/tutorials/Tray_Notifications_MenuBar/window_menubar.gif b/tutorials/Tray_Notifications_MenuBar/window_menubar.gif deleted file mode 100644 index 8c08c60e28..0000000000 Binary files a/tutorials/Tray_Notifications_MenuBar/window_menubar.gif and /dev/null differ diff --git a/tutorials/Tray_Notifications_MenuBar_new/README.md b/tutorials/Tray_Notifications_MenuBar_new/README.md index 0d6f7b9744..6f07bd165e 100644 --- a/tutorials/Tray_Notifications_MenuBar_new/README.md +++ b/tutorials/Tray_Notifications_MenuBar_new/README.md @@ -1,4 +1,4 @@ -# Menu, tray, notifications (new Composable API, experimental) +# Menu, tray, notifications ## What is covered @@ -16,22 +16,23 @@ You can add an application icon to the system tray. You can also send notificati import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.Text +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.window.Notification import androidx.compose.ui.window.Tray import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberTrayState -import java.awt.Color -import java.awt.image.BufferedImage -@OptIn(ExperimentalComposeUiApi::class) fun main() = application { var count by remember { mutableStateOf(0) } var isOpen by remember { mutableStateOf(true) } @@ -39,13 +40,13 @@ fun main() = application { if (isOpen) { Window( onCloseRequest = ::exitApplication, - icon = remember { getMyAppIcon() } + icon = MyAppIcon ) { val trayState = rememberTrayState() val notification = Notification("Notification", "Message from MyApp!") Tray( state = trayState, - icon = remember { getTrayIcon() }, + icon = TrayIcon, menu = { Item( "Increment value", @@ -73,38 +74,32 @@ fun main() = application { modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { - Text(text = "Value: ${count}") + Text(text = "Value: $count") } } } } -fun getMyAppIcon(): BufferedImage { - val size = 256 - val image = BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB) - val graphics = image.createGraphics() - graphics.color = Color.green - graphics.fillOval(size / 4, 0, size / 2, size) - graphics.color = Color.blue - graphics.fillOval(0, size / 4, size, size / 2) - graphics.color = Color.red - graphics.fillOval(size / 4, size / 4, size / 2, size / 2) - graphics.dispose() - return image +object MyAppIcon : Painter() { + override val intrinsicSize = Size(256f, 256f) + + override fun DrawScope.onDraw() { + drawOval(Color.Green, Offset(size.width / 4, 0f), Size(size.width / 2f, size.height)) + drawOval(Color.Blue, Offset(0f, size.height / 4), Size(size.width, size.height / 2f)) + drawOval(Color.Red, Offset(size.width / 4, size.height / 4), Size(size.width / 2f, size.height / 2f)) + } } -fun getTrayIcon(): BufferedImage { - val size = 256 - val image = BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB) - val graphics = image.createGraphics() - graphics.color = Color.orange - graphics.fillOval(0, 0, size, size) - graphics.dispose() - return image +object TrayIcon : Painter() { + override val intrinsicSize = Size(256f, 256f) + + override fun DrawScope.onDraw() { + drawOval(Color(0xFFFFA500)) + } } ``` -![](tray.gif) +Tray ## MenuBar @@ -121,57 +116,67 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyShortcut import androidx.compose.ui.window.MenuBar import androidx.compose.ui.window.Window import androidx.compose.ui.window.application @OptIn(ExperimentalComposeUiApi::class) -fun main() { - // Currently we use Swing's menu under the hood, so we need to set this property to change the look and feel of the menu on Windows/Linux - System.setProperty("skiko.rendering.laf.global", "true") - - application { - var action by remember { mutableStateOf("Last action: None") } - var isOpen by remember { mutableStateOf(true) } - - if (isOpen) { - var isSubmenuShowing by remember { mutableStateOf(false) } - - Window(onCloseRequest = { isOpen = false }) { - MenuBar { - Menu("Actions") { - Item( - if (isSubmenuShowing) "Hide advanced settings" else "Show advanced settings", - onClick = { - isSubmenuShowing = !isSubmenuShowing - } - ) - if (isSubmenuShowing) { - Menu("Settings") { - Item("Setting 1", onClick = { action = "Last action: Setting 1" }) - Item("Setting 2", onClick = { action = "Last action: Setting 2" }) - } +fun main() = application { + var action by remember { mutableStateOf("Last action: None") } + var isOpen by remember { mutableStateOf(true) } + + if (isOpen) { + var isSubmenuShowing by remember { mutableStateOf(false) } + + Window(onCloseRequest = { isOpen = false }) { + MenuBar { + Menu("File", mnemonic = 'F') { + Item("Copy", onClick = { action = "Last action: Copy" }, shortcut = KeyShortcut(Key.C, ctrl = true)) + Item("Paste", onClick = { action = "Last action: Paste" }, shortcut = KeyShortcut(Key.V, ctrl = true)) + } + Menu("Actions", mnemonic = 'A') { + CheckboxItem( + "Advanced settings", + checked = isSubmenuShowing, + onCheckedChange = { + isSubmenuShowing = !isSubmenuShowing + } + ) + if (isSubmenuShowing) { + Menu("Settings") { + Item("Setting 1", onClick = { action = "Last action: Setting 1" }) + Item("Setting 2", onClick = { action = "Last action: Setting 2" }) } - Separator() - Item("About", onClick = { action = "Last action: About" }) - Item("Exit", onClick = { isOpen = false }) - } - Menu("File") { - Item("Copy", onClick = { action = "Last action: Copy" }) - Item("Paste", onClick = { action = "Last action: Paste" },) } + Separator() + Item("About", icon = TrayIcon, onClick = { action = "Last action: About" }) + Item("Exit", onClick = { isOpen = false }, shortcut = KeyShortcut(Key.Escape), mnemonic = 'E') } + } - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Text(text = action) - } + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text(text = action) } } } } + +object TrayIcon : Painter() { + override val intrinsicSize = Size(256f, 256f) + + override fun DrawScope.onDraw() { + drawOval(Color(0xFFFFA500)) + } +} ``` -![](window_menubar.gif) + diff --git a/tutorials/Web/Building_UI/README.md b/tutorials/Web/Building_UI/README.md index 367d72b3cd..72f4d06a95 100644 --- a/tutorials/Web/Building_UI/README.md +++ b/tutorials/Web/Building_UI/README.md @@ -1,6 +1,6 @@ # Building the UI with Compose Web -**The API is experimental, and breaking changes can be expected** +**The API is not finalized, and breaking changes can be expected** ## Introduction @@ -62,7 +62,7 @@ If you want to apply styles to text, it needs to be wrapped in a container with ``` kotlin Span( - attrs = { style { color("red") } } // inline style + attrs = { style { color(Color.red) } } // inline style ) { Text("Red text") } @@ -202,7 +202,7 @@ fun main() { Text("Arbitrary text") Span({ - style { color("red") } // inline style + style { color(Color.red) } // inline style }) { Text("Red text") } diff --git a/tutorials/Web/Events_Handling/README.md b/tutorials/Web/Events_Handling/README.md index c857fc364e..60fed77606 100644 --- a/tutorials/Web/Events_Handling/README.md +++ b/tutorials/Web/Events_Handling/README.md @@ -1,6 +1,6 @@ # Events handling in Compose Web -**The API is experimental, and breaking changes can be expected** +**The API is not finalized, and breaking changes can be expected** You can add event listeners in the `attrs` block: @@ -8,11 +8,11 @@ You can add event listeners in the `attrs` block: ``` kotlin Button( attrs = { - onClick { wrappedMouseEvent -> - // wrappedMouseEvent is of `WrappedMouseEvent` type - println("button clicked at ${wrappedMouseEvent.movementX}, ${wrappedMouseEvent.movementY}") + onClick { event -> + // event is of `SyntheticMouseEvent` type + println("button clicked at ${event.movementX}, ${event.movementY}") - val nativeEvent = wrappedMouseEvent.nativeEvent // [MouseEvent](https://developer.mozilla.org/en/docs/Web/API/MouseEvent) + val nativeEvent = event.nativeEvent // [MouseEvent](https://developer.mozilla.org/en/docs/Web/API/MouseEvent) } } ) { @@ -37,22 +37,19 @@ TextArea( #### Other event handlers -For events that don't have their own configuration functions in the `attrs` block, you can use `addEventListener` with the `name` of the event, `options`, and an pass an `eventListener` which receives a `WrappedEvent`. In this example, we're defining the behavior of a `Form` element when it triggers the `submit` event: +For events that don't have their own configuration functions in the `attrs` block, you can use `addEventListener` with the `name` of the event, `options`, and an pass an `eventListener` which receives a `SyntheticEvent`. In this example, we're defining the behavior of a `Form` element when it triggers the `submit` event: ``` kotlin Form(attrs = { this.addEventListener("submit") { console.log("Hello, Submit!") - it.nativeEvent.preventDefault() + it.preventDefault() } }) ``` -Your event handlers receive wrapped events that inherit from `GenericWrappedEvent`, which also provides access to the underlying `nativeEvent` – the actual event created by JS runtime - -https://developer.mozilla.org/en-US/docs/Web/API/Event - -There are more event listeners supported out of a box. We plan to add the documentation for them later on. In the meantime, you can find all supported event listeners in the [source code](https://github.com/JetBrains/androidx/blob/compose-web-main/compose/web/src/jsMain/kotlin/androidx/compose/web/attributes/EventsListenerBuilder.kt). +There are more event listeners supported out of a box. We plan to add the documentation for them later on. In the meantime, you can find all supported event listeners in the [source code](https://github.com/JetBrains/compose-jb/blob/master/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/EventsListenerBuilder.kt). ### Runnable example @@ -67,11 +64,8 @@ fun main() { renderComposable(rootElementId = "root") { Button( attrs = { - onClick { wrappedMouseEvent -> - // wrappedMouseEvent is of `WrappedMouseEvent` type - println("button clicked at ${wrappedMouseEvent.movementX}, ${wrappedMouseEvent.movementY}") - - val nativeEvent = wrappedMouseEvent.nativeEvent + onClick { event -> + println("button clicked at ${event.movementX}, ${event.movementY}") } } ) { diff --git a/tutorials/Web/Getting_Started/README.md b/tutorials/Web/Getting_Started/README.md index ae8a9d43c8..9fe4d56aea 100644 --- a/tutorials/Web/Getting_Started/README.md +++ b/tutorials/Web/Getting_Started/README.md @@ -1,6 +1,6 @@ # Getting Started With Compose for Web -**The API is experimental, and breaking changes can be expected** +**The API is not finalized, and breaking changes can be expected** ## Introduction @@ -25,7 +25,7 @@ The project wizard doesn't support Compose for web projects yet, so we need to p - Tick `Kotlin DSL build script` - Tick `Kotlin/Multiplatform` -![](create-mpp.png) + #### 2. Update `settings.gradle.kts`: @@ -42,14 +42,15 @@ pluginManagement { ``` kotlin // Add compose gradle plugin plugins { - kotlin("multiplatform") version "1.5.10" - id("org.jetbrains.compose") version "0.5.0-build228" + kotlin("multiplatform") version "1.5.21" + id("org.jetbrains.compose") version "0.5.0-build270" } // Add maven repositories repositories { mavenCentral() maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + google() } // Enable JS(IR) target and add dependencies @@ -134,8 +135,8 @@ Use the command line to run: Or run it from the IDE: -![](run_project.png) + The browser will open `localhost:8080`: -![](run_result.png) + diff --git a/tutorials/Web/README.md b/tutorials/Web/README.md index 4deaa1e73a..b0795d75c9 100644 --- a/tutorials/Web/README.md +++ b/tutorials/Web/README.md @@ -1,5 +1,5 @@ # Compose for Web -**The API is experimental, and breaking changes can be expected** +**The API is not finalized, and breaking changes can be expected** ### Content: diff --git a/tutorials/Web/Style_Dsl/README.md b/tutorials/Web/Style_Dsl/README.md index 0fc24de954..e478bee424 100644 --- a/tutorials/Web/Style_Dsl/README.md +++ b/tutorials/Web/Style_Dsl/README.md @@ -1,5 +1,5 @@ # Style DSL in Compose Web -**The API is experimental, and breaking changes can be expected** +**The API is not finalized, and breaking changes can be expected** ## Introduction In this tutorial we have a look at how to style the components using the Style DSL. It’s a typesafe DSL for style sheets, which you can use to express CSS rules in your Kotlin code, and even modify styles based on the state of your Compose application. @@ -101,11 +101,11 @@ object AppStylesheet : StyleSheet() { // A convenient way to create a class selector // AppStylesheet.container can be used as a class in component attrs val container by style { - color("red") + color(Color.red) // hover selector for a class self + hover() style { // self is a selector for `container` - color("green") + color(Color.green) } } } @@ -135,9 +135,9 @@ object AppStylesheet : StyleSheet() { The style DSL also provides support for CSS variables. ``` kotlin -object MyVariables : CSSVariables { +object MyVariables { // declare a variable - val contentBackgroundColor by variable() + val contentBackgroundColor by variable() } object MyStyleSheet: StyleSheet() { @@ -169,6 +169,30 @@ import org.jetbrains.compose.web.css.* import org.jetbrains.compose.web.dom.* import org.jetbrains.compose.web.renderComposable +object MyVariables { + // declare a variable + val contentBackgroundColor by variable() +} + +object MyStyleSheet: StyleSheet() { + + val container by style { + //set variable's value for the `container` scope + MyVariables.contentBackgroundColor(Color("blue")) + } + + val content by style { + // get the value + backgroundColor(MyVariables.contentBackgroundColor.value()) + } + + val contentWithDefaultBgColor by style { + // default value can be provided as well + // default value is used when the value is not previously set + backgroundColor(MyVariables.contentBackgroundColor.value(Color("#333"))) + } +} + object AppStylesheet : StyleSheet() { val container by style { // container is a class display(DisplayStyle.Flex) diff --git a/tutorials/Web/Using_Effects/README.md b/tutorials/Web/Using_Effects/README.md new file mode 100644 index 0000000000..b846daa109 --- /dev/null +++ b/tutorials/Web/Using_Effects/README.md @@ -0,0 +1,187 @@ +# Using Effects in Compose Web +**The API is not finalized, and breaking changes can be expected** + +## Introduction +Compose for Web introduces a few dom-specific effects on top of [existing effects from Compose](https://developer.android.com/jetpack/compose/side-effects). + + +### ref in AttrsBuilder + +Under the hood, `ref` uses [DisposableEffect](https://developer.android.com/jetpack/compose/side-effects#disposableeffect) + +`ref` can be used to retrieve a reference to a html element. +The lambda that `ref` takes in is not Composable. It will be called only once when an element added into a composition. +Likewise, the lambda passed in `onDispose` will be called only once when an element leaves the composition. + +``` kotlin +Div(attrs = { + ref { htmlDivElement -> + // htmlDivElement is a reference to the HTMLDivElement + onDispose { + // add clean up code here + } + } +}) { + // Content() +} +``` + +Only one `ref` can be used per element. Calling it more than once will dismiss earlier calls. + +For example, `ref` can be used to add and remove some event listeners not provided by compose-web from the box. + +### DisposableRefEffect + +Under the hood, `DisposableRefEffect` uses [DisposableEffect](https://developer.android.com/jetpack/compose/side-effects#disposableeffect) + +`DisposableRefEffect` is similar to `ref`, since it also provides a reference to an element. At the same time it has few differences. + +- `DisposableRefEffect` can be added only within a content lambda of an element, while `ref` can be used only in `attrs` scope. +- Unlike `ref`, `DisposableRefEffect` can be used as many times as needed and every effect will be unique. +- DisposableRefEffect can be used with a `key` and without it. When it's used with a `key: Any`, the effect will be disposed and reset when `key` value changes. When it's used without a key, then it behaves like `ref` - the effect gets called only once when an element enters the composition, and it's disposed only when the element leaves the composition. + + +``` kotlin +Div { + // without a key + DisposableRefEffect { htmlDivElement -> + // htmlDivElement is a reference to the HTMLDivElement + onDispose { + // add clean up code here + } + } +} + + +var state by remember { mutableStateOf(1) } + +Div { + // with a key. + // The effect will be called for every new state's value + DisposableRefEffect(state) { htmlDivElement -> + // htmlDivElement is a reference to the HTMLDivElement + onDispose { + // add clean up code here + } + } +} +``` + +### DomSideEffect + +Under the hood, `DomSideEffect` uses [SideEffect](https://developer.android.com/jetpack/compose/side-effects#sideeffect-publish) + +`DomSideEffect` as well as `DisposableRefEffect` can be used with a key and without it. + +Unlike `DisposableRefEffect`, `DomSideEffect` without a key is invoked on every successful recomposition. +With a `key`, it will be invoked only when the `key` value changes. + +Same as [SideEffect](https://developer.android.com/jetpack/compose/side-effects#sideeffect-publish), `DomSideEffect` can be helpful when there is a need to update objects not managed by Compose. +In case of web, it often involves updating HTML nodes, therefore `DomSideEffect` provides a reference to an element in the lambda. + +### Code Sample using effects + +The code below showcases how it's possible to use non-composable components in Compose by applying `DomSideEffect` and `DisposableRefEffect`. + +```kotlin +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.runtime.Composable +import kotlinx.browser.document +import org.jetbrains.compose.web.css.* +import org.jetbrains.compose.web.dom.* +import org.jetbrains.compose.web.renderComposable +import org.w3c.dom.HTMLElement +import org.w3c.dom.HTMLParagraphElement + + +// Here we pretend that `RedBoldTextNotComposableRenderer` +// wraps a UI logic provided by 3rd party library that doesn't use Compose + +object RedBoldTextNotComposableRenderer { + fun unmountFrom(root: HTMLElement) { + root.removeChild(root.firstChild!!) + } + + fun mountIn(root: HTMLElement) { + val pElement = document.createElement("p") as HTMLParagraphElement + pElement.setAttribute("style", "color: red; font-weight: bold;") + root.appendChild(pElement) + } + + fun renderIn(root: HTMLElement, text: String) { + (root.firstChild as HTMLParagraphElement).innerText = text + } +} + +// Here we define a Composable wrapper for the above code. Here we use DomSideEffect and DisposableRefEffect. +@Composable // @param `show: Boolean` was left here intentionally for the sake of the example +fun ComposableWrapperForRedBoldTextFrom3rdPartyLib(state: Int, show: Boolean) { + Div(attrs = { + style { + backgroundColor(Color.lightgray) + width(100.px) + minHeight(40.px) + padding(30.px) + } + }) { + if (!show) { + Text("No content rendered by the 3rd party library") + } + + Div { + if (show) { + // Update the content rendered by "non-compose library" according to the `state` + DomSideEffect(state) { div -> + RedBoldTextNotComposableRenderer.renderIn(div, "Value = $state") + } + } + + DisposableRefEffect(show) { div -> + if (show) { + // Let "non-compose library" control the part of the page. + // The content of this div is independent of Compose. + // It will be managed by RedBoldTextNotComposableRenderer + RedBoldTextNotComposableRenderer.mountIn(div) + } + onDispose { + if (show) { + // Clean up the html created/managed by "non-compose library" + RedBoldTextNotComposableRenderer.unmountFrom(div) + } + } + } + } + } +} + +fun main() { + var state by mutableStateOf(0) + var showUncontrolledElements by mutableStateOf(false) + + renderComposable(rootElementId = "root") { + + ComposableWrapperForRedBoldTextFrom3rdPartyLib(state = state, show = showUncontrolledElements) + + Div { + Label(forId = "checkbox") { + Text("Show/hide text rendered by 3rd party library") + } + + CheckboxInput(checked = false) { + id("checkbox") + onInput { + showUncontrolledElements = it.value + } + } + } + + Button(attrs = { + onClick { state += 1 } + }) { + Text("Incr. count ($state)") + } + } +} +``` diff --git a/tutorials/Window_API/README.md b/tutorials/Window_API/README.md deleted file mode 100755 index c673e1599d..0000000000 --- a/tutorials/Window_API/README.md +++ /dev/null @@ -1,502 +0,0 @@ -# Top level windows management - -## What is covered - -In this tutorial we will show you how to work with windows using Compose for Desktop. - -## Windows creation - -The main class for creating windows is AppWindow. The easiest way to create and launch a new window is to use an instance of the AppWindow class and call its method `show()`. You can see an example below: - -```kotlin -import androidx.compose.desktop.AppWindow -import javax.swing.SwingUtilities.invokeLater - -fun main() = invokeLater { - AppWindow().show { - // Content - } -} -``` - -Note that AppWindow should be created in AWT Event Thread. Instead of calling `invokeLater()` explicitly you can use `Window` DSL: -```kotlin -import androidx.compose.desktop.Window - -fun main() { - Window { - // Content - } -} -``` - -There are two types of window – modal and regular. Below are the functions for creating each type of window: - -1. Window – regular window type. -2. Dialog – modal window type. Such a window locks its parent window until the user completes working with it and closes the modal window. - -You can see an example of both types of window below. - -```kotlin -import androidx.compose.desktop.Window -import androidx.compose.material.Button -import androidx.compose.material.Text -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.window.v1.Dialog - -fun main() { - Window { - val dialogState = remember { mutableStateOf(false) } - - Button(onClick = { dialogState.value = true }) { - Text(text = "Open dialog") - } - - if (dialogState.value) { - Dialog( - onDismissRequest = { dialogState.value = false } - ) { - // Dialog's content - } - } - } -} -``` - -## Window attributes - -Each window has following parameters, all of them could be omitted and have default values: - -1. title – window title -2. size – initial window size -3. location – initial window position -4. centered – set the window to the center of the display -5. icon – window icon -6. menuBar – window context menu -7. undecorated – disable native border and title bar of the window -8. resizable – makes the window resizable or unresizable -9. events – window events -10. onDismissEvent – event when removing the window content from a composition - -An example of using window parameters in the creation step: - -```kotlin -import androidx.compose.desktop.AppManager -import androidx.compose.desktop.Window -import androidx.compose.desktop.WindowEvents -import androidx.compose.material.Text -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material.Button -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.window.v1.MenuItem -import androidx.compose.ui.window.v1.KeyStroke -import androidx.compose.ui.window.v1.Menu -import androidx.compose.ui.window.v1.MenuBar -import java.awt.Color -import java.awt.image.BufferedImage - -@OptIn(ExperimentalComposeUiApi::class) -fun main() { - val count = mutableStateOf(0) - val windowPos = mutableStateOf(IntOffset.Zero) - - Window( - title = "MyApp", - size = IntSize(400, 250), - location = IntOffset(100, 100), - centered = false, // true - by default - icon = getMyAppIcon(), - menuBar = MenuBar( - Menu( - name = "Actions", - MenuItem( - name = "Increment value", - onClick = { - count.value++ - }, - shortcut = KeyStroke(Key.I) - ), - MenuItem( - name = "Exit", - onClick = { AppManager.exit() }, - shortcut = KeyStroke(Key.X) - ) - ) - ), - undecorated = true, // false - by default - events = WindowEvents( - onRelocate = { location -> - windowPos.value = location - } - ) - ) { - // content - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Column { - Text(text = "Location: ${windowPos.value} Value: ${count.value}") - Button( - onClick = { - AppManager.exit() - } - ) { - Text(text = "Close app") - } - } - } - } -} - -fun getMyAppIcon() : BufferedImage { - val size = 256 - val image = BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB) - val graphics = image.createGraphics() - graphics.color = Color.orange - graphics.fillOval(0, 0, size, size) - graphics.dispose() - return image -} -``` - -![Window attributes](window_attr.gif) - -## Window properties - -AppWindow parameters correspond to the following properties: - -1. title – window title -2. width – window width -3. height – window height -4. x – position of the left top corner of the window along the X axis -5. y – position of the left top corner of the window along the Y axis -6. resizable - returns `true` if the window resizable, `false` otherwise -7. icon – window icon image -8. events – window events - -To get the properties of a window, it is enough to have a link to the current or specific window. There are two ways to get the current focused window: - -1. Using the global environment: - -```kotlin -import androidx.compose.desktop.LocalAppWindow -import androidx.compose.desktop.Window -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material.Button -import androidx.compose.material.Text -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.IntOffset - -fun main() { - val windowPos = mutableStateOf(IntOffset.Zero) - - Window { - val current = LocalAppWindow.current - - // Content - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Column { - Text(text = "Location: ${windowPos.value}") - Button( - onClick = { - windowPos.value = IntOffset(current.x, current.y) - } - ) { - Text(text = "Print window location") - } - } - } - } -} -``` - -2. Using AppManager: - -```kotlin -import androidx.compose.desktop.AppManager -import androidx.compose.desktop.Window -import androidx.compose.material.Text -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material.Button -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.IntOffset - -fun main() { - val windowPos = mutableStateOf(IntOffset.Zero) - - Window { - // Content - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Column { - Text(text = "Location: ${windowPos.value}") - Button( - onClick = { - val current = AppManager.focusedWindow - if (current != null) { - windowPos.value = IntOffset(current.x, current.y) - } - } - ) { - Text(text = "Print window location") - } - } - } - } -} -``` - -![Window properties](current_window.gif) - -Using the following methods, you can change the properties of the AppWindow: - -1. setTitle(title: String) – window title -2. setSize(width: Int, height: Int) – window size -3. setLocation(x: Int, y: Int) – window position -4. setWindowCentered() – set the window to the center of the display -5. setIcon(image: BufferedImage?) – window icon -6. setMenuBar(menuBar: MenuBar) - window menu bar - -```kotlin -import androidx.compose.desktop.LocalAppWindow -import androidx.compose.desktop.Window -import androidx.compose.material.Text -import androidx.compose.material.Button - -fun main() { - Window { - val window = LocalAppWindow.current - // Content - Button( - onClick = { - window.setWindowCentered() - } - ) { - Text(text = "Center the window") - } - } -} -``` - -![Window properties](center_the_window.gif) - -## Methods - -Using the following methods, you can change the state of the AppWindow: - -1. show(parentComposition: CompositionReference? = null, content: @Composable () -> Unit) – shows a window with the given Compose content, -`parentComposition` is the parent of this window's composition. -2. close() - closes the window. -3. minimize() - minimizes the window to the taskbar. If the window is in fullscreen mode this method is ignored. -4. maximize() - maximizes the window to fill all available screen space. If the window is in fullscreen mode this method is ignored. -5. makeFullscreen() - switches the window to fullscreen mode if the window is resizable. If the window is in fullscreen mode `minimize()` and `maximize()` methods are ignored. -6. restore() - restores the normal state and size of the window after maximizing/minimizing/fullscreen mode. - -You can know about window state via properties below: - -1. isMinimized - returns true if the window is minimized, false otherwise. -2. isMaximized - returns true if the window is maximized, false otherwise. -3. isFullscreen - returns true if the window is in fullscreen state, false otherwise. - -```kotlin -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.Spacer -import androidx.compose.desktop.AppManager -import androidx.compose.desktop.AppWindow -import androidx.compose.material.Button -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import javax.swing.SwingUtilities.invokeLater - -fun main() = invokeLater { - AppWindow().show { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Column( - modifier = Modifier.padding(top = 20.dp, bottom = 20.dp) - ) { - Button("Minimize", { AppManager.focusedWindow?.minimize() }) - Button("Maximize", { AppManager.focusedWindow?.maximize() }) - Button("Fullscreen", { AppManager.focusedWindow?.makeFullscreen() }) - Button("Restore", { AppManager.focusedWindow?.restore() }) - Spacer(modifier = Modifier.height(20.dp)) - Button("Close", { AppManager.focusedWindow?.close() }) - } - } - } -} - -@Composable -fun Button(text: String = "", action: (() -> Unit)? = null) { - Button( - modifier = Modifier.size(150.dp, 30.dp), - onClick = { action?.invoke() } - ) { - Text(text) - } - Spacer(modifier = Modifier.height(10.dp)) -} -``` - -![Window state](window_state.gif) - -## Window events - -Events can be defined using the events parameter in the window creation step or redefine using the events property at runtime. -Actions can be assigned to the following window events: - -1. onOpen – event during window opening -2. onClose – event during window closing -3. onMinimize – event during window minimizing -4. onMaximize – event during window maximizing -5. onRestore – event during restoring window size after window minimize/maximize -6. onFocusGet – event when window gets focus -7. onFocusLost – event when window loses focus -8. onResize – event on window resize (argument is window size as IntSize) -9. onRelocate – event of the window reposition on display (argument is window position as IntOffset) - -```kotlin -import androidx.compose.desktop.Window -import androidx.compose.desktop.WindowEvents -import androidx.compose.material.Text -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.IntSize - -fun main() { - val windowSize = mutableStateOf(IntSize.Zero) - val focused = mutableStateOf(false) - - Window( - events = WindowEvents( - onFocusGet = { focused.value = true }, - onFocusLost = { focused.value = false }, - onResize = { size -> - windowSize.value = size - } - ) - ) { - // Content - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Text(text = "Size: ${windowSize.value} Focused: ${focused.value}") - } - } -} -``` - -![Window events](focus_the_window.gif) - -## AppManager - -The AppManager singleton is used to customize the behavior of the entire application. Its main features: - -1. Description of common application events -``` kotlin -AppManager.setEvents( - onAppStart = { println("onAppStart") }, // Invoked before the first window is created - onAppExit = { println("onAppExit") } // Invoked after all windows are closed -) -``` -2. Customization of common application context menu -``` kotlin -AppManager.setMenu( - getCommonAppMenuBar() // Custom function that returns MenuBar -) -``` -3. Access to the application windows list -``` kotlin -val windows = AppManager.windows -``` -4. Getting the current focused window -``` kotlin -val current = AppManager.focusedWindow -``` -5. Application exit -``` kotlin -AppManager.exit() // Closes all windows -``` - -## Access to Swing components - -Compose for Desktop is tightly integrated with Swing at the top-level windows layer. For more detailed customization, you can access the JFrame class: - -```kotlin -import androidx.compose.desktop.AppManager -import androidx.compose.desktop.Window -import androidx.compose.material.Text -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material.Button -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier - -fun main() { - val scaleFactor = mutableStateOf(0.0) - Window { - // Content - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Column { - Button( - onClick = { - val current = AppManager.focusedWindow - if (current != null) { - val jFrame = current.window - // Do whatever you want with it - scaleFactor.value = jFrame.graphicsConfiguration.defaultTransform.scaleX - } - } - ) { - Text(text = "Check display scaling factor") - } - Text(text = "Scaling factor: ${scaleFactor.value}") - } - } - } -} -``` - -![Access to Swing components](scaling_factor.jpg) diff --git a/tutorials/Window_API/center_the_window.gif b/tutorials/Window_API/center_the_window.gif deleted file mode 100644 index 9b8eb0aeda..0000000000 Binary files a/tutorials/Window_API/center_the_window.gif and /dev/null differ diff --git a/tutorials/Window_API/current_window.gif b/tutorials/Window_API/current_window.gif deleted file mode 100644 index dc0807761b..0000000000 Binary files a/tutorials/Window_API/current_window.gif and /dev/null differ diff --git a/tutorials/Window_API/focus_the_window.gif b/tutorials/Window_API/focus_the_window.gif deleted file mode 100644 index 2d62993bb4..0000000000 Binary files a/tutorials/Window_API/focus_the_window.gif and /dev/null differ diff --git a/tutorials/Window_API/scaling_factor.jpg b/tutorials/Window_API/scaling_factor.jpg deleted file mode 100644 index 6faa9eda03..0000000000 Binary files a/tutorials/Window_API/scaling_factor.jpg and /dev/null differ diff --git a/tutorials/Window_API/window_attr.gif b/tutorials/Window_API/window_attr.gif deleted file mode 100644 index a0d59d1c25..0000000000 Binary files a/tutorials/Window_API/window_attr.gif and /dev/null differ diff --git a/tutorials/Window_API/window_state.gif b/tutorials/Window_API/window_state.gif deleted file mode 100644 index d037300757..0000000000 Binary files a/tutorials/Window_API/window_state.gif and /dev/null differ diff --git a/tutorials/Window_API_new/README.md b/tutorials/Window_API_new/README.md index ef59b6c212..e04116a3fd 100644 --- a/tutorials/Window_API_new/README.md +++ b/tutorials/Window_API_new/README.md @@ -1,4 +1,4 @@ -# Top level windows management (new Composable API, experimental) +# Top level windows management ## What is covered @@ -13,11 +13,9 @@ Top-level windows can be conditionally created in other composable functions and The main function for creating windows is `Window`. This function should be used in a Composable scope. The easiest way to create a Composable scope is to use the `application` function: ```kotlin -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.window.Window import androidx.compose.ui.window.application -@OptIn(ExperimentalComposeUiApi::class) fun main() = application { Window(onCloseRequest = ::exitApplication) { // Content @@ -33,11 +31,9 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.window.Window import androidx.compose.ui.window.application -@OptIn(ExperimentalComposeUiApi::class) fun main() = application { var fileName by remember { mutableStateOf("Untitled") } @@ -48,7 +44,7 @@ fun main() = application { } } ``` -![](window_properties.gif) +Window properties You can also close/open windows using a simple `if` statement. @@ -61,18 +57,18 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import kotlinx.coroutines.delay -@OptIn(ExperimentalComposeUiApi::class) fun main() = application { var isPerformingTask by remember { mutableStateOf(true) } + LaunchedEffect(Unit) { delay(2000) // Do some heavy lifting isPerformingTask = false } + if (isPerformingTask) { Window(onCloseRequest = ::exitApplication) { Text("Performing some tasks. Please wait!") @@ -84,7 +80,7 @@ fun main() = application { } } ``` -![](window_splash.gif) +Window splash If the window requires some custom logic on close (for example, to show a dialog), you can override the close action using `onCloseRequest`. @@ -96,16 +92,14 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Window import androidx.compose.ui.window.application -@OptIn(ExperimentalComposeUiApi::class) fun main() = application { var isOpen by remember { mutableStateOf(true) } var isAskingToClose by remember { mutableStateOf(false) } - + if (isOpen) { Window( onCloseRequest = { isAskingToClose = true } @@ -126,7 +120,7 @@ fun main() = application { } } ``` -![](ask_to_close.gif) +Ask to close If you don't need to close the window and just need to hide it (for example to the tray), you can change the `windowState.isVisible` state: ```kotlin @@ -136,16 +130,15 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.window.Tray import androidx.compose.ui.window.Window import androidx.compose.ui.window.application -import androidx.compose.ui.window.rememberWindowState import kotlinx.coroutines.delay -import java.awt.Color -import java.awt.image.BufferedImage -@OptIn(ExperimentalComposeUiApi::class) fun main() = application { var isVisible by remember { mutableStateOf(true) } @@ -166,7 +159,7 @@ fun main() = application { if (!isVisible) { Tray( - remember { getTrayIcon() }, + TrayIcon, hint = "Counter", onAction = { isVisible = true }, menu = { @@ -176,40 +169,39 @@ fun main() = application { } } -fun getTrayIcon(): BufferedImage { - val size = 256 - val image = BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB) - val graphics = image.createGraphics() - graphics.color = Color.orange - graphics.fillOval(0, 0, size, size) - graphics.dispose() - return image +object TrayIcon : Painter() { + override val intrinsicSize = Size(256f, 256f) + + override fun DrawScope.onDraw() { + drawOval(Color(0xFFFFA500)) + } } ``` -![](hide_instead_of_close.gif) +Hide instead of closing If an application has multiple windows, then it is better to put its state into a separate class and open/close window in response to `mutableStateListOf` changes (see [notepad example](https://github.com/JetBrains/compose-jb/tree/master/examples/notepad) for more complex use cases): ```kotlin import androidx.compose.runtime.Composable +import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember -import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.window.ApplicationScope import androidx.compose.ui.window.MenuBar import androidx.compose.ui.window.Window import androidx.compose.ui.window.application -@OptIn(ExperimentalComposeUiApi::class) fun main() = application { val applicationState = remember { MyApplicationState() } for (window in applicationState.windows) { - MyWindow(window) + key(window) { + MyWindow(window) + } } } -@OptIn(ExperimentalComposeUiApi::class) @Composable -private fun MyWindow( +private fun ApplicationScope.MyWindow( state: MyWindowState ) = Window(onCloseRequest = state::close, title = state.title) { MenuBar { @@ -254,7 +246,59 @@ private class MyWindowState( fun close() = close(this) } ``` -![](multiple_windows.gif) +Multiple windows + +## Function `singleWindowApplication` + +There is a simplified function for creating a single window application: +```kotlin +import androidx.compose.ui.window.singleWindowApplication + +fun main() = singleWindowApplication { + // Content +} +``` +Use it if: +- your application has only one window +- you don't need custom closing logic +- you don't need to change the window parameters after it is already created + +## Adaptive window size + +Sometimes we want to show some content as a whole without knowing in advance what exactly will be shown, meaning that we don’t know the optimal window dimensions for it. By setting one or both dimensions of your window’s WindowSize to Dp.Unspecified, Compose for Desktop will automatically adjust the initial size of your window in that dimension to accommodate its content: +```kotlin +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Text +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import androidx.compose.ui.window.rememberWindowState + +fun main() = application { + Window( + onCloseRequest = ::exitApplication, + state = rememberWindowState(width = Dp.Unspecified, height = Dp.Unspecified), + title = "Adaptive", + resizable = false + ) { + Column(Modifier.background(Color(0xFFEEEEEE))) { + Row { + Text("label 1", Modifier.size(100.dp, 100.dp).padding(10.dp).background(Color.White)) + Text("label 2", Modifier.size(150.dp, 200.dp).padding(5.dp).background(Color.White)) + Text("label 3", Modifier.size(200.dp, 300.dp).padding(25.dp).background(Color.White)) + } + } + } +} +``` +Adaptive window size ## Changing the state (maximized, minimized, fullscreen, size, position) of the window. @@ -268,7 +312,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.material.Checkbox import androidx.compose.material.Text import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window @@ -277,7 +320,6 @@ import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberWindowState -@OptIn(ExperimentalComposeUiApi::class) fun main() = application { val state = rememberWindowState(placement = WindowPlacement.Maximized) @@ -299,7 +341,7 @@ fun main() = application { Row(verticalAlignment = Alignment.CenterVertically) { Checkbox( - state.placement == WindowPlacement.Fullscreen, + state.placement == WindowPlacement.Maximized, { state.placement = if (it) { WindowPlacement.Maximized @@ -336,15 +378,14 @@ fun main() = application { } } ``` -![](state.gif) +Changing the state ## Listening the state of the window Reading the state in composition is useful when you need to update UI, but there are cases when you need to react to the state changes and send a value to another non-composable level of your application (write it to the database, for example): -``` +```kotlin import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.snapshotFlow -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.WindowSize @@ -354,20 +395,19 @@ import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -@OptIn(ExperimentalComposeUiApi::class) fun main() = application { val state = rememberWindowState() - Window(state) { + Window(onCloseRequest = ::exitApplication, state) { // Content - + LaunchedEffect(state) { snapshotFlow { state.size } .onEach(::onWindowResize) .launchIn(this) snapshotFlow { state.position } - .filterNot { it.isInitial } + .filterNot { it.isSpecified } .onEach(::onWindowRelocate) .launchIn(this) } @@ -383,34 +423,6 @@ private fun onWindowRelocate(position: WindowPosition) { } ``` -## Handle window-level shortcuts -```kotlin -import androidx.compose.material.TextField -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.input.key.key -import androidx.compose.ui.window.Window -import androidx.compose.ui.window.application - -@OptIn(ExperimentalComposeUiApi::class) -fun main() = application { - Window( - onCloseRequest = ::exitApplication, - onPreviewKeyEvent = { - when (it.key) { - Key.Escape -> { - exitApplication() - true - } - else -> false - } - } - ) { - TextField("Text", {}) - } -} -``` - ## Dialogs There are two types of window – modal and regular. Below are the functions for creating each: @@ -423,19 +435,16 @@ You can see an example of both types of window below. import androidx.compose.material.Button import androidx.compose.material.Text import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogState import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowPosition -import androidx.compose.ui.window.WindowState import androidx.compose.ui.window.application +import androidx.compose.ui.window.rememberDialogState -@OptIn(ExperimentalComposeUiApi::class) fun main() = application { Window( onCloseRequest = ::exitApplication, @@ -449,7 +458,7 @@ fun main() = application { if (isDialogOpen) { Dialog( onCloseRequest = { isDialogOpen = false }, - state = DialogState(position = WindowPosition(Alignment.Center)) + state = rememberDialogState(position = WindowPosition(Alignment.Center)) ) { // Dialog's content } @@ -461,13 +470,11 @@ fun main() = application { ## Swing interoperability Because Compose for Desktop uses Swing under the hood, it is possible to create a window using Swing directly: ```kotlin -import androidx.compose.desktop.ComposeWindow -import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.awt.ComposeWindow import java.awt.Dimension import javax.swing.JFrame import javax.swing.SwingUtilities -@OptIn(ExperimentalComposeUiApi::class) fun main() = SwingUtilities.invokeLater { ComposeWindow().apply { size = Dimension(300, 300) @@ -483,16 +490,23 @@ fun main() = SwingUtilities.invokeLater { You can also access ComposeWindow in the Composable `Window` scope: ```kotlin import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.window.Window -import androidx.compose.ui.window.application -import java.awt.Cursor - -@OptIn(ExperimentalComposeUiApi::class) -fun main() = application { - Window(onCloseRequest = ::exitApplication) { - LaunchedEffect(Unit) { - window.cursor = Cursor(Cursor.CROSSHAIR_CURSOR) +import androidx.compose.ui.window.singleWindowApplication +import java.awt.datatransfer.DataFlavor +import java.awt.dnd.DnDConstants +import java.awt.dnd.DropTarget +import java.awt.dnd.DropTargetAdapter +import java.awt.dnd.DropTargetDropEvent + +fun main() = singleWindowApplication { + LaunchedEffect(Unit) { + window.dropTarget = DropTarget().apply { + addDropTargetListener(object : DropTargetAdapter() { + override fun drop(event: DropTargetDropEvent) { + event.acceptDrop(DnDConstants.ACTION_COPY); + val fileName = event.transferable.getTransferData(DataFlavor.javaFileListFlavor) + println(fileName) + } + }) } } } @@ -505,13 +519,11 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.window.AwtWindow import androidx.compose.ui.window.application import java.awt.FileDialog import java.awt.Frame -@OptIn(ExperimentalComposeUiApi::class) fun main() = application { var isOpen by remember { mutableStateOf(true) } @@ -525,7 +537,6 @@ fun main() = application { } } -@OptIn(ExperimentalComposeUiApi::class) @Composable private fun FileDialog( parent: Frame? = null, @@ -544,3 +555,54 @@ private fun FileDialog( dispose = FileDialog::dispose ) ``` + +## Draggable window area +If you window is undecorated and you want to add a custom draggable titlebar to it (or make the whole window draggable), you can use `DraggableWindowArea`: +```kotlin +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.window.WindowDraggableArea +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application + +fun main() = application { + Window(onCloseRequest = ::exitApplication, undecorated = true) { + WindowDraggableArea { + Box(Modifier.fillMaxWidth().height(48.dp).background(Color.DarkGray)) + } + } +} +``` +Note that `WindowDraggableArea` can be used only inside `singleWindowApplication`, `Window` and `Dialog`. If you need to use it in another Composable function, pass `WindowScope` as a receiver there: +```kotlin +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.window.WindowDraggableArea +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.WindowScope +import androidx.compose.ui.window.application + +fun main() = application { + Window(onCloseRequest = ::exitApplication, undecorated = true) { + AppWindowTitleBar() + } +} + +@Composable +private fun WindowScope.AppWindowTitleBar() = WindowDraggableArea { + Box(Modifier.fillMaxWidth().height(48.dp).background(Color.DarkGray)) +} +``` +Draggable area diff --git a/tutorials/Window_API_new/adaptive.png b/tutorials/Window_API_new/adaptive.png new file mode 100644 index 0000000000..9c4fa9fdd8 Binary files /dev/null and b/tutorials/Window_API_new/adaptive.png differ diff --git a/tutorials/Window_API_new/draggable_area.gif b/tutorials/Window_API_new/draggable_area.gif new file mode 100644 index 0000000000..bc3d443d6e Binary files /dev/null and b/tutorials/Window_API_new/draggable_area.gif differ diff --git a/web/benchmark-core/build.gradle.kts b/web/benchmark-core/build.gradle.kts index a680eb117c..a24d72c0c6 100644 --- a/web/benchmark-core/build.gradle.kts +++ b/web/benchmark-core/build.gradle.kts @@ -41,14 +41,6 @@ kotlin { } } -val printBundleSize by tasks.registering { - doLast { - val jsFile = buildDir.resolve("distributions/web-benchmark-core.js") - val size = jsFile.length() - println("##teamcity[buildStatisticValue key='landingPageBundleSize' value='$size']") - } -} - val printBenchmarkResults by tasks.registering { doLast { val report = buildDir.resolve("reports/tests/jsTest/classes/BenchmarkTests.html").readText() @@ -77,4 +69,3 @@ val printBenchmarkResults by tasks.registering { } tasks.named("jsTest") { finalizedBy(printBenchmarkResults) } -tasks.named("build") { finalizedBy(printBundleSize) } \ No newline at end of file diff --git a/web/benchmark-core/src/jsMain/kotlin/com/sample/HighlightJs.kt b/web/benchmark-core/src/jsMain/kotlin/com/sample/HighlightJs.kt deleted file mode 100644 index 9b03dc49ac..0000000000 --- a/web/benchmark-core/src/jsMain/kotlin/com/sample/HighlightJs.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.sample - -import org.w3c.dom.HTMLElement - -//@JsName("hljs") -//@JsModule("highlight.js") -//@JsNonModule -class HighlightJs { - companion object { - fun highlightElement(block: HTMLElement) {} - } -} \ No newline at end of file diff --git a/web/benchmark-core/src/jsMain/kotlin/com/sample/Main.kt b/web/benchmark-core/src/jsMain/kotlin/com/sample/Main.kt deleted file mode 100644 index 6eba6af0e4..0000000000 --- a/web/benchmark-core/src/jsMain/kotlin/com/sample/Main.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.sample - -import androidx.compose.runtime.Composable -import org.jetbrains.compose.web.css.* -import org.jetbrains.compose.web.css.selectors.* -import org.jetbrains.compose.web.attributes.* -import org.jetbrains.compose.web.dom.* -import org.jetbrains.compose.web.* -import com.sample.components.* -import com.sample.content.* -import com.sample.style.AppStylesheet -import org.w3c.dom.HTMLElement - - -fun main() { - renderComposable(rootElementId = "root") { - Style(AppStylesheet) - - Layout { - Header() - MainContentLayout { - Intro() - ComposeWebLibraries() - GetStarted() - CodeSamples() - JoinUs() - } - PageFooter() - } - } -} \ No newline at end of file diff --git a/web/benchmark-core/src/jsMain/kotlin/com/sample/components/Card.kt b/web/benchmark-core/src/jsMain/kotlin/com/sample/components/Card.kt deleted file mode 100644 index eabc22b0fa..0000000000 --- a/web/benchmark-core/src/jsMain/kotlin/com/sample/components/Card.kt +++ /dev/null @@ -1,80 +0,0 @@ -package com.sample.components - -import androidx.compose.runtime.Composable -import org.jetbrains.compose.web.css.* -import org.jetbrains.compose.web.css.selectors.* -import org.jetbrains.compose.web.attributes.* -import org.jetbrains.compose.web.dom.* -import org.jetbrains.compose.web.* -import com.sample.style.* - - -data class LinkOnCard(val linkText: String, val linkUrl: String) - -@Composable -private fun CardTitle(title: String, darkTheme: Boolean = false) { - H3(attrs = { - classes(WtTexts.wtH3) - if (darkTheme) classes(WtTexts.wtH3ThemeDark) - }) { - Text(title) - } -} - -@Composable -private fun CardLink(link: LinkOnCard) { - A( - attrs = { - classes(WtTexts.wtLink, WtOffsets.wtTopOffset24) - target(ATarget.Blank) - }, - href = link.linkUrl - ) { - Text(link.linkText) - } -} - -@Composable -fun Card( - title: String, - links: List, - darkTheme: Boolean = false, - wtExtraStyleClasses: List = listOf(WtCols.wtCol6, WtCols.wtColMd6, WtCols.wtColSm12), - content: @Composable () -> Unit -) { - Div(attrs = { - classes(WtCards.wtCard, WtOffsets.wtTopOffset24) - classes(*wtExtraStyleClasses.toTypedArray()) - classes(if (darkTheme) WtCards.wtCardThemeDark else WtCards.wtCardThemeLight) - }) { - Div(attrs = { - classes(WtCards.wtCardSection, WtCards.wtVerticalFlex) - }) { - - Div(attrs = { classes(WtCards.wtVerticalFlexGrow) }) { - CardTitle(title = title, darkTheme = darkTheme) - content() - } - - links.forEach { - CardLink(it) - } - } - } -} - -@Composable -fun CardDark( - title: String, - links: List, - wtExtraStyleClasses: List = listOf(WtCols.wtCol6, WtCols.wtColMd6, WtCols.wtColSm12), - content: @Composable () -> Unit -) { - Card( - title = title, - links = links, - darkTheme = true, - wtExtraStyleClasses = wtExtraStyleClasses, - content = content - ) -} \ No newline at end of file diff --git a/web/benchmark-core/src/jsMain/kotlin/com/sample/components/Layout.kt b/web/benchmark-core/src/jsMain/kotlin/com/sample/components/Layout.kt deleted file mode 100644 index 79475a7209..0000000000 --- a/web/benchmark-core/src/jsMain/kotlin/com/sample/components/Layout.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.sample.components - -import androidx.compose.runtime.Composable -import org.jetbrains.compose.web.css.* -import org.jetbrains.compose.web.dom.* -import com.sample.style.WtContainer -import com.sample.style.WtOffsets -import com.sample.style.WtSections - -@Composable -fun Layout(content: @Composable () -> Unit) { - Div( - attrs = { - style { - display(DisplayStyle.Flex) - flexDirection(FlexDirection.Column) - height(100.percent) - margin(0.px) - property("box-sizing", "border-box") - } - } - ) { - content() - } -} - -@Composable -fun MainContentLayout(content: @Composable () -> Unit) { - Main( - attrs = { - style { - property("flex", "1 0 auto") - property("box-sizing", "border-box") - } - } - ) { - content() - } -} - -@Composable -fun ContainerInSection(sectionThemeStyleClass: String? = null, content: @Composable () -> Unit) { - Section(attrs = { - if (sectionThemeStyleClass != null) { - classes(WtSections.wtSection, sectionThemeStyleClass) - } else { - classes(WtSections.wtSection) - } - }) { - Div( - attrs = { - classes(WtContainer.wtContainer, WtOffsets.wtTopOffset96) - } - ) { - content() - } - } -} \ No newline at end of file diff --git a/web/benchmark-core/src/jsMain/kotlin/com/sample/content/AboutComposeWebLibsSection.kt b/web/benchmark-core/src/jsMain/kotlin/com/sample/content/AboutComposeWebLibsSection.kt deleted file mode 100644 index 4d96233552..0000000000 --- a/web/benchmark-core/src/jsMain/kotlin/com/sample/content/AboutComposeWebLibsSection.kt +++ /dev/null @@ -1,96 +0,0 @@ -package com.sample.content - -import androidx.compose.runtime.Composable -import org.jetbrains.compose.web.css.* -import org.jetbrains.compose.web.css.selectors.* -import org.jetbrains.compose.web.attributes.* -import org.jetbrains.compose.web.dom.* -import org.jetbrains.compose.web.* -import com.sample.components.Card -import com.sample.components.ContainerInSection -import com.sample.components.LinkOnCard -import com.sample.style.* - -data class CardWithListPresentation( - val title: String, - val list: List, - val links: List = emptyList() -) - -private fun createAboutComposeWebCards(): List { - return listOf( - CardWithListPresentation( - title = "Composable DOM API", - list = listOf( - "Express your design and layout in terms of DOM elements and HTML tags", - "Use a type-safe HTML DSL to build your UI representation", - "Get full control over the look and feel of your application by creating stylesheets with a typesafe CSS DSL", - "Integrate with other JavaScript libraries via DOM subtrees" - ) - ), - CardWithListPresentation( - title = "Multiplatform Widgets With Web Support", - list = listOf( - "Use and build Compose widgets that work on Android, Desktop, and Web by utilizing Kotlin's expect-actual mechanisms to provide platform-specific implementations", - "Experiment with a set of layout primitives and APIs that mimic the features you already know from Compose for Desktop and Android" - ) - ) - ) -} - -@Composable -fun ComposeWebLibraries() { - ContainerInSection(WtSections.wtSectionBgGrayLight) { - H2(attrs = { classes(WtTexts.wtH2) }) { - Text("Building user interfaces with Compose for Web") - } - - Div(attrs = { - classes(WtRows.wtRow, WtRows.wtRowSizeM) - }) { - Div(attrs = { - classes(WtCols.wtCol6, WtCols.wtColMd6, WtCols.wtColSm12, WtOffsets.wtTopOffset24) - }) { - P(attrs = { - classes(WtTexts.wtText1) - }) { - Text("Compose for Web allows you to build reactive user interfaces for the web in Kotlin, using the concepts and APIs of Jetpack Compose to express the state, behavior, and logic of your application.") - } - } - - Div(attrs = { - classes(WtCols.wtCol6, WtCols.wtColMd6, WtCols.wtColSm12, WtOffsets.wtTopOffset24) - }) { - P(attrs = { - classes(WtTexts.wtText1) - }) { - Text("Compose for Web provides multiple ways of declaring user interfaces in Kotlin code, allowing you to have full control over your website layout with a declarative DOM API, or use versions of the widgets you already know from Jetpack Compose for Desktop and Android.\n") - } - } - } - - Div(attrs = { - classes(WtRows.wtRow, WtRows.wtRowSizeM, WtOffsets.wtTopOffset48) - }) { - createAboutComposeWebCards().forEach { CardWithList(it) } - } - } -} - -@Composable -private fun CardWithList(card: CardWithListPresentation) { - Card( - title = card.title, - links = card.links - ) { - Ul(attrs = { - classes(WtTexts.wtText2) - }) { - card.list.forEachIndexed { ix, it -> - Li(attrs = { - style { property("padding-top", 24.px) } - }) { Text(it) } - } - } - } -} \ No newline at end of file diff --git a/web/benchmark-core/src/jsMain/kotlin/com/sample/content/CodeSamplesSwitcher.kt b/web/benchmark-core/src/jsMain/kotlin/com/sample/content/CodeSamplesSwitcher.kt deleted file mode 100644 index 30fe58fd9a..0000000000 --- a/web/benchmark-core/src/jsMain/kotlin/com/sample/content/CodeSamplesSwitcher.kt +++ /dev/null @@ -1,83 +0,0 @@ -package com.sample.content - -import androidx.compose.runtime.Composable -import org.jetbrains.compose.web.css.* -import org.jetbrains.compose.web.css.selectors.* -import org.jetbrains.compose.web.attributes.* -import org.jetbrains.compose.web.dom.* -import com.sample.style.AppStylesheet - -private object SwitcherVariables : CSSVariables { - val labelWidth by variable() - val labelPadding by variable() -} - -object SwitcherStylesheet : StyleSheet(AppStylesheet) { - - val boxed by style { - - media(maxWidth(640.px)) { - self style { - SwitcherVariables.labelWidth(48.px) - SwitcherVariables.labelPadding(5.px) - } - } - - descendant(self, CSSSelector.Type("label")) style { - display(DisplayStyle.InlineBlock) - property("width", SwitcherVariables.labelWidth.value(56.px)) - property("padding", SwitcherVariables.labelPadding.value(10.px)) - property("transition", "all 0.3s") - property("text-align", "center") - property("box-sizing", "border-box") - - border { - style(LineStyle.Solid) - width(3.px) - color(Color("transparent")) - borderRadius(20.px, 20.px, 20.px) - } - color("#aaa") - } - - border { - style(LineStyle.Solid) - width(1.px) - color(Color("#aaa")) - padding(0.px) - borderRadius(22.px, 22.px, 22.px) - } - - descendant(self, selector("input[type=\"radio\"]")) style { - display(DisplayStyle.None) - } - - descendant(self, selector("input[type=\"radio\"]:checked + label")) style { - border { - style(LineStyle.Solid) - width(3.px) - color(Color("#167dff")) - borderRadius(20.px, 20.px, 20.px) - } - color("#000") - } - } -} - -@Composable -fun CodeSampleSwitcher(count: Int, current: Int, onSelect: (Int) -> Unit) { - Form(attrs = { - classes(SwitcherStylesheet.boxed) - }) { - repeat(count) { ix -> - Input(type = InputType.Radio, attrs = { - name("code-snippet") - value("snippet$ix") - id("snippet$ix") - if (current == ix) checked() - onRadioInput { onSelect(ix) } - }) - Label(forId = "snippet$ix") { Text("${ix + 1}") } - } - } -} diff --git a/web/benchmark-core/src/jsMain/kotlin/com/sample/content/CodeSnippets.kt b/web/benchmark-core/src/jsMain/kotlin/com/sample/content/CodeSnippets.kt deleted file mode 100644 index 931008a112..0000000000 --- a/web/benchmark-core/src/jsMain/kotlin/com/sample/content/CodeSnippets.kt +++ /dev/null @@ -1,280 +0,0 @@ -package com.sample.content - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue -import androidx.compose.runtime.mutableStateOf -import org.jetbrains.compose.web.css.* -import org.jetbrains.compose.web.dom.* -import com.sample.HighlightJs -import com.sample.components.ContainerInSection -import com.sample.style.* -import org.jetbrains.compose.web.css.keywords.auto -import org.w3c.dom.HTMLElement - -private fun HTMLElement.setHighlightedCode(code: String) { - innerText = code - HighlightJs.highlightElement(this) -} - -private val SimpleCounterSnippet = CodeSnippetData( - title = "Simple Counter using Composable DOM", - source = """ - fun main() { - val count = mutableStateOf(0) - - renderComposable(rootElementId = "root") { - Button(attrs = { - onClick { count.value = count.value - 1 } - }) { - Text("-") - } - Span(style = { padding(15.px) }) { /* we use inline style here */ - Text("${"$"}{count.value}") - } - Button(attrs = { - onClick { count.value = count.value + 1 } - }) { - Text("+") - } - } - } - """.trimIndent() -) - -private val DeclareAndUseStylesheet = CodeSnippetData( - title = "Declare and use a stylesheet", - source = """ - object MyStyleSheet : StyleSheet() { - val container by style { /* define a class `container` */ - border(1.px, LineStyle.Solid, Color.RGB(255, 0, 0)) - } - } - - @Composable - fun MyComponent() { - Div(attrs = { - classes(MyStyleSheet.container) /* use `container` class */ - }) { - Text("Hello world!") - } - } - - fun main() { - renderComposable(rootElementId = "root") { - Style(MyStyleSheet) /* mount the stylesheet */ - MyComponent() - } - } - """.trimIndent() -) - -private val DeclareAndUseCssVariable = CodeSnippetData( - title = "Declare and use CSS variables", - source = """ - object MyVariables : CSSVariables { - val contentBackgroundColor by variable() /* declare a variable */ - } - - object MyStyleSheet: StyleSheet() { - val container by style { - MyVariables.contentBackgroundColor(Color("blue")) /* set its value */ - } - val content by style { - backgroundColor(MyVariables.contentBackgroundColor.value()) /* use it */ - } - } - - @Composable - fun MyComponent() { - Div(attrs = { - classes(MyStyleSheet.container) - }) { - Span(attrs = { - classes(MyStyleSheet.content) - }) { - Text("Hello world!") - } - } - } - """.trimIndent() -) - -private val HoverSelectorAndMedia = CodeSnippetData( - title = "Hover selector and media query examples", - source = """ - object MyStyleSheet: StyleSheet() { - val container by style { - - backgroundColor(Color("blue")) - - padding(20.px) - - hover(self) style { /* `self` is a reference to the class */ - backgroundColor(Color("red")) - } - - media(maxWidth(500.px)) { - self style { - padding(10.px) - } - } - } - } - """.trimIndent() -) - -private val DefineCssClassInComponent = CodeSnippetData( - title = "Define a CSS class in a component", - source = """ - object MyStyleSheet: StyleSheet() {} - - @Composable - fun MyComponent() { - Div(attrs = { - /* the class name will be generated at runtime */ - classes(MyStyleSheet.css { - - backgroundColor(Color("blue")) - - self + ":hover" style { /* this is an example of a raw selector */ - backgroundColor(Color("red")) - } - }) - }) { - Text("Hello world!") - } - } - """.trimIndent() -) - -private val LayoutsSample = CodeSnippetData( - title = "Counter for Web and Desktop", - source = """ - /* Shared code in commonMain - App.kt (No direct control over DOM or CSS here) */ - - private val counter = mutableStateOf(0) - - @Composable - fun App() { - Row { - Button(onClick = { counter.value = counter.value - 1 }) { - Text("-") - } - - Text("${"$"}{counter.value}", modifier = Modifier.padding(16.dp)) - - Button(onClick = { counter.value = counter.value + 1 }) { - Text("+") - } - } - } - - /* Desktop specific code in desktopMain: */ - - fun main() = Window(title = "Demo", size = IntSize(800, 800)) { - App() - } - - /* Web specific code in jsMain: */ - - fun main() = renderComposable(rootElementId = "root") { - App() - } - """.trimIndent() -) - -private val allSnippets = arrayOf( - SimpleCounterSnippet, - DeclareAndUseStylesheet, - DeclareAndUseCssVariable, - HoverSelectorAndMedia, - DefineCssClassInComponent, - LayoutsSample -) - -private var currentCodeSnippet: CodeSnippetData by mutableStateOf(allSnippets[0]) -private var selectedSnippetIx: Int by mutableStateOf(0) - -@Composable -fun CodeSamples() { - ContainerInSection { - Div(attrs = { - classes(WtRows.wtRow) - style { - justifyContent(JustifyContent.SpaceBetween) - } - }) { - Div(attrs = { classes(WtCols.wtCol6, WtCols.wtColMd4, WtCols.wtColSm12) }) { - H1(attrs = { - classes(WtTexts.wtH2) - }) { - Text("Code samples") - } - } - - Div(attrs = { classes(WtOffsets.wtTopOffsetSm24) }) { - CodeSampleSwitcher(count = allSnippets.size, current = selectedSnippetIx) { - selectedSnippetIx = it - currentCodeSnippet = allSnippets[it] - } - } - } - - TitledCodeSample(title = currentCodeSnippet.title, code = currentCodeSnippet.source) - } -} - -@Composable -private fun TitledCodeSample(title: String, code: String) { - H3(attrs = { - classes(WtTexts.wtH3, WtOffsets.wtTopOffset48) - }) { - Text(title) - } - - Div( - attrs = { - classes(WtOffsets.wtTopOffset24) - style { - backgroundColor(Color.RGBA(39, 40, 44, 0.05)) - borderRadius(8.px, 8.px, 8.px) - property("padding", "12px 16px") - } - } - ) { - FormattedCodeSnippet(code = code) - } -} - -@Composable -fun FormattedCodeSnippet(code: String, language: String = "kotlin") { - Pre(attrs = { - style { - property("max-height", 25.em) - property("overflow", "auto") - height(auto) - } - }) { - Code( - attrs = { - classes("language-$language", "hljs") - style { - property("font-family", "'JetBrains Mono', monospace") - property("tab-size", 4) - fontSize(10.pt) - backgroundColor(Color("transparent")) - } - } - ) { - DomSideEffect(code) { - it.setHighlightedCode(code) - } - } - } -} - -private data class CodeSnippetData( - val title: String, - val source: String -) \ No newline at end of file diff --git a/web/benchmark-core/src/jsMain/kotlin/com/sample/content/Footer.kt b/web/benchmark-core/src/jsMain/kotlin/com/sample/content/Footer.kt deleted file mode 100644 index 30c37f0b23..0000000000 --- a/web/benchmark-core/src/jsMain/kotlin/com/sample/content/Footer.kt +++ /dev/null @@ -1,111 +0,0 @@ -package com.sample.content - -import androidx.compose.runtime.Composable -import org.jetbrains.compose.web.css.* -import org.jetbrains.compose.web.attributes.* -import org.jetbrains.compose.web.dom.* -import com.sample.style.* - - -@Composable -fun PageFooter() { - Footer(attrs = { - style { - flexShrink(0) - property("box-sizing", "border-box") - } - }) { - Section(attrs = { - classes(WtSections.wtSectionBgGrayDark) - style { - property("padding", "24px 0") - } - }) { - Div(attrs = { classes(WtContainer.wtContainer) }) { - Div(attrs = { - classes(WtRows.wtRow, WtRows.wtRowSizeM, WtRows.wtRowSmAlignItemsCenter) - style { - justifyContent(JustifyContent.Center) - flexWrap(FlexWrap.Wrap) - } - }) { - Div(attrs = { - classes(WtCols.wtColInline) - }) { - P(attrs = { - classes(WtTexts.wtText1, WtTexts.wtText1ThemeDark) - }) { - Text("Follow us") - } - } - - Div(attrs = { - classes(WtCols.wtColInline) - }) { - getSocialLinks().forEach { SocialIconLink(it) } - } - } - - CopyrightInFooter() - } - } - } -} - -@Composable -private fun CopyrightInFooter() { - Div(attrs = { - classes(WtRows.wtRow, WtRows.wtRowSizeM, WtRows.wtRowSmAlignItemsCenter, WtOffsets.wtTopOffset48) - style { - justifyContent(JustifyContent.SpaceEvenly) - flexWrap(FlexWrap.Wrap) - property("padding", "0px 12px") - } - }) { - Span(attrs = { - classes(WtTexts.wtText3, WtTexts.wtTextPale) - }) { - Text("Copyright © 2000-2021 JetBrains s.r.o.") - } - - Span(attrs = { - classes(WtTexts.wtText3, WtTexts.wtTextPale) - }) { - Text("Developed with drive and IntelliJ IDEA") - } - } -} - -@Composable -private fun SocialIconLink(link: SocialLink) { - A(attrs = { - classes(WtTexts.wtSocialButtonItem) - target(ATarget.Blank) - }, href = link.url) { - Img(src = link.iconSvg) {} - } -} - -private data class SocialLink( - val id: String, - val url: String, - val title: String, - val iconSvg: String -) - -private fun getSocialLinks(): List { - return listOf( - SocialLink("facebook", "https://www.facebook.com/JetBrains", "JetBrains on Facebook", "ic_fb.svg"), - SocialLink("twitter", "https://twitter.com/jetbrains", "JetBrains on Twitter", "ic_twitter.svg"), - SocialLink( - "linkedin", - "https://www.linkedin.com/company/jetbrains", - "JetBrains on Linkedin", - "ic_linkedin.svg" - ), - SocialLink("youtube", "https://www.youtube.com/user/JetBrainsTV", "JetBrains on YouTube", "ic_youtube.svg"), - SocialLink("instagram", "https://www.instagram.com/jetbrains/", "JetBrains on Instagram", "ic_insta.svg"), - SocialLink("blog", "https://blog.jetbrains.com/", "JetBrains blog", "ic_jb_blog.svg"), - SocialLink("rss", "https://blog.jetbrains.com/feed/", "JetBrains RSS Feed", "ic_feed.svg"), - ) -} \ No newline at end of file diff --git a/web/benchmark-core/src/jsMain/kotlin/com/sample/content/GetStartedSection.kt b/web/benchmark-core/src/jsMain/kotlin/com/sample/content/GetStartedSection.kt deleted file mode 100644 index a515af28ed..0000000000 --- a/web/benchmark-core/src/jsMain/kotlin/com/sample/content/GetStartedSection.kt +++ /dev/null @@ -1,108 +0,0 @@ -package com.sample.content - -import androidx.compose.runtime.Composable -import org.jetbrains.compose.web.css.* -import org.jetbrains.compose.web.css.selectors.* -import org.jetbrains.compose.web.attributes.* -import org.jetbrains.compose.web.dom.* -import org.jetbrains.compose.web.* -import com.sample.components.CardDark -import com.sample.components.ContainerInSection -import com.sample.components.LinkOnCard -import com.sample.style.* - -private data class GetStartedCardPresentation( - val title: String, - val content: String, - val links: List -) - -private fun getCards(): List { - return listOf( - GetStartedCardPresentation( - title = "Start tutorial here", - content = "In this tutorial we will see how to create our first web UI application using Compose for Web.", - links = listOf( - LinkOnCard( - linkText = "View tutorial", - linkUrl = "https://github.com/JetBrains/compose-jb/tree/master/tutorials/Web/Getting_Started" - ) - ) - ), - GetStartedCardPresentation( - title = "Landing page example", - content = "An example of a landing page built using the Composable DOM API and Stylesheet DSL.", - links = listOf( - LinkOnCard( - linkText = "Explore the source code", - linkUrl = "https://github.com/JetBrains/compose-jb/tree/master/examples/web_landing" - ) - ) - ), - GetStartedCardPresentation( - title = "Falling Balls app example", - content = "This example demonstrates the use of multiplatform widgets – sharing user interface code between Compose for Desktop and Web.", - links = listOf( - LinkOnCard( - linkText = "Explore the source code", - linkUrl = "https://github.com/JetBrains/compose-jb/tree/master/examples/falling_balls_with_web" - ), - LinkOnCard( - linkText = "Play", - linkUrl = "https://falling-balls.ui.pages.jetbrains.team/" - ) - ) - ) - ) -} - -@Composable -private fun CardContent(text: String) { - P(attrs = { - classes(WtTexts.wtText2, WtTexts.wtText2ThemeDark, WtOffsets.wtTopOffset24) - }) { - Text(text) - } -} - -@Composable -fun GetStarted() { - ContainerInSection(WtSections.wtSectionBgGrayDark) { - H1(attrs = { - classes(WtTexts.wtH2, WtTexts.wtH2ThemeDark) - }) { - Text("Try out the Compose for Web") - } - - Div(attrs = { - classes(WtRows.wtRowSizeM, WtRows.wtRow, WtOffsets.wtTopOffset24) - }) { - Div(attrs = { - classes(WtCols.wtCol6, WtCols.wtColMd10, WtCols.wtColSm12, WtOffsets.wtTopOffset24) - }) { - P(attrs = { - classes(WtTexts.wtText1) - styleBuilder.color(Color("#fff")) - }) { - Text("Ready for your next adventure? Learn how to build reactive user interfaces with Compose for Web.") - } - } - } - - Div( - attrs = { - classes(WtRows.wtRow, WtRows.wtRowSizeM, WtOffsets.wtTopOffset24) - } - ) { - getCards().forEach { - CardDark( - title = it.title, - links = it.links, - wtExtraStyleClasses = listOf(WtCols.wtCol4, WtCols.wtColMd6, WtCols.wtColSm12) - ) { - CardContent(it.content) - } - } - } - } -} \ No newline at end of file diff --git a/web/benchmark-core/src/jsMain/kotlin/com/sample/content/Header.kt b/web/benchmark-core/src/jsMain/kotlin/com/sample/content/Header.kt deleted file mode 100644 index f0f02f1060..0000000000 --- a/web/benchmark-core/src/jsMain/kotlin/com/sample/content/Header.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.sample.content - -import androidx.compose.runtime.Composable -import org.jetbrains.compose.web.css.* -import org.jetbrains.compose.web.attributes.* -import org.jetbrains.compose.web.dom.* -import com.sample.style.* -import kotlinx.browser.window - -@Composable -fun Header() { - Section(attrs = { - classes(WtSections.wtSectionBgGrayDark) - }) { - Div(attrs = { classes(WtContainer.wtContainer) }) { - Div(attrs = { - classes(WtRows.wtRow, WtRows.wtRowSizeM) - style { - alignItems(AlignItems.Center) - justifyContent(JustifyContent.SpaceBetween) - } - }) { - Logo() - // TODO: support i18n - //LanguageButton() - } - } - } -} - -@Composable -private fun Logo() { - Div(attrs = { - classes(WtCols.wtColInline) - }) { - A(attrs = { - target(ATarget.Blank) - }, href = "https://www.jetbrains.com/") { - Div(attrs = { - classes("jetbrains-logo", "_logo-jetbrains-square", "_size-3") - }) {} - } - } -} - -@Composable -private fun LanguageButton() { - Div(attrs = { - classes(WtCols.wtColInline) - }) { - Button(attrs = { - classes(WtTexts.wtButton, WtTexts.wtLangButton) - onClick { window.alert("Oops! This feature is yet to be implemented") } - style { - property("cursor", "pointer") - } - }) { - Img(src = "ic_lang.svg", attrs = { - style { - property("padding-left", 8.px) - property("padding-right", 8.px) - } - }) - Text("English") - } - } -} \ No newline at end of file diff --git a/web/benchmark-core/src/jsMain/kotlin/com/sample/content/IntroSection.kt b/web/benchmark-core/src/jsMain/kotlin/com/sample/content/IntroSection.kt deleted file mode 100644 index 7de2779296..0000000000 --- a/web/benchmark-core/src/jsMain/kotlin/com/sample/content/IntroSection.kt +++ /dev/null @@ -1,221 +0,0 @@ -package com.sample.content - -import androidx.compose.runtime.* -import org.jetbrains.compose.web.css.* -import org.jetbrains.compose.web.attributes.* -import org.jetbrains.compose.web.dom.* -import com.sample.components.ContainerInSection -import com.sample.style.* -import org.w3c.dom.HTMLElement - -@Composable -fun Intro() { - ContainerInSection { - Div(attrs = { - classes(WtRows.wtRow, WtRows.wtRowSizeM, WtRows.wtRowSmAlignItemsCenter) - }) { - - Div(attrs = { - classes(WtCols.wtCol2, WtCols.wtColMd3) - styleBuilder.alignSelf(AlignSelf.Start) - }) { - Img(src = "i1.svg", attrs = { classes(AppStylesheet.composeLogo) }) - } - - Div(attrs = { - classes( - WtCols.wtCol10, - WtCols.wtColMd8, - WtCols.wtColSm12, - WtOffsets.wtTopOffsetSm12 - ) - }) { - H1(attrs = { classes(WtTexts.wtHero) }) { - Text("Compose for ") - Span(attrs = { - classes(WtTexts.wtHero) - style { - display(DisplayStyle.InlineBlock) - property("white-space", "nowrap") - } - }) { - Text("Web") - - Span(attrs = { classes(AppStylesheet.composeTitleTag) }) { - Text("Technology preview") - } - } - } - Div(attrs = { - classes(WtDisplay.wtDisplayMdNone) - }) { - IntroAboutComposeWeb() - } - } - } - - - Div(attrs = { - classes(WtDisplay.wtDisplayNone, WtDisplay.wtDisplayMdBlock) - }) { - IntroAboutComposeWeb() - } - } -} - -@Composable -private fun IntroAboutComposeWeb() { - Div(attrs = { - classes(WtRows.wtRow, WtRows.wtRowSizeM) - }) { - - Div(attrs = { - classes(WtCols.wtCol9, WtCols.wtColMd9, WtCols.wtColSm12) - }) { - P(attrs = { classes(WtTexts.wtSubtitle2, WtOffsets.wtTopOffset24) }) { - Text("Reactive web UIs for Kotlin, based on Google's ") - - A(href = "https://developer.android.com/jetpack/compose", attrs = { - classes(WtTexts.wtLink) - target(ATarget.Blank) - }) { - Text("modern toolkit") - } - - Text(" and brought to you by JetBrains") - } - - P(attrs = { - classes(WtTexts.wtText1, WtOffsets.wtTopOffset24) - }) { - Text( - "Compose for Web simplifies and accelerates UI development for web applications, " + - "and aims to enable UI code sharing between web, desktop, and Android applications " + - "in the future. Currently in technology preview." - ) - } - - ComposeWebStatusMessage() - - IntroCodeSample() - - A( - attrs = { - classes(WtTexts.wtButton, WtOffsets.wtTopOffset24) - target(ATarget.Blank) - }, - href = "https://github.com/jetbrains/compose-jb" - ) { - Text("Explore on GitHub") - } - } - } -} - -@Composable -private fun IntroCodeSample() { - Div(attrs = { - style { - marginTop(24.px) - backgroundColor(Color.RGBA(39, 40, 44, 0.05)) - borderRadius(8.px) - property("font-family", "'JetBrains Mono', monospace") - } - }) { - Div(attrs = { - style { - property("padding", "12px 16px") - } - }) { - FormattedCodeSnippet( - code = """ - fun greet() = listOf("Hello", "Hallo", "Hola", "Servus").random() - - renderComposable("greetingContainer") { - var greeting by remember { mutableStateOf(greet()) } - Button(attrs = { onClick { greeting = greet() } }) { - Text(greeting) - } - } - """.trimIndent() - ) - } - - Hr(attrs = { - style { - height(1.px) - border(width = 0.px) - backgroundColor(Color.RGBA(39, 40, 44, 0.15)) - } - }) - - IntroCodeSampleResult() - } -} - -@Composable -private fun IntroCodeSampleResult() { - Div(attrs = { - style { - property("padding", "12px 16px") - display(DisplayStyle.Flex) - flexDirection(FlexDirection.Row) - alignItems(AlignItems.Center) - } - }) { - Span( - attrs = { - classes(WtTexts.wtText2) - style { - property("margin-right", 8.px) - } - }, - ) { - Text("Result:") - } - - fun greet() = listOf("Hello", "Hallo", "Hola", "Servus").random() - - Div(attrs = { - id("greetingContainer") - }) { - var greeting by remember { mutableStateOf(greet()) } - Button(attrs = { onClick { greeting = greet() } }) { - Text(greeting) - } - } - } -} - -@Composable -private fun ComposeWebStatusMessage() { - Div(attrs = { - classes(WtRows.wtRow, WtRows.wtRowSizeXs, WtOffsets.wtTopOffset24) - }) { - Div(attrs = { - classes(WtCols.wtColInline) - }) { - Img(src = "ic_info.svg", attrs = { - style { - width(24.px) - height(24.px) - } - }) - } - - Div(attrs = { - classes(WtCols.wtColAutoFill) - }) { - P(attrs = { - classes(WtTexts.wtText3) - }) { - Text( - "With its current status Technology Preview, Compose for Web " + - "is not production-ready, and should only be used in experiments. " + - "We are hard at work to bring you great learning materials, tutorials, " + - "and documentation, and optimize the performance of Compose for Web in the future!" - ) - } - } - } -} \ No newline at end of file diff --git a/web/benchmark-core/src/jsMain/kotlin/com/sample/content/JoinUs.kt b/web/benchmark-core/src/jsMain/kotlin/com/sample/content/JoinUs.kt deleted file mode 100644 index f928f1eb6d..0000000000 --- a/web/benchmark-core/src/jsMain/kotlin/com/sample/content/JoinUs.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.sample.content - -import androidx.compose.runtime.Composable -import org.jetbrains.compose.web.css.* -import org.jetbrains.compose.web.css.selectors.* -import org.jetbrains.compose.web.attributes.* -import org.jetbrains.compose.web.dom.* -import org.jetbrains.compose.web.* -import com.sample.components.ContainerInSection -import com.sample.style.* - -@Composable -fun JoinUs() { - ContainerInSection(WtSections.wtSectionBgGrayLight) { - Div(attrs = { - classes(WtRows.wtRow, WtRows.wtRowSizeM) - }) { - Div(attrs = { - classes(WtCols.wtCol9, WtCols.wtColMd11, WtCols.wtColSm12) - }) { - - P(attrs = { - classes(WtTexts.wtSubtitle2) - }) { - Text("Interested in Compose for other platforms?") - - P { - Text("Have a look at ") - A(href = "https://www.jetbrains.com/lp/compose/", attrs = { - classes(WtTexts.wtLink) - target(ATarget.Blank) - }) { - Text("Compose for Desktop") - } - } - } - - P(attrs = { - classes(WtTexts.wtSubtitle2, WtOffsets.wtTopOffset24) - }) { - Text("Feel free to join the ") - LinkToSlack( - url = "https://kotlinlang.slack.com/archives/C01F2HV7868", - text = "#compose-web" - ) - Text(" channel on Kotlin Slack to discuss Compose for Web, or ") - LinkToSlack( - url = "https://kotlinlang.slack.com/archives/CJLTWPH7S", - text = "#compose" - ) - Text(" for general Compose discussions") - } - } - } - - A(attrs = { - classes(WtTexts.wtButton, WtTexts.wtButtonContrast, WtOffsets.wtTopOffset24) - target(ATarget.Blank) - }, href = "https://surveys.jetbrains.com/s3/kotlin-slack-sign-up") { - Text("Join Kotlin Slack") - } - } -} - -@Composable -private fun LinkToSlack(url: String, text: String) { - A(href = url, attrs = { - target(ATarget.Blank) - classes(WtTexts.wtLink) - }) { - Text(text) - } -} \ No newline at end of file diff --git a/web/benchmark-core/src/jsMain/kotlin/com/sample/style/Stylesheet.kt b/web/benchmark-core/src/jsMain/kotlin/com/sample/style/Stylesheet.kt deleted file mode 100644 index 3c9605cbe1..0000000000 --- a/web/benchmark-core/src/jsMain/kotlin/com/sample/style/Stylesheet.kt +++ /dev/null @@ -1,95 +0,0 @@ -package com.sample.style - -import org.jetbrains.compose.web.css.* -import org.jetbrains.compose.web.css.selectors.CSSSelector - -object AppCSSVariables : CSSVariables { - val wtColorGreyLight by variable() - val wtColorGreyDark by variable() - - val wtOffsetTopUnit by variable() - val wtHorizontalLayoutGutter by variable() - val wtFlowUnit by variable() - - val wtHeroFontSize by variable() - val wtHeroLineHeight by variable() - val wtSubtitle2FontSize by variable() - val wtSubtitle2LineHeight by variable() - val wtH2FontSize by variable() - val wtH2LineHeight by variable() - val wtH3FontSize by variable() - val wtH3LineHeight by variable() - - val wtColCount by variable() -} - - -object AppStylesheet : StyleSheet() { - val composeLogo by style { - property("max-width", 100.percent) - } - - val composeTitleTag by style { - property("padding", "5px 12px") - property("letter-spacing", "normal") - property("font-weight", 400) - property("line-height", 24.px) - - position(Position.Relative) - top((-32).px) - marginLeft(8.px) - fontSize(15.px) - backgroundColor(Color.RGBA(39, 40, 44, .05)) - color(Color.RGBA(39,40,44,.7)) - borderRadius(4.px, 4.px, 4.px) - - media(maxWidth(640.px)) { - self style { - top((-16).px) - } - } - } - - init { - "label, a, button" style { - property( - "font-family", - "system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Droid Sans,Helvetica Neue,Arial,sans-serif" - ) - } - - CSSSelector.Universal style { - AppCSSVariables.wtColorGreyLight(Color("#f4f4f4")) - AppCSSVariables.wtColorGreyDark(Color("#323236")) - AppCSSVariables.wtOffsetTopUnit(24.px) - - margin(0.px) - } - - media(maxWidth(640.px)) { - CSSSelector.Universal style { - AppCSSVariables.wtOffsetTopUnit(16.px) - AppCSSVariables.wtFlowUnit(16.px) - } - } - - CSSSelector.Attribute( - name = "class", - value = "wtCol", - operator = CSSSelector.Attribute.Operator.Contains - ) style { - property("margin-right", AppCSSVariables.wtHorizontalLayoutGutter.value()) - property("margin-left", AppCSSVariables.wtHorizontalLayoutGutter.value()) - - property( - "flex-basis", - "calc(8.33333%*${AppCSSVariables.wtColCount.value()} - ${AppCSSVariables.wtHorizontalLayoutGutter.value()}*2)" - ) - property( - "max-width", - "calc(8.33333%*${AppCSSVariables.wtColCount.value()} - ${AppCSSVariables.wtHorizontalLayoutGutter.value()}*2)" - ) - property("box-sizing", "border-box") - } - } -} diff --git a/web/benchmark-core/src/jsMain/kotlin/com/sample/style/WtCard.kt b/web/benchmark-core/src/jsMain/kotlin/com/sample/style/WtCard.kt deleted file mode 100644 index 3c8af92bc8..0000000000 --- a/web/benchmark-core/src/jsMain/kotlin/com/sample/style/WtCard.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.sample.style - -import org.jetbrains.compose.web.css.AlignItems -import org.jetbrains.compose.web.css.Color -import org.jetbrains.compose.web.css.DisplayStyle -import org.jetbrains.compose.web.css.FlexDirection -import org.jetbrains.compose.web.css.LineStyle -import org.jetbrains.compose.web.css.Position -import org.jetbrains.compose.web.css.StyleSheet -import org.jetbrains.compose.web.css.alignItems -import org.jetbrains.compose.web.css.backgroundColor -import org.jetbrains.compose.web.css.border -import org.jetbrains.compose.web.css.color -import org.jetbrains.compose.web.css.display -import org.jetbrains.compose.web.css.flexDirection -import org.jetbrains.compose.web.css.flexGrow -import org.jetbrains.compose.web.css.maxWidth -import org.jetbrains.compose.web.css.media -import org.jetbrains.compose.web.css.padding -import org.jetbrains.compose.web.css.percent -import org.jetbrains.compose.web.css.position -import org.jetbrains.compose.web.css.px - -object WtCards : StyleSheet(AppStylesheet) { - val wtCard by style { - display(DisplayStyle.Flex) - flexDirection(FlexDirection.Column) - border(1.px, LineStyle.Solid) - property("min-height", 0) - property("box-sizing", "border-box") - } - - val wtCardThemeLight by style { - border(color = Color.RGBA(39, 40, 44, .2)) - color("#27282c") - backgroundColor("white") - } - - val wtCardThemeDark by style { - backgroundColor(Color.RGBA(255, 255, 255, 0.05)) - color(Color.RGBA(255, 255, 255, 0.6)) - border(0.px) - } - - val wtCardSection by style { - position(Position.Relative) - property("overflow", "auto") - property("flex", "1 1 auto") - property("min-height", 0) - property("box-sizing", "border-box") - property("padding", "24px 32px") - - media(maxWidth(640.px)) { - self style { padding(16.px) } - } - } - - val wtVerticalFlex by style { - display(DisplayStyle.Flex) - flexDirection(FlexDirection.Column) - alignItems(AlignItems.FlexStart) - } - - val wtVerticalFlexGrow by style { - flexGrow(1) - property("max-width", 100.percent) - } -} \ No newline at end of file diff --git a/web/benchmark-core/src/jsMain/kotlin/com/sample/style/WtContainer.kt b/web/benchmark-core/src/jsMain/kotlin/com/sample/style/WtContainer.kt deleted file mode 100644 index fc2ef401ec..0000000000 --- a/web/benchmark-core/src/jsMain/kotlin/com/sample/style/WtContainer.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.sample.style - -import androidx.compose.runtime.Composable -import org.jetbrains.compose.web.css.* -import org.jetbrains.compose.web.css.selectors.* -import org.jetbrains.compose.web.attributes.* -import org.jetbrains.compose.web.dom.* -import org.jetbrains.compose.web.* - -object WtContainer : StyleSheet(AppStylesheet) { - val wtContainer by style { - property("margin-left", "auto") - property("margin-right", "auto") - property("box-sizing", "border-box") - property("padding-left", 22.px) - property("padding-right", 22.px) - property("max-width", 1276.px) - - media(maxWidth(640.px)) { - self style { - property("max-width", 100.percent) - property("padding-left", 16.px) - property("padding-right", 16.px) - } - } - - media(maxWidth(1276.px)) { - self style { - property("max-width", 996.px) - property("padding-left", 996.px) - property("padding-right", 22.px) - } - } - - media(maxWidth(1000.px)) { - self style { - property("max-width", 100.percent) - property("padding-left", 22.px) - property("padding-right", 22.px) - } - } - } -} \ No newline at end of file diff --git a/web/benchmark-core/src/jsMain/kotlin/com/sample/style/WtDisplay.kt b/web/benchmark-core/src/jsMain/kotlin/com/sample/style/WtDisplay.kt deleted file mode 100644 index 7407054b85..0000000000 --- a/web/benchmark-core/src/jsMain/kotlin/com/sample/style/WtDisplay.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.sample.style - -import androidx.compose.runtime.Composable -import org.jetbrains.compose.web.css.* -import org.jetbrains.compose.web.css.selectors.* -import org.jetbrains.compose.web.attributes.* -import org.jetbrains.compose.web.dom.* -import org.jetbrains.compose.web.* - -object WtDisplay : StyleSheet(AppStylesheet) { - val wtDisplayNone by style { - display(DisplayStyle.None) - } - - val wtDisplayMdBlock by style { - media(maxWidth(1000.px)) { - self style { - display(DisplayStyle.Block) - } - } - } - - val wtDisplayMdNone by style { - media(maxWidth(1000.px)) { - self style { - display(DisplayStyle.None) - } - } - } -} \ No newline at end of file diff --git a/web/benchmark-core/src/jsMain/kotlin/com/sample/style/WtOffest.kt b/web/benchmark-core/src/jsMain/kotlin/com/sample/style/WtOffest.kt deleted file mode 100644 index a1aa20f0e8..0000000000 --- a/web/benchmark-core/src/jsMain/kotlin/com/sample/style/WtOffest.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.sample.style - -import androidx.compose.runtime.Composable -import org.jetbrains.compose.web.css.* -import org.jetbrains.compose.web.css.selectors.* -import org.jetbrains.compose.web.attributes.* -import org.jetbrains.compose.web.dom.* -import org.jetbrains.compose.web.* - -object WtOffsets : StyleSheet(AppStylesheet) { - val wtTopOffset96 by style { - marginTop(96.px) - property( - "margin-top", - "calc(4*${AppCSSVariables.wtOffsetTopUnit.value(24.px)})" - ) - } - - val wtTopOffset24 by style { - marginTop(24.px) - property( - "margin-top", - "calc(1*${AppCSSVariables.wtOffsetTopUnit.value(24.px)})" - ) - } - - val wtTopOffset48 by style { - marginTop(48.px) - } - - val wtTopOffsetSm12 by style { - media(maxWidth(640.px)) { - self style { - marginTop(12.px) - } - } - } - - val wtTopOffsetSm24 by style { - media(maxWidth(640.px)) { - self style { - marginTop(24.px) - } - } - } -} \ No newline at end of file diff --git a/web/benchmark-core/src/jsMain/kotlin/com/sample/style/WtRow.kt b/web/benchmark-core/src/jsMain/kotlin/com/sample/style/WtRow.kt deleted file mode 100644 index 59b94b9a3e..0000000000 --- a/web/benchmark-core/src/jsMain/kotlin/com/sample/style/WtRow.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.sample.style - -import androidx.compose.runtime.Composable -import org.jetbrains.compose.web.css.* -import org.jetbrains.compose.web.css.selectors.* -import org.jetbrains.compose.web.attributes.* -import org.jetbrains.compose.web.dom.* -import org.jetbrains.compose.web.* - -object WtRows : StyleSheet(AppStylesheet) { - - val wtRow by style { - AppCSSVariables.wtHorizontalLayoutGutter(0.px) - display(DisplayStyle.Flex) - flexWrap(FlexWrap.Wrap) - - property( - "margin-right", - "calc(-1*${AppCSSVariables.wtHorizontalLayoutGutter.value()})" - ) - property( - "margin-left", - "calc(-1*${AppCSSVariables.wtHorizontalLayoutGutter.value()})" - ) - property("box-sizing", "border-box") - } - - val wtRowSizeM by style { - AppCSSVariables.wtHorizontalLayoutGutter(16.px) - - media(maxWidth(640.px)) { - self style { - AppCSSVariables.wtHorizontalLayoutGutter(8.px) - } - } - } - - val wtRowSizeXs by style { - AppCSSVariables.wtHorizontalLayoutGutter(6.px) - } - - val wtRowSmAlignItemsCenter by style { - self style { - alignItems(AlignItems.Center) - } - } -} \ No newline at end of file diff --git a/web/benchmark-core/src/jsMain/kotlin/com/sample/style/WtSection.kt b/web/benchmark-core/src/jsMain/kotlin/com/sample/style/WtSection.kt deleted file mode 100644 index 0263c903d2..0000000000 --- a/web/benchmark-core/src/jsMain/kotlin/com/sample/style/WtSection.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.sample.style - -import org.jetbrains.compose.web.css.* -import org.jetbrains.compose.web.css.selectors.* -import org.jetbrains.compose.web.attributes.* -import org.jetbrains.compose.web.dom.* -import org.jetbrains.compose.web.* - -object WtSections : StyleSheet(AppStylesheet) { - - val wtSection by style { - property("box-sizing", "border-box") - property("padding-bottom", 96.px) - property("padding-top", 1.px) - property( - propertyName = "padding-bottom", - value = "calc(4*${AppCSSVariables.wtOffsetTopUnit.value(24.px)})" - ) - backgroundColor("#fff") - } - - val wtSectionBgGrayLight by style { - backgroundColor("#f4f4f4") - backgroundColor(AppCSSVariables.wtColorGreyLight.value()) - } - - val wtSectionBgGrayDark by style { - backgroundColor("#323236") - backgroundColor(AppCSSVariables.wtColorGreyDark.value()) - } -} \ No newline at end of file diff --git a/web/benchmark-core/src/jsMain/kotlin/com/sample/style/WtText.kt b/web/benchmark-core/src/jsMain/kotlin/com/sample/style/WtText.kt deleted file mode 100644 index d2025f0a81..0000000000 --- a/web/benchmark-core/src/jsMain/kotlin/com/sample/style/WtText.kt +++ /dev/null @@ -1,224 +0,0 @@ -package com.sample.style - -import org.jetbrains.compose.web.css.* -import org.jetbrains.compose.web.css.selectors.* -import org.jetbrains.compose.web.attributes.* -import org.jetbrains.compose.web.dom.* -import org.jetbrains.compose.web.* - -object WtTexts : StyleSheet(AppStylesheet) { - - val wtHero by style { - color("#27282c") - fontSize(60.px) - property("font-size", AppCSSVariables.wtHeroFontSize.value(60.px)) - property("letter-spacing", (-1.5).px) - property("font-weight", 900) - property("line-height", 64.px) - property("line-height", AppCSSVariables.wtHeroLineHeight.value(64.px)) - - media(maxWidth(640.px)) { - self style { - AppCSSVariables.wtHeroFontSize(42.px) - AppCSSVariables.wtHeroLineHeight(48.px) - } - } - - property( - "font-family", - "Gotham SSm A,Gotham SSm B,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Droid Sans,Helvetica Neue,Arial,sans-serif" - ) - } - - val wtSubtitle2 by style { - color("#27282c") - fontSize(28.px) - property("font-size", AppCSSVariables.wtSubtitle2FontSize.value(28.px)) - property("letter-spacing", "normal") - property("font-weight", 300) - property("line-height", 40.px) - property("line-height", AppCSSVariables.wtSubtitle2LineHeight.value(40.px)) - - media(maxWidth(640.px)) { - self style { - AppCSSVariables.wtSubtitle2FontSize(24.px) - AppCSSVariables.wtSubtitle2LineHeight(32.px) - } - } - - property( - "font-family", - "Gotham SSm A,Gotham SSm B,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Droid Sans,Helvetica Neue,Arial,sans-serif" - ) - } - - val wtText1 by style { - color(Color.RGBA(39, 40, 44, .7)) - fontSize(18.px) - property("letter-spacing", "normal") - property("font-weight", 400) - property("line-height", 28.px) - - property( - "font-family", - "system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Droid Sans,Helvetica Neue,Arial,sans-serif" - ) - } - - val wtText1ThemeDark by style { - color(Color.RGBA(255, 255, 255, 0.6)) - } - - val wtText2 by style { - color(Color.RGBA(39, 40, 44, .7)) - fontSize(15.px) - property("letter-spacing", "normal") - property("font-weight", 400) - property("line-height", 24.px) - - property( - "font-family", - "system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Droid Sans,Helvetica Neue,Arial,sans-serif" - ) - } - - val wtText3 by style { - color(Color.RGBA(39, 40, 44, .7)) - fontSize(12.px) - property("letter-spacing", "normal") - property("font-weight", 400) - property("line-height", 16.px) - - property( - "font-family", - "system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Droid Sans,Helvetica Neue,Arial,sans-serif" - ) - } - - val wtTextPale by style { - color(Color.RGBA(255, 255, 255, 0.30)) - } - - val wtText2ThemeDark by style { - color(Color.RGBA(255, 255, 255, 0.6)) - } - - val wtText3ThemeDark by style { - color(Color.RGBA(255, 255, 255, 0.6)) - } - - val wtLink by style { - property("border-bottom", "1px solid transparent") - property("text-decoration", "none") - color("#167dff") - - hover(self) style { - property("border-bottom-color", "#167dff") - } - } - - val wtH2 by style { - color("#27282c") - fontSize(31.px) - property("font-size", AppCSSVariables.wtH2FontSize.value(31.px)) - property("letter-spacing", (-.5).px) - property("font-weight", 700) - property("line-height", 40.px) - property("line-height", AppCSSVariables.wtH2LineHeight.value(40.px)) - - media(maxWidth(640.px)) { - self style { - AppCSSVariables.wtH2FontSize(24.px) - AppCSSVariables.wtH2LineHeight(32.px) - } - } - - property( - "font-family", - "Gotham SSm A,Gotham SSm B,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Droid Sans,Helvetica Neue,Arial,sans-serif" - ) - } - - val wtH2ThemeDark by style { - color("#fff") - } - - val wtH3 by style { - color("#27282c") - fontSize(21.px) - property("font-size", AppCSSVariables.wtH3FontSize.value(20.px)) - property("letter-spacing", "normal") - property("font-weight", 700) - property("line-height", 28.px) - property("line-height", AppCSSVariables.wtH3LineHeight.value(28.px)) - - property( - "font-family", - "system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Droid Sans,Helvetica Neue,Arial,sans-serif" - ) - } - - val wtH3ThemeDark by style { - color("#fff") - } - - val wtButton by style { - color("white") - backgroundColor("#167dff") - fontSize(15.px) - display(DisplayStyle.InlineBlock) - property("text-decoration", "none") - property("border-radius", 24.px) - property("padding", "12px 32px") - property("line-height", 24.px) - property("font-weight", 400) - property("width", "fit-content") - - hover(self) style { - backgroundColor(Color.RGBA(22, 125, 255, .8)) - } - } - - val wtLangButton by style { - display(DisplayStyle.LegacyInlineFlex) - justifyContent(JustifyContent.Center) - alignItems(AlignItems.Center) - backgroundColor(Color("transparent")) - border(0.px) - - property("outline", "none") - - hover(self) style { - backgroundColor(Color.RGBA(255, 255, 255, 0.1)) - } - } - - val wtButtonContrast by style { - color("white") - backgroundColor("#27282c") - - hover(self) style { - backgroundColor(Color.RGBA(39, 40, 44, .7)) - } - } - - val wtSocialButtonItem by style { - property("margin-right", 16.px) - marginLeft(16.px) - padding(12.px) - backgroundColor("transparent") - display(DisplayStyle.LegacyInlineFlex) - - hover(self) style { - backgroundColor(Color.RGBA(255, 255, 255, 0.1)) - property("border-radius", 24.px) - } - - media(maxWidth(640.px)) { - self style { - property("margin-right", 8.px) - property("margin-left", 8.px) - } - } - } -} \ No newline at end of file diff --git a/web/benchmark-core/src/jsMain/resources/index.html b/web/benchmark-core/src/jsMain/resources/index.html deleted file mode 100644 index b9fb031003..0000000000 --- a/web/benchmark-core/src/jsMain/resources/index.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - Compose for Web UI Framework | JetBrains: Developer Tools for Professionals and Teams - - - - - - - - -
- - - \ No newline at end of file diff --git a/web/build.gradle.kts b/web/build.gradle.kts index 6e20c5f141..606211d12d 100644 --- a/web/build.gradle.kts +++ b/web/build.gradle.kts @@ -1,7 +1,8 @@ +val COMPOSE_CORE_VERSION: String by project val COMPOSE_WEB_VERSION: String by project val COMPOSE_REPO_USERNAME: String? by project val COMPOSE_REPO_KEY: String? by project -val COMPOSE_WEB_BUILD_WITH_EXAMPLES = project.property("COMPOSE_WEB_BUILD_WITH_EXAMPLES")!!.toString()?.toBoolean() +val COMPOSE_WEB_BUILD_WITH_SAMPLES = project.property("compose.web.buildSamples")!!.toString().toBoolean() apply() @@ -12,7 +13,7 @@ subprojects { version = COMPOSE_WEB_VERSION pluginManager.withPlugin("maven-publish") { - configure { + configure { repositories { maven { name = "internal" @@ -26,7 +27,7 @@ subprojects { } } - if (COMPOSE_WEB_BUILD_WITH_EXAMPLES) { + if (COMPOSE_WEB_BUILD_WITH_SAMPLES) { println("substituting published artifacts with projects ones in project $name") configurations.all { resolutionStrategy.dependencySubstitution { @@ -42,13 +43,15 @@ subprojects { repositories { gradlePluginPortal() + mavenLocal() mavenCentral() - maven { - url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") + maven { + url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") } maven { url = uri("https://packages.jetbrains.team/maven/p/ui/dev") } + google() } } diff --git a/web/buildSrc/build.gradle.kts b/web/buildSrc/build.gradle.kts index 53c872a4d0..e1bd854525 100644 --- a/web/buildSrc/build.gradle.kts +++ b/web/buildSrc/build.gradle.kts @@ -10,5 +10,5 @@ repositories { } plugins { - id("org.jetbrains.kotlin.jvm") version "1.5.20" + id("org.jetbrains.kotlin.jvm") version "1.5.21" } diff --git a/web/compose-compiler-integration/README.md b/web/compose-compiler-integration/README.md new file mode 100644 index 0000000000..25ae6ecbdb --- /dev/null +++ b/web/compose-compiler-integration/README.md @@ -0,0 +1,9 @@ +RUN from project root directory: +`./gradlew :compose-compiler-integration:checkComposeCases` + + +To use specific version (the default is 0.0.0-SNASPHOT): +`./gradlew :compose-compiler-integration:checkComposeCases -PCOMPOSE_CORE_VERSION=0.5.0-build243` + +To fun only filtered cases (check for contained in file path): +`./gradlew :compose-compiler-integration:checkComposeCases -PFILTER_CASES=CaseName` diff --git a/web/compose-compiler-integration/build.gradle.kts b/web/compose-compiler-integration/build.gradle.kts new file mode 100644 index 0000000000..03de875e9e --- /dev/null +++ b/web/compose-compiler-integration/build.gradle.kts @@ -0,0 +1,165 @@ +fun cloneTemplate(templateName: String, contentMain: String, contentLib: String): File { + val tempDir = file("${project.buildDir.absolutePath}/temp/cloned-$templateName") + tempDir.deleteRecursively() + tempDir.mkdirs() + file("${projectDir.absolutePath}/main-template").copyRecursively(tempDir) + // tempDir.deleteOnExit() + File("$tempDir/src/commonMain/kotlin/Main.kt").printWriter().use { out -> + out.println(contentMain) + } + File("$tempDir/lib/src/commonMain/kotlin/Lib.kt").printWriter().use { out -> + out.println(contentLib) + } + return tempDir +} + +fun build( + caseName: String, + directory: File, + failureExpected: Boolean = false, + composeVersion: String, + vararg buildCmd: String = arrayOf("build", "jsNodeRun") +) { + val isWin = System.getProperty("os.name").startsWith("Win") + val arguments = buildCmd.toMutableList().also { + it.add("-PCOMPOSE_CORE_VERSION=$composeVersion") + }.toTypedArray() + + val procBuilder = if (isWin) { + ProcessBuilder("gradlew.bat", *arguments) + } else { + ProcessBuilder("bash", "./gradlew", *arguments) + } + val proc = procBuilder + .directory(directory) + .redirectOutput(ProcessBuilder.Redirect.PIPE) + .redirectError(ProcessBuilder.Redirect.PIPE) + .start() + + proc.waitFor(5, TimeUnit.MINUTES) + + "(COMPOSE_INTEGRATION_VERSION=\\[.*\\])".toRegex().find( + proc.inputStream.bufferedReader().readText() + )?.also { + println(it.groupValues[1]) + } + + println(proc.errorStream.bufferedReader().readText()) + + if (proc.exitValue() != 0 && !failureExpected) { + throw GradleException("Error compiling $caseName") + } + + if (failureExpected && proc.exitValue() == 0) { + throw AssertionError("$caseName compilation did not fail!!!") + } +} + +data class RunChecksResult( + val cases: Map +) { + val totalCount = cases.size + val failedCount = cases.filter { it.value != null }.size + val hasFailed = failedCount > 0 + + fun printResults() { + cases.forEach { (name, throwable) -> + println(name + " : " + (throwable ?: "OK")) + } + } + + fun reportToTeamCity() { + cases.forEach { (caseName, error) -> + println("##teamcity[testStarted name='compileTestCase_$caseName']") + if (error != null) { + println("##teamcity[testFailed name='compileTestCase_$caseName']") + } + println("##teamcity[testFinished name='compileTestCase_$caseName']") + } + } +} + +fun runCasesInDirectory( + dir: File, + filterPath: String, + expectCompilationError: Boolean, + composeVersion: String +): RunChecksResult { + return dir.listFiles()!!.filter { it.absolutePath.contains(filterPath) }.mapIndexed { _, file -> + println("Running check for ${file.name}, expectCompilationError = $expectCompilationError, composeVersion = $composeVersion") + + val contentLines = file.readLines() + val startMainLineIx = contentLines.indexOf("// @Module:Main").let { ix -> + if (ix == -1) 0 else ix + 1 + } + + val startLibLineIx = contentLines.indexOf("// @Module:Lib").let { ix -> + if (ix == -1) contentLines.size else ix - 1 + } + + require(startMainLineIx < startLibLineIx) { + "The convention is that @Module:Lib should go after @Module:Main" + } + + val mainContent = contentLines.let { lines -> + val endLineIx = if (startLibLineIx < lines.size) startLibLineIx - 1 else lines.lastIndex + lines.slice(startMainLineIx..endLineIx).joinToString(separator = "\n") + } + + val libContent = contentLines.let { lines -> + if (startLibLineIx < lines.size) { + lines.slice(startLibLineIx..lines.lastIndex) + } else { + emptyList() + }.joinToString(separator = "\n") + } + + val caseName = file.name + val tmpDir = cloneTemplate(caseName, contentMain = mainContent, contentLib = libContent) + + caseName to kotlin.runCatching { + build( + caseName = caseName, + directory = tmpDir, + failureExpected = expectCompilationError, + composeVersion = composeVersion + ) + }.exceptionOrNull() + + }.let { + RunChecksResult(it.toMap()) + } +} + +tasks.register("checkComposeCases") { + doLast { + val filterCases = project.findProperty("FILTER_CASES")?.toString() ?: "" + val composeVersion = project.findProperty("COMPOSE_CORE_VERSION")?.toString() ?: "0.0.0-SNASPHOT" + + val expectedFailingCasesDir = File("${projectDir.absolutePath}/testcases/failing") + val expectedFailingResult = runCasesInDirectory( + dir = expectedFailingCasesDir, + expectCompilationError = true, + filterPath = filterCases, + composeVersion = composeVersion + ) + + val passingCasesDir = File("${projectDir.absolutePath}/testcases/passing") + val passingResult = runCasesInDirectory( + dir = passingCasesDir, + expectCompilationError = false, + filterPath = filterCases, + composeVersion = composeVersion + ) + + expectedFailingResult.printResults() + expectedFailingResult.reportToTeamCity() + + passingResult.printResults() + passingResult.reportToTeamCity() + + if (expectedFailingResult.hasFailed || passingResult.hasFailed) { + error("There were failed cases. Check the logs above") + } + } +} diff --git a/examples/web-getting-started/build.gradle.kts b/web/compose-compiler-integration/main-template/build.gradle.kts similarity index 52% rename from examples/web-getting-started/build.gradle.kts rename to web/compose-compiler-integration/main-template/build.gradle.kts index 6fc617b41f..04b3a5a6fd 100644 --- a/examples/web-getting-started/build.gradle.kts +++ b/web/compose-compiler-integration/main-template/build.gradle.kts @@ -1,23 +1,27 @@ plugins { - kotlin("multiplatform") version "1.5.10" - id("org.jetbrains.compose") version "0.5.0-build228" + kotlin("multiplatform") version "1.5.21" + id("org.jetbrains.compose") } repositories { + mavenLocal() mavenCentral() maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") } kotlin { js(IR) { - browser() + nodejs {} + browser() {} binaries.executable() } + sourceSets { - val jsMain by getting { + val commonMain by getting { dependencies { - implementation(compose.web.core) + implementation(kotlin("stdlib-common")) implementation(compose.runtime) + implementation(project(":lib")) } } } diff --git a/examples/web_landing/gradle/wrapper/gradle-wrapper.jar b/web/compose-compiler-integration/main-template/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from examples/web_landing/gradle/wrapper/gradle-wrapper.jar rename to web/compose-compiler-integration/main-template/gradle/wrapper/gradle-wrapper.jar diff --git a/web/compose-compiler-integration/main-template/gradle/wrapper/gradle-wrapper.properties b/web/compose-compiler-integration/main-template/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..0f80bbf516 --- /dev/null +++ b/web/compose-compiler-integration/main-template/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/web/compose-compiler-integration/main-template/gradlew b/web/compose-compiler-integration/main-template/gradlew new file mode 100755 index 0000000000..fbd7c51583 --- /dev/null +++ b/web/compose-compiler-integration/main-template/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# 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 +# +# https://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. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/web/compose-compiler-integration/main-template/gradlew.bat b/web/compose-compiler-integration/main-template/gradlew.bat new file mode 100644 index 0000000000..5093609d51 --- /dev/null +++ b/web/compose-compiler-integration/main-template/gradlew.bat @@ -0,0 +1,104 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/web/compose-compiler-integration/main-template/lib/build.gradle.kts b/web/compose-compiler-integration/main-template/lib/build.gradle.kts new file mode 100644 index 0000000000..784f3b836b --- /dev/null +++ b/web/compose-compiler-integration/main-template/lib/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + kotlin("multiplatform")// version "1.5.10" + id("org.jetbrains.compose")// version (System.getenv("COMPOSE_INTEGRATION_VERSION") ?: "0.0.0-SNASPHOT") +} + +repositories { + mavenCentral() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") +} + +kotlin { + js(IR) { + nodejs {} + browser() {} + binaries.executable() + } + + sourceSets { + val commonMain by getting { + dependencies { + implementation(kotlin("stdlib-common")) + implementation(compose.runtime) + } + } + } +} diff --git a/web/compose-compiler-integration/main-template/lib/src/commonMain/kotlin/Lib.kt b/web/compose-compiler-integration/main-template/lib/src/commonMain/kotlin/Lib.kt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/web/compose-compiler-integration/main-template/settings.gradle.kts b/web/compose-compiler-integration/main-template/settings.gradle.kts new file mode 100644 index 0000000000..11be86e909 --- /dev/null +++ b/web/compose-compiler-integration/main-template/settings.gradle.kts @@ -0,0 +1,24 @@ +pluginManagement { + repositories { + gradlePluginPortal() + maven { + url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") + } + } + + resolutionStrategy { + eachPlugin { + if (requested.id.id == "org.jetbrains.compose") { + val useVersion = if (extra.has("COMPOSE_CORE_VERSION")) { + extra["COMPOSE_CORE_VERSION"].toString() + } else { + "0.0.0-SNASPHOT" + } + println("COMPOSE_INTEGRATION_VERSION=[$useVersion]") + useVersion(useVersion) + } + } + } +} + +include(":lib") diff --git a/web/compose-compiler-integration/main-template/src/commonMain/kotlin/Main.kt b/web/compose-compiler-integration/main-template/src/commonMain/kotlin/Main.kt new file mode 100644 index 0000000000..9e9b8221ae --- /dev/null +++ b/web/compose-compiler-integration/main-template/src/commonMain/kotlin/Main.kt @@ -0,0 +1 @@ +// Empty file. It's used as a template project. diff --git a/web/compose-compiler-integration/testcases/failing/CompanionGetValueOperatorComposable.kt b/web/compose-compiler-integration/testcases/failing/CompanionGetValueOperatorComposable.kt new file mode 100644 index 0000000000..f5a0dd9917 --- /dev/null +++ b/web/compose-compiler-integration/testcases/failing/CompanionGetValueOperatorComposable.kt @@ -0,0 +1,25 @@ +// @Module:Main + +// https://github.com/JetBrains/compose-jb/issues/827 + +import kotlin.reflect.KProperty +import androidx.compose.runtime.Composable + +interface Router { + companion object { + @Composable + operator fun getValue(ref: Any?, property: KProperty<*>): Router { + return object : Router {} + } + } +} + +fun main() { + callComposable { + val router by Router + } +} + +fun callComposable(content: @Composable () -> Unit) { + // does nothing +} diff --git a/web/compose-compiler-integration/testcases/failing/InstanceGetValueOperatorComposable.kt b/web/compose-compiler-integration/testcases/failing/InstanceGetValueOperatorComposable.kt new file mode 100644 index 0000000000..9f2e6d36ec --- /dev/null +++ b/web/compose-compiler-integration/testcases/failing/InstanceGetValueOperatorComposable.kt @@ -0,0 +1,23 @@ +// @Module:Main + +// https://github.com/JetBrains/compose-jb/issues/827 + +import kotlin.reflect.KProperty +import androidx.compose.runtime.Composable + +class Router { + @Composable + operator fun getValue(ref: Any?, property: KProperty<*>): Router { + return Router() + } +} + +fun main() { + callComposable { + val router by Router() + } +} + +fun callComposable(content: @Composable () -> Unit) { + // does nothing +} diff --git a/web/compose-compiler-integration/testcases/failing/WithComposableBlockUsingTypeParameterAndDefaultValue.kt b/web/compose-compiler-integration/testcases/failing/WithComposableBlockUsingTypeParameterAndDefaultValue.kt new file mode 100644 index 0000000000..8a17d35e11 --- /dev/null +++ b/web/compose-compiler-integration/testcases/failing/WithComposableBlockUsingTypeParameterAndDefaultValue.kt @@ -0,0 +1,30 @@ +// https://github.com/JetBrains/compose-jb/issues/774 + +import androidx.compose.runtime.Composable + +fun main() { + callComposable { + Foo { } + FooTakesTypedComposableLambda2("T") + FooTakesTypedComposableLambda3("T") + } +} + +fun callComposable(content: @Composable () -> Unit) { + // does nothing +} + +class RouterState + +@Composable +fun Foo(block: @Composable (RouterState) -> Unit = {}) {} + +@Composable +fun FooTakesTypedComposableLambda2(t: T, composable: @Composable (T) -> T = { t }) { + composable(t) +} + +@Composable +fun FooTakesTypedComposableLambda3(t: T, composable: @Composable () -> T = { t }) { + composable() +} diff --git a/web/compose-compiler-integration/testcases/passing/ComposableWithParamsWithDefaultValues.kt b/web/compose-compiler-integration/testcases/passing/ComposableWithParamsWithDefaultValues.kt new file mode 100644 index 0000000000..39585b16e9 --- /dev/null +++ b/web/compose-compiler-integration/testcases/passing/ComposableWithParamsWithDefaultValues.kt @@ -0,0 +1,67 @@ +// @Module:Main +import androidx.compose.runtime.Composable +import androidx.compose.runtime.currentComposer +import androidx.compose.runtime.Composer + +fun main() { + callComposable { + FooTakesLambda() + InlineFooTakesLambda() + + FooTakesComposableLambda() + InlineFooTakesComposableLambda() + + FooTakesTypedExtesionComposableLambdaWithExplicitTypesAndDefaultLambda("4", 5) + ComposableWithDifferentDefaultValuesForParameters(a = Any()) + ComposableWithReturnAndWithDefaultLambda().invoke() + } +} + +fun callComposable(content: @Composable () -> Unit) { + val c = content +} + +// @Module:Lib +import androidx.compose.runtime.Composable + +@Composable +fun FooTakesLambda(block: () -> Unit = {}) { + block() +} + +@Composable +inline fun InlineFooTakesLambda(block: () -> Unit = {}) { + block() +} + +@Composable +fun FooTakesComposableLambda(composable: @Composable () -> Unit = {}) { + composable() +} + +@Composable +inline fun InlineFooTakesComposableLambda(composable: @Composable () -> Unit = {}) { + composable() +} + +@Composable +fun FooTakesTypedExtesionComposableLambdaWithExplicitTypesAndDefaultLambda( + t: String, k: Int, composable: @Composable String.(Int) -> Double = { (this + ". $it").toDouble() } +) { + t.composable(k) +} + +@Composable +fun ComposableWithDifferentDefaultValuesForParameters( + a: Any, i: Int = 1, b: Boolean = false, s: String = "s", + u: Unit = Unit, a2: Any = Any(), l: List = listOf("1") +) { + a.toString() + "$i $b $s $u $a2 $l" +} + +@Composable +fun ComposableWithReturnAndWithDefaultLambda( + l: @Composable () -> (@Composable () -> Unit) = { { } } +): @Composable () -> Unit { + return { l() } +} diff --git a/web/compose-compiler-integration/testcases/passing/ComposableWithTypeParams.kt b/web/compose-compiler-integration/testcases/passing/ComposableWithTypeParams.kt new file mode 100644 index 0000000000..e3ecb6ee57 --- /dev/null +++ b/web/compose-compiler-integration/testcases/passing/ComposableWithTypeParams.kt @@ -0,0 +1,36 @@ +// @Module:Main +import androidx.compose.runtime.Composable +import androidx.compose.runtime.currentComposer +import androidx.compose.runtime.Composer + +fun main() { + callComposable { + + FooTakesTypedComposableLambda { "text" } + FooTakesTypedComposableLambda2(10) { it + 100 } + FooTakesTypedExtesionComposableLambda("text", Any()) { } + } +} + +fun callComposable(content: @Composable () -> Unit) { + val c = content +} + +// @Module:Lib +import androidx.compose.runtime.Composable + + +@Composable +fun FooTakesTypedComposableLambda(composable: @Composable () -> T) { + composable() +} + +@Composable +fun FooTakesTypedComposableLambda2(t: T, composable: @Composable (T) -> T) { + composable(t) +} + +@Composable +fun FooTakesTypedExtesionComposableLambda(t: T, k: K, composable: @Composable T.(K) -> R) { + t.composable(k) +} diff --git a/web/compose-compiler-integration/testcases/passing/PassingComposableToConstructor.kt b/web/compose-compiler-integration/testcases/passing/PassingComposableToConstructor.kt new file mode 100644 index 0000000000..998f694ddf --- /dev/null +++ b/web/compose-compiler-integration/testcases/passing/PassingComposableToConstructor.kt @@ -0,0 +1,45 @@ +// @Module:Main + +// https://youtrack.jetbrains.com/issue/KT-46880 +import androidx.compose.runtime.Composable + +fun main() { + val instance = testCase { } + val instance2 = TestCase2() + + callComposable { + instance.composable() + instance2.composable() + } +} + +fun callComposable(content: @Composable () -> Unit) { + // does nothing +} + +// @Module:Lib +import androidx.compose.runtime.Composable +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + +class TestCase(val composable: @Composable () -> Unit) { + operator fun provideDelegate( + thisRef: Any, + property: KProperty<*> + ): ReadOnlyProperty { + return ReadOnlyProperty { _, _ -> property.name } + } +} + +class TestCase2(val composable: @Composable () -> Unit = {}) { + operator fun provideDelegate( + thisRef: Any, + property: KProperty<*> + ): ReadOnlyProperty { + return ReadOnlyProperty { _, _ -> property.name } + } +} + +fun testCase(composable: @Composable () -> Unit): TestCase { + return TestCase(composable) +} diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/attributes/AttrsBuilder.kt b/web/core/src/jsMain/kotlin/androidx/compose/web/attributes/AttrsBuilder.kt deleted file mode 100644 index dfd9f4a02e..0000000000 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/attributes/AttrsBuilder.kt +++ /dev/null @@ -1,77 +0,0 @@ -package org.jetbrains.compose.web.attributes - -import androidx.compose.runtime.DisposableEffectResult -import androidx.compose.runtime.DisposableEffectScope -import org.jetbrains.compose.web.css.StyleBuilder -import org.jetbrains.compose.web.css.StyleBuilderImpl -import org.w3c.dom.Element -import org.w3c.dom.HTMLElement - -open class AttrsBuilder : EventsListenerBuilder() { - internal val attributesMap = mutableMapOf() - val styleBuilder = StyleBuilderImpl() - - val propertyUpdates = mutableListOf Unit, Any>>() - var refEffect: (DisposableEffectScope.(Element) -> DisposableEffectResult)? = null - - fun style(builder: StyleBuilder.() -> Unit) { - styleBuilder.apply(builder) - } - - fun classes(vararg classes: String) = prop(setClassList, classes) - - fun id(value: String) = attr(ID, value) - fun hidden() = attr(HIDDEN, true.toString()) - fun title(value: String) = attr(TITLE, value) - fun dir(value: DirType) = attr(DIR, value.dirStr) - fun draggable(value: Draggable) = attr(DRAGGABLE, value.str) - fun contentEditable(value: Boolean) = attr(CONTENT_EDITABLE, value.toString()) - fun lang(value: String) = attr(LANG, value) - fun tabIndex(value: Int) = attr(TAB_INDEX, value.toString()) - fun spellCheck(value: Boolean) = attr(SPELLCHECK, value.toString()) - - fun ref(effect: DisposableEffectScope.(Element) -> DisposableEffectResult) { - this.refEffect = effect - } - - fun attr(attr: String, value: String): AttrsBuilder { - attributesMap[attr] = value - return this - } - - @Suppress("UNCHECKED_CAST") - fun prop(update: (E, V) -> Unit, value: V) { - propertyUpdates.add((update to value) as Pair<(Element, Any) -> Unit, Any>) - } - - fun collect(): Map { - return attributesMap - } - - internal fun copyFrom(attrsBuilder: AttrsBuilder) { - refEffect = attrsBuilder.refEffect - styleBuilder.copyFrom(attrsBuilder.styleBuilder) - - attributesMap.putAll(attrsBuilder.attributesMap) - propertyUpdates.addAll(attrsBuilder.propertyUpdates) - - copyListenersFrom(attrsBuilder) - } - - companion object { - const val CLASS = "class" - const val ID = "id" - const val HIDDEN = "hidden" - const val TITLE = "title" - const val DIR = "dir" - const val DRAGGABLE = "draggable" - const val CONTENT_EDITABLE = "contenteditable" - const val LANG = "lang" - const val TAB_INDEX = "tabindex" - const val SPELLCHECK = "spellcheck" - } -} - -val setClassList: (HTMLElement, Array) -> Unit = { e, classList -> - e.classList.add(*classList) -} diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/attributes/InputAttrsBuilder.kt b/web/core/src/jsMain/kotlin/androidx/compose/web/attributes/InputAttrsBuilder.kt deleted file mode 100644 index 4d96b08a30..0000000000 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/attributes/InputAttrsBuilder.kt +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ - -package androidx.compose.web.attributes - -import org.jetbrains.compose.web.attributes.* -import org.jetbrains.compose.web.events.GenericWrappedEvent -import org.jetbrains.compose.web.events.WrappedCheckBoxInputEvent -import org.jetbrains.compose.web.events.WrappedRadioInputEvent -import org.jetbrains.compose.web.events.WrappedTextInputEvent -import org.w3c.dom.HTMLElement -import org.w3c.dom.HTMLInputElement -import org.w3c.dom.events.Event -import org.w3c.dom.events.EventTarget - -class SyntheticInputEvent( - val value: ValueType, - val target: Element, - val nativeEvent: Event -) { - - val bubbles: Boolean = nativeEvent.bubbles - val cancelable: Boolean = nativeEvent.cancelable - val composed: Boolean = nativeEvent.composed - val currentTarget: HTMLElement? = nativeEvent.currentTarget.unsafeCast() - val eventPhase: Short = nativeEvent.eventPhase - val defaultPrevented: Boolean = nativeEvent.defaultPrevented - val timestamp: Number = nativeEvent.timeStamp - val type: String = nativeEvent.type - val isTrusted: Boolean = nativeEvent.isTrusted - - fun preventDefault(): Unit = nativeEvent.preventDefault() - fun stopPropagation(): Unit = nativeEvent.stopPropagation() - fun stopImmediatePropagation(): Unit = nativeEvent.stopImmediatePropagation() - fun composedPath(): Array = nativeEvent.composedPath() -} - -class InputAttrsBuilder(val inputType: InputType) : AttrsBuilder() { - - fun onInput(options: Options = Options.DEFAULT, listener: (SyntheticInputEvent) -> Unit) { - addEventListener(INPUT, options) { - val value = inputType.inputValue(it.nativeEvent) - listener(SyntheticInputEvent(value, it.nativeEvent.target as HTMLInputElement, it.nativeEvent)) - } - } - - @Deprecated( - message = "It's not reliable as it can be applied to any input type.", - replaceWith = ReplaceWith("onInput(options, listener)"), - level = DeprecationLevel.WARNING - ) - fun onTextInput(options: Options = Options.DEFAULT, listener: (WrappedTextInputEvent) -> Unit) { - listeners.add(TextInputEventListener(options, listener)) - } - - @Deprecated( - message = "It's not reliable as it can be applied to any input type.", - replaceWith = ReplaceWith("onInput(options, listener)"), - level = DeprecationLevel.WARNING - ) - fun onCheckboxInput( - options: Options = Options.DEFAULT, - listener: (WrappedCheckBoxInputEvent) -> Unit - ) { - listeners.add(CheckBoxInputEventListener(options, listener)) - } - - @Deprecated( - message = "It's not reliable as it can be applied to any input type.", - replaceWith = ReplaceWith("onInput(options, listener)"), - level = DeprecationLevel.WARNING - ) - fun onRadioInput( - options: Options = Options.DEFAULT, - listener: (WrappedRadioInputEvent) -> Unit - ) { - listeners.add(RadioInputEventListener(options, listener)) - } - - @Deprecated( - message = "It's not reliable as it can be applied to any input type.", - replaceWith = ReplaceWith("onInput(options, listener)"), - level = DeprecationLevel.WARNING - ) - fun onRangeInput( - options: Options = Options.DEFAULT, - listener: (GenericWrappedEvent<*>) -> Unit - ) { - listeners.add(WrappedEventListener(INPUT, options, listener)) - } -} diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/attributes/PredefinedAttrValues.kt b/web/core/src/jsMain/kotlin/androidx/compose/web/attributes/PredefinedAttrValues.kt deleted file mode 100644 index 1d2b604d5e..0000000000 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/attributes/PredefinedAttrValues.kt +++ /dev/null @@ -1,163 +0,0 @@ -package org.jetbrains.compose.web.attributes - -import org.w3c.dom.events.Event - -sealed class InputType(val typeStr: String) { - - object Button : InputTypeWithUnitValue("button") - object Checkbox : InputTypeCheckedValue("checkbox") - object Color : InputTypeWithStringValue("color") - object Date : InputTypeWithStringValue("date") - object DateTimeLocal : InputTypeWithStringValue("datetime-local") - object Email : InputTypeWithStringValue("email") - object File : InputTypeWithStringValue("file") - object Hidden : InputTypeWithStringValue("hidden") - object Month : InputTypeWithStringValue("month") - object Number : InputTypeNumberValue("number") - object Password : InputTypeWithStringValue("password") - object Radio : InputTypeCheckedValue("radio") - object Range : InputTypeNumberValue("range") - object Search : InputTypeWithStringValue("search") - object Submit : InputTypeWithUnitValue("submit") - object Tel : InputTypeWithStringValue("tel") - object Text : InputTypeWithStringValue("text") - object Time : InputTypeWithStringValue("time") - object Url : InputTypeWithStringValue("url") - object Week : InputTypeWithStringValue("week") - - open class InputTypeWithStringValue(name: String) : InputType(name) { - override fun inputValue(event: Event) = Week.valueAsString(event) - } - - open class InputTypeWithUnitValue(name: String) : InputType(name) { - override fun inputValue(event: Event) = Unit - } - - open class InputTypeCheckedValue(name: String) : InputType(name) { - override fun inputValue(event: Event): Boolean { - return event.target?.asDynamic()?.checked?.unsafeCast() ?: false - } - } - - open class InputTypeNumberValue(name: String) : InputType(name) { - override fun inputValue(event: Event): kotlin.Number? { - return event.target?.asDynamic()?.valueAsNumber ?: null - } - } - - abstract fun inputValue(event: Event): T - - protected fun valueAsString(event: Event): String { - return event.target?.asDynamic()?.value?.unsafeCast() ?: "" - } -} - -sealed class DirType(val dirStr: String) { - object Ltr : DirType("ltr") - object Rtl : DirType("rtl") - object Auto : DirType("auto") -} - -sealed class ATarget(val targetStr: String) { - object Blank : ATarget("_blank") - object Parent : ATarget("_parent") - object Self : ATarget("_self") - object Top : ATarget("_top") -} - -sealed class ARel(val relStr: String) { - object Alternate : ARel("alternate") - object Author : ARel("author") - object Bookmark : ARel("bookmark") - object External : ARel("external") - object Help : ARel("help") - object License : ARel("license") - object Next : ARel("next") - object First : ARel("first") - object Prev : ARel("prev") - object Last : ARel("last") - object NoFollow : ARel("nofollow") - object NoOpener : ARel("noopener") - object NoReferrer : ARel("noreferrer") - object Opener : ARel("opener") - object Search : ARel("search") - object Tag : ARel("tag") - - class CustomARel(value: String) : ARel(value) -} - -enum class Draggable(val str: String) { - True("true"), False("false"), Auto("auto"); -} - -enum class ButtonType(val str: String) { - Button("button"), Reset("reset"), Submit("submit") -} - -sealed class ButtonFormTarget(val targetStr: String) { - object Blank : ButtonFormTarget("_blank") - object Parent : ButtonFormTarget("_parent") - object Self : ButtonFormTarget("_self") - object Top : ButtonFormTarget("_top") -} - -enum class ButtonFormMethod(val methodStr: String) { - Get("get"), Post("post") -} - -enum class ButtonFormEncType(val typeStr: String) { - MultipartFormData("multipart/form-data"), - ApplicationXWwwFormUrlEncoded("application/x-www-form-urlencoded"), - TextPlain("text/plain") -} - -enum class FormEncType(val typeStr: String) { - MultipartFormData("multipart/form-data"), - ApplicationXWwwFormUrlEncoded("application/x-www-form-urlencoded"), - TextPlain("text/plain") -} - -enum class FormMethod(val methodStr: String) { - Get("get"), - Post("post"), - Dialog("dialog") -} - -sealed class FormTarget(val targetStr: String) { - object Blank : FormTarget("_blank") - object Parent : FormTarget("_parent") - object Self : FormTarget("_self") - object Top : FormTarget("_top") -} - -enum class InputFormEncType(val typeStr: String) { - MultipartFormData("multipart/form-data"), - ApplicationXWwwFormUrlEncoded("application/x-www-form-urlencoded"), - TextPlain("text/plain") -} - -enum class InputFormMethod(val methodStr: String) { - Get("get"), - Post("post"), - Dialog("dialog") -} - -sealed class InputFormTarget(val targetStr: String) { - object Blank : InputFormTarget("_blank") - object Parent : InputFormTarget("_parent") - object Self : InputFormTarget("_self") - object Top : InputFormTarget("_top") -} - -enum class TextAreaWrap(val str: String) { - Hard("hard"), - Soft("soft"), - Off("off") -} - -enum class Scope(val str: String) { - Row("row"), - Rowgroup("rowgroup"), - Col("col"), - Colgroup("colgroup") -} diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/attributes/TextAreaAttrsBuilder.kt b/web/core/src/jsMain/kotlin/androidx/compose/web/attributes/TextAreaAttrsBuilder.kt deleted file mode 100644 index 9231233a18..0000000000 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/attributes/TextAreaAttrsBuilder.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ - -package androidx.compose.web.attributes - -import org.jetbrains.compose.web.attributes.* -import org.jetbrains.compose.web.events.WrappedTextInputEvent -import org.w3c.dom.HTMLTextAreaElement - -class TextAreaAttrsBuilder : AttrsBuilder() { - - fun onInput( - options: Options = Options.DEFAULT, - listener: (SyntheticInputEvent) -> Unit - ) { - addEventListener(INPUT, options) { - val text = it.nativeEvent.target.asDynamic().value.unsafeCast() - listener(SyntheticInputEvent(text, it.nativeEvent.target as HTMLTextAreaElement, it.nativeEvent)) - } - } - - @Deprecated( - message = "It's not reliable as it can be applied to any input type.", - replaceWith = ReplaceWith("onInput(options, listener)"), - level = DeprecationLevel.WARNING - ) - fun onTextInput(options: Options = Options.DEFAULT, listener: (WrappedTextInputEvent) -> Unit) { - listeners.add(TextInputEventListener(options, listener)) - } -} diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/attributes/WrappedEventListener.kt b/web/core/src/jsMain/kotlin/androidx/compose/web/attributes/WrappedEventListener.kt deleted file mode 100644 index 2fd626a7ba..0000000000 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/attributes/WrappedEventListener.kt +++ /dev/null @@ -1,168 +0,0 @@ -package org.jetbrains.compose.web.attributes - -import org.jetbrains.compose.web.events.GenericWrappedEvent -import org.jetbrains.compose.web.events.WrappedCheckBoxInputEvent -import org.jetbrains.compose.web.events.WrappedClipboardEvent -import org.jetbrains.compose.web.events.WrappedDragEvent -import org.jetbrains.compose.web.events.WrappedEventImpl -import org.jetbrains.compose.web.events.WrappedFocusEvent -import org.jetbrains.compose.web.events.WrappedInputEvent -import org.jetbrains.compose.web.events.WrappedKeyboardEvent -import org.jetbrains.compose.web.events.WrappedMouseEvent -import org.jetbrains.compose.web.events.WrappedPointerEvent -import org.jetbrains.compose.web.events.WrappedRadioInputEvent -import org.jetbrains.compose.web.events.WrappedTextInputEvent -import org.jetbrains.compose.web.events.WrappedTouchEvent -import org.jetbrains.compose.web.events.WrappedWheelEvent -import org.w3c.dom.DragEvent -import org.w3c.dom.TouchEvent -import org.w3c.dom.clipboard.ClipboardEvent -import org.w3c.dom.events.Event -import org.w3c.dom.events.FocusEvent -import org.w3c.dom.events.InputEvent -import org.w3c.dom.events.KeyboardEvent -import org.w3c.dom.events.MouseEvent -import org.w3c.dom.events.WheelEvent -import org.w3c.dom.pointerevents.PointerEvent - -open class WrappedEventListener>( - val event: String, - val options: Options, - val listener: (T) -> Unit -) : org.w3c.dom.events.EventListener { - - @Suppress("UNCHECKED_CAST") - override fun handleEvent(event: Event) { - listener(WrappedEventImpl(event) as T) - } -} - -class Options { - // TODO: add options for addEventListener - - companion object { - val DEFAULT = Options() - } -} - -internal class MouseEventListener( - event: String, - options: Options, - listener: (WrappedMouseEvent) -> Unit -) : WrappedEventListener(event, options, listener) { - override fun handleEvent(event: Event) { - listener(WrappedMouseEvent(event as MouseEvent)) - } -} - -internal class MouseWheelEventListener( - event: String, - options: Options, - listener: (WrappedWheelEvent) -> Unit -) : WrappedEventListener(event, options, listener) { - override fun handleEvent(event: Event) { - listener(WrappedWheelEvent(event as WheelEvent)) - } -} - -internal class KeyboardEventListener( - event: String, - options: Options, - listener: (WrappedKeyboardEvent) -> Unit -) : WrappedEventListener(event, options, listener) { - override fun handleEvent(event: Event) { - listener(WrappedKeyboardEvent(event as KeyboardEvent)) - } -} - -internal class FocusEventListener( - event: String, - options: Options, - listener: (WrappedFocusEvent) -> Unit -) : WrappedEventListener(event, options, listener) { - override fun handleEvent(event: Event) { - listener(WrappedFocusEvent(event as FocusEvent)) - } -} - -internal class TouchEventListener( - event: String, - options: Options, - listener: (WrappedTouchEvent) -> Unit -) : WrappedEventListener(event, options, listener) { - override fun handleEvent(event: Event) { - listener(WrappedTouchEvent(event as TouchEvent)) - } -} - -internal class DragEventListener( - event: String, - options: Options, - listener: (WrappedDragEvent) -> Unit -) : WrappedEventListener(event, options, listener) { - override fun handleEvent(event: Event) { - listener(WrappedDragEvent(event as DragEvent)) - } -} - -internal class PointerEventListener( - event: String, - options: Options, - listener: (WrappedPointerEvent) -> Unit -) : WrappedEventListener(event, options, listener) { - override fun handleEvent(event: Event) { - listener(WrappedPointerEvent(event as PointerEvent)) - } -} - -internal class ClipboardEventListener( - event: String, - options: Options, - listener: (WrappedClipboardEvent) -> Unit -) : WrappedEventListener(event, options, listener) { - override fun handleEvent(event: Event) { - listener(WrappedClipboardEvent(event as ClipboardEvent)) - } -} - -internal class InputEventListener( - event: String, - options: Options, - listener: (WrappedInputEvent) -> Unit -) : WrappedEventListener(event, options, listener) { - override fun handleEvent(event: Event) { - listener(WrappedInputEvent(event as InputEvent)) - } -} - -internal class RadioInputEventListener( - options: Options, - listener: (WrappedRadioInputEvent) -> Unit -) : WrappedEventListener(EventsListenerBuilder.INPUT, options, listener) { - override fun handleEvent(event: Event) { - val checked = event.target.asDynamic().checked as Boolean - listener(WrappedRadioInputEvent(event, checked)) - } -} - -internal class CheckBoxInputEventListener( - options: Options, - listener: (WrappedCheckBoxInputEvent) -> Unit -) : WrappedEventListener( - EventsListenerBuilder.INPUT, options, listener -) { - override fun handleEvent(event: Event) { - val checked = event.target.asDynamic().checked as Boolean - listener(WrappedCheckBoxInputEvent(event, checked)) - } -} - -internal class TextInputEventListener( - options: Options, - listener: (WrappedTextInputEvent) -> Unit -) : WrappedEventListener(EventsListenerBuilder.INPUT, options, listener) { - override fun handleEvent(event: Event) { - val text = event.target.asDynamic().value as String - listener(WrappedTextInputEvent(event as InputEvent, text)) - } -} diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSProperties.kt b/web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSProperties.kt deleted file mode 100644 index 32d56f27a5..0000000000 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSProperties.kt +++ /dev/null @@ -1,239 +0,0 @@ -@file:Suppress("Unused", "NOTHING_TO_INLINE") - -package org.jetbrains.compose.web.css - -import org.jetbrains.compose.web.css.keywords.CSSAutoKeyword - -fun StyleBuilder.opacity(value: Number) { - property("opacity", value) -} - -fun StyleBuilder.order(value: Int) { - property("order", value) -} - -fun StyleBuilder.flexGrow(value: Number) { - property("flex-grow", value) -} - -fun StyleBuilder.flexShrink(value: Number) { - property("flex-shrink", value) -} - -fun StyleBuilder.opacity(value: CSSSizeValue) { - property("opacity", (value.value / 100)) -} - -fun StyleBuilder.color(value: String) { - property("color", value) -} - -fun StyleBuilder.color(value: CSSColorValue) { - // color hasn't Typed OM yet - property("color", value) -} - -fun StyleBuilder.backgroundColor(value: CSSColorValue) { - property("background-color", value) -} - -fun StyleBuilder.backgroundColor(value: String) { - property("background-color", value) -} - -@Suppress("EqualsOrHashCode") -class CSSBorder : CSSStyleValue { - var width: CSSNumeric? = null - var style: LineStyle? = null - var color: CSSColorValue? = null - - override fun equals(other: Any?): Boolean { - return if (other is CSSBorder) { - width == other.width && style == other.style && color == other.color - } else false - } - - override fun toString(): String { - val values = listOfNotNull(width, style, color) - return values.joinToString(" ") - } -} - -inline fun CSSBorder.width(size: CSSNumeric) { - width = size -} - -inline fun CSSBorder.style(style: LineStyle) { - this.style = style -} - -inline fun CSSBorder.color(color: CSSColorValue) { - this.color = color -} - -inline fun StyleBuilder.border(crossinline borderBuild: CSSBorder.() -> Unit) { - property("border", CSSBorder().apply(borderBuild)) -} - -fun StyleBuilder.border( - width: CSSLengthValue? = null, - style: LineStyle? = null, - color: CSSColorValue? = null -) { - border { - width?.let { width(it) } - style?.let { style(it) } - color?.let { color(it) } - } -} - -fun StyleBuilder.display(displayStyle: DisplayStyle) { - property("display", displayStyle.value) -} - -fun StyleBuilder.flexDirection(flexDirection: FlexDirection) { - property("flex-direction", flexDirection.value) -} - -fun StyleBuilder.flexWrap(flexWrap: FlexWrap) { - property("flex-wrap", flexWrap.value) -} - -fun StyleBuilder.flexFlow(flexDirection: FlexDirection, flexWrap: FlexWrap) { - property( - "flex-flow", - "${flexDirection.value} ${flexWrap.value}" - ) -} - -fun StyleBuilder.justifyContent(justifyContent: JustifyContent) { - property( - "justify-content", - justifyContent.value - ) -} -fun StyleBuilder.alignSelf(alignSelf: AlignSelf) { - property( - "align-self", - alignSelf.value - ) -} - -fun StyleBuilder.alignItems(alignItems: AlignItems) { - property( - "align-items", - alignItems.value - ) -} - -fun StyleBuilder.alignContent(alignContent: AlignContent) { - property( - "align-content", - alignContent.value - ) -} - -fun StyleBuilder.position(position: Position) { - property( - "position", - position.value - ) -} - -fun StyleBuilder.borderRadius(r: CSSNumeric) { - property("border-radius", r) -} - -fun StyleBuilder.borderRadius(topLeft: CSSNumeric, bottomRight: CSSNumeric) { - property("border-radius", "$topLeft $bottomRight") -} - -fun StyleBuilder.borderRadius( - topLeft: CSSNumeric, - topRightAndBottomLeft: CSSNumeric, - bottomRight: CSSNumeric -) { - property("border-radius", "$topLeft $topRightAndBottomLeft $bottomRight") -} - -fun StyleBuilder.borderRadius( - topLeft: CSSNumeric, - topRight: CSSNumeric, - bottomRight: CSSNumeric, - bottomLeft: CSSNumeric -) { - property( - "border-radius", - "$topLeft $topRight $bottomRight $bottomLeft" - ) -} - -fun StyleBuilder.width(value: CSSNumeric) { - property("width", value) -} - -fun StyleBuilder.width(value: CSSAutoKeyword) { - property("width", value) -} - -fun StyleBuilder.height(value: CSSNumeric) { - property("height", value) -} - -fun StyleBuilder.height(value: CSSAutoKeyword) { - property("height", value) -} - -fun StyleBuilder.top(value: CSSLengthOrPercentageValue) { - property("top", value) -} - -fun StyleBuilder.top(value: CSSAutoKeyword) { - property("top", value) -} - -fun StyleBuilder.bottom(value: CSSLengthOrPercentageValue) { - property("bottom", value) -} - -fun StyleBuilder.bottom(value: CSSAutoKeyword) { - property("bottom", value) -} - -fun StyleBuilder.left(value: CSSLengthOrPercentageValue) { - property("left", value) -} - -fun StyleBuilder.left(value: CSSAutoKeyword) { - property("left", value) -} - -fun StyleBuilder.right(value: CSSLengthOrPercentageValue) { - property("right", value) -} - -fun StyleBuilder.right(value: CSSAutoKeyword) { - property("right", value) -} - -fun StyleBuilder.fontSize(value: CSSNumeric) { - property("font-size", value) -} - -fun StyleBuilder.margin(value: CSSNumeric) { - // marign hasn't Typed OM yet - property("margin", value) -} - -fun StyleBuilder.marginLeft(value: CSSNumeric) { - property("margin-left", value) -} - -fun StyleBuilder.marginTop(value: CSSNumeric) { - property("margin-top", value) -} - -fun StyleBuilder.padding(value: CSSNumeric) { - // padding hasn't Typed OM yet - property("padding", value) -} diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/css/Color.kt b/web/core/src/jsMain/kotlin/androidx/compose/web/css/Color.kt deleted file mode 100644 index 0726eac2e2..0000000000 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/css/Color.kt +++ /dev/null @@ -1,34 +0,0 @@ -@file:Suppress("unused", "MemberVisibilityCanBePrivate") - -package org.jetbrains.compose.web.css - -external interface CSSColorValue: StylePropertyValue, CSSVariableValueAs - -abstract class Color : CSSStyleValue, CSSColorValue { - data class Named(val value: String) : Color() { - override fun toString(): String = value - } - - data class RGB(val r: Number, val g: Number, val b: Number) : Color() { - override fun toString(): String = "rgb($r, $g, $b)" - } - - data class RGBA(val r: Number, val g: Number, val b: Number, val a: Number) : Color() { - override fun toString(): String = "rgba($r, $g, $b, $a)" - } - - data class HSL(val h: CSSAngleValue, val s: Number, val l: Number) : Color() { - constructor(h: Number, s: Number, l: Number) : this(h.deg, s, l) - - override fun toString(): String = "hsl($h, $s%, $l%)" - } - - data class HSLA(val h: CSSAngleValue, val s: Number, val l: Number, val a: Number) : Color() { - constructor(h: Number, s: Number, l: Number, a: Number) : this(h.deg, s, l, a) - - override fun toString(): String = "hsla($h, $s%, $l%, $a)" - } - -} - -fun Color(name: String): Color = Color.Named(name) \ No newline at end of file diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/elements/Base.kt b/web/core/src/jsMain/kotlin/androidx/compose/web/elements/Base.kt deleted file mode 100644 index 784c5dec87..0000000000 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/elements/Base.kt +++ /dev/null @@ -1,244 +0,0 @@ -package org.jetbrains.compose.web.dom - -import androidx.compose.runtime.Applier -import androidx.compose.runtime.Composable -import androidx.compose.runtime.ComposeCompilerApi -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.DisposableEffectResult -import androidx.compose.runtime.DisposableEffectScope -import androidx.compose.runtime.ExplicitGroupsComposable -import androidx.compose.runtime.SkippableUpdater -import androidx.compose.runtime.currentComposer -import androidx.compose.runtime.remember -import org.jetbrains.compose.web.DomApplier -import org.jetbrains.compose.web.DomElementWrapper -import org.jetbrains.compose.web.attributes.AttrsBuilder -import kotlinx.browser.document -import org.w3c.dom.Audio -import org.w3c.dom.Element -import org.w3c.dom.HTMLAnchorElement -import org.w3c.dom.HTMLAreaElement -import org.w3c.dom.HTMLAudioElement -import org.w3c.dom.HTMLBRElement -import org.w3c.dom.HTMLButtonElement -import org.w3c.dom.HTMLDataListElement -import org.w3c.dom.HTMLDivElement -import org.w3c.dom.HTMLElement -import org.w3c.dom.HTMLEmbedElement -import org.w3c.dom.HTMLFieldSetElement -import org.w3c.dom.HTMLFormElement -import org.w3c.dom.HTMLHRElement -import org.w3c.dom.HTMLHeadingElement -import org.w3c.dom.HTMLIFrameElement -import org.w3c.dom.HTMLImageElement -import org.w3c.dom.HTMLInputElement -import org.w3c.dom.HTMLLIElement -import org.w3c.dom.HTMLLabelElement -import org.w3c.dom.HTMLLegendElement -import org.w3c.dom.HTMLMapElement -import org.w3c.dom.HTMLMeterElement -import org.w3c.dom.HTMLOListElement -import org.w3c.dom.HTMLObjectElement -import org.w3c.dom.HTMLOptGroupElement -import org.w3c.dom.HTMLOptionElement -import org.w3c.dom.HTMLOutputElement -import org.w3c.dom.HTMLParagraphElement -import org.w3c.dom.HTMLParamElement -import org.w3c.dom.HTMLPictureElement -import org.w3c.dom.HTMLPreElement -import org.w3c.dom.HTMLProgressElement -import org.w3c.dom.HTMLSelectElement -import org.w3c.dom.HTMLSourceElement -import org.w3c.dom.HTMLSpanElement -import org.w3c.dom.HTMLStyleElement -import org.w3c.dom.HTMLTableCaptionElement -import org.w3c.dom.HTMLTableCellElement -import org.w3c.dom.HTMLTableColElement -import org.w3c.dom.HTMLTableElement -import org.w3c.dom.HTMLTableRowElement -import org.w3c.dom.HTMLTableSectionElement -import org.w3c.dom.HTMLTextAreaElement -import org.w3c.dom.HTMLTrackElement -import org.w3c.dom.HTMLUListElement -import org.w3c.dom.HTMLVideoElement - -@OptIn(ComposeCompilerApi::class) -@Composable -@ExplicitGroupsComposable -inline fun > ComposeDomNode( - noinline factory: () -> T, - elementScope: TScope, - noinline attrsSkippableUpdate: @Composable SkippableUpdater.() -> Unit, - noinline content: (@Composable TScope.() -> Unit)? -) { - if (currentComposer.applier !is E) error("Invalid applier") - currentComposer.startNode() - if (currentComposer.inserting) { - currentComposer.createNode(factory) - } else { - currentComposer.useNode() - } - - SkippableUpdater(currentComposer).apply { - attrsSkippableUpdate() - } - - currentComposer.startReplaceableGroup(0x7ab4aae9) - content?.invoke(elementScope) - currentComposer.endReplaceableGroup() - currentComposer.endNode() -} - -class DisposableEffectHolder( - var effect: (DisposableEffectScope.(Element) -> DisposableEffectResult)? = null -) - -interface ElementBuilder { - fun create(): TElement - - private open class ElementBuilderImplementation(private val tagName: String) : ElementBuilder { - private val el: Element by lazy { document.createElement(tagName) } - override fun create(): TElement = el.cloneNode() as TElement - } - - companion object { - fun createBuilder(tagName: String): ElementBuilder { - return object : ElementBuilderImplementation(tagName) {} - } - - val Address: ElementBuilder = ElementBuilderImplementation("address") - val Article: ElementBuilder = ElementBuilderImplementation("article") - val Aside: ElementBuilder = ElementBuilderImplementation("aside") - val Header: ElementBuilder = ElementBuilderImplementation("header") - - val Area: ElementBuilder = ElementBuilderImplementation("area") - val Audio: ElementBuilder = ElementBuilderImplementation("audio") - val Map: ElementBuilder = ElementBuilderImplementation("map") - val Track: ElementBuilder = ElementBuilderImplementation("track") - val Video: ElementBuilder = ElementBuilderImplementation("video") - - val Datalist: ElementBuilder = ElementBuilderImplementation("datalist") - val Fieldset: ElementBuilder = ElementBuilderImplementation("fieldset") - val Legend: ElementBuilder = ElementBuilderImplementation("legend") - val Meter: ElementBuilder = ElementBuilderImplementation("meter") - val Output: ElementBuilder = ElementBuilderImplementation("output") - val Progress: ElementBuilder = ElementBuilderImplementation("progress") - - val Embed: ElementBuilder = ElementBuilderImplementation("embed") - val Iframe: ElementBuilder = ElementBuilderImplementation("iframe") - val Object: ElementBuilder = ElementBuilderImplementation("object") - val Param: ElementBuilder = ElementBuilderImplementation("param") - val Picture: ElementBuilder = ElementBuilderImplementation("picture") - val Source: ElementBuilder = ElementBuilderImplementation("source") - - val Div: ElementBuilder = ElementBuilderImplementation("div") - val A: ElementBuilder = ElementBuilderImplementation("a") - val Input: ElementBuilder = ElementBuilderImplementation("input") - val Button: ElementBuilder = ElementBuilderImplementation("button") - - val H1: ElementBuilder = ElementBuilderImplementation("h1") - val H2: ElementBuilder = ElementBuilderImplementation("h2") - val H3: ElementBuilder = ElementBuilderImplementation("h3") - val H4: ElementBuilder = ElementBuilderImplementation("h4") - val H5: ElementBuilder = ElementBuilderImplementation("h5") - val H6: ElementBuilder = ElementBuilderImplementation("h6") - - val P: ElementBuilder = ElementBuilderImplementation("p") - - val Em: ElementBuilder = ElementBuilderImplementation("em") - val I: ElementBuilder = ElementBuilderImplementation("i") - val B: ElementBuilder = ElementBuilderImplementation("b") - val Small: ElementBuilder = ElementBuilderImplementation("small") - - val Span: ElementBuilder = ElementBuilderImplementation("span") - - val Br: ElementBuilder = ElementBuilderImplementation("br") - - val Ul: ElementBuilder = ElementBuilderImplementation("ul") - val Ol: ElementBuilder = ElementBuilderImplementation("ol") - - val Li: ElementBuilder = ElementBuilderImplementation("li") - - val Img: ElementBuilder = ElementBuilderImplementation("img") - val Form: ElementBuilder = ElementBuilderImplementation("form") - - val Select: ElementBuilder = ElementBuilderImplementation("select") - val Option: ElementBuilder = ElementBuilderImplementation("option") - val OptGroup: ElementBuilder = ElementBuilderImplementation("optgroup") - - val Section: ElementBuilder = ElementBuilderImplementation("section") - val TextArea: ElementBuilder = ElementBuilderImplementation("textarea") - val Nav: ElementBuilder = ElementBuilderImplementation("nav") - val Pre: ElementBuilder = ElementBuilderImplementation("pre") - val Code: ElementBuilder = ElementBuilderImplementation("code") - - val Main: ElementBuilder = ElementBuilderImplementation("main") - val Footer: ElementBuilder = ElementBuilderImplementation("footer") - val Hr: ElementBuilder = ElementBuilderImplementation("hr") - val Label: ElementBuilder = ElementBuilderImplementation("label") - val Table: ElementBuilder = ElementBuilderImplementation("table") - val Caption: ElementBuilder = ElementBuilderImplementation("caption") - val Col: ElementBuilder = ElementBuilderImplementation("col") - val Colgroup: ElementBuilder = ElementBuilderImplementation("colgroup") - val Tr: ElementBuilder = ElementBuilderImplementation("tr") - val Thead: ElementBuilder = ElementBuilderImplementation("thead") - val Th: ElementBuilder = ElementBuilderImplementation("th") - val Td: ElementBuilder = ElementBuilderImplementation("td") - val Tbody: ElementBuilder = ElementBuilderImplementation("tbody") - val Tfoot: ElementBuilder = ElementBuilderImplementation("tfoot") - - val Style: ElementBuilder = ElementBuilderImplementation("style") - } -} - -@Composable -fun TagElement( - elementBuilder: ElementBuilder, - applyAttrs: (AttrsBuilder.() -> Unit)?, - content: (@Composable ElementScope.() -> Unit)? -) { - val scope = remember { ElementScopeImpl() } - val refEffect = remember { DisposableEffectHolder() } - - ComposeDomNode, DomElementWrapper, DomApplier>( - factory = { - DomElementWrapper(elementBuilder.create() as HTMLElement).also { - scope.element = it.node.unsafeCast() - } - }, - attrsSkippableUpdate = { - val attrsApplied = AttrsBuilder().also { - if (applyAttrs != null) { - it.applyAttrs() - } - } - refEffect.effect = attrsApplied.refEffect - val attrsCollected = attrsApplied.collect() - val events = attrsApplied.collectListeners() - - update { - set(attrsCollected, DomElementWrapper::updateAttrs) - set(events, DomElementWrapper::updateEventListeners) - set(attrsApplied.propertyUpdates, DomElementWrapper::updateProperties) - set(attrsApplied.styleBuilder, DomElementWrapper::updateStyleDeclarations) - } - }, - elementScope = scope, - content = content - ) - - DisposableEffect(null) { - refEffect.effect?.invoke(this, scope.element) ?: onDispose {} - } -} - -@Composable -fun TagElement( - tagName: String, - applyAttrs: AttrsBuilder.() -> Unit, - content: (@Composable ElementScope.() -> Unit)? -) = TagElement( - elementBuilder = ElementBuilder.createBuilder(tagName), - applyAttrs = applyAttrs, - content = content -) diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/elements/InputElements.kt b/web/core/src/jsMain/kotlin/androidx/compose/web/elements/InputElements.kt deleted file mode 100644 index ce0c7695e0..0000000000 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/elements/InputElements.kt +++ /dev/null @@ -1,192 +0,0 @@ -package org.jetbrains.compose.web.dom - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.NonRestartableComposable -import androidx.compose.web.attributes.InputAttrsBuilder -import org.jetbrains.compose.web.attributes.* - -private fun InputAttrsBuilder.applyAttrsWithStringValue( - value: String, - attrsBuilder: InputAttrsBuilder.() -> Unit -) { - if (value.isNotEmpty()) value(value) - attrsBuilder() -} - -@Composable -@NonRestartableComposable -fun CheckboxInput(checked: Boolean = false, attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { - Input( - type = InputType.Checkbox, - attrs = { - if (checked) checked() - this.attrsBuilder() - } - ) -} - -@Composable -@NonRestartableComposable -fun DateInput(value: String = "", attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { - Input(type = InputType.Date, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) -} - -@Composable -@NonRestartableComposable -fun DateTimeLocalInput(value: String = "", attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { - Input(type = InputType.DateTimeLocal, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) -} - -@Composable -@NonRestartableComposable -fun EmailInput(value: String = "", attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { - Input(type = InputType.Email, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) -} - -@Composable -@NonRestartableComposable -fun FileInput(value: String = "", attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { - Input(type = InputType.File, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) -} - -@Composable -@NonRestartableComposable -fun HiddenInput(attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { - Input(type = InputType.Hidden, attrs = attrsBuilder) -} - -@Composable -@NonRestartableComposable -fun MonthInput(value: String = "", attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { - Input(type = InputType.Month, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) -} - -@Composable -@NonRestartableComposable -fun NumberInput( - value: Number? = null, - min: Number? = null, - max: Number? = null, - attrsBuilder: InputAttrsBuilder.() -> Unit = {} -) { - Input( - type = InputType.Number, - attrs = { - if (value != null) value(value.toString()) - if (min != null) min(min.toString()) - if (max != null) max(max.toString()) - attrsBuilder() - } - ) -} - -@Composable -@NonRestartableComposable -fun PasswordInput(value: String = "", attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { - Input(type = InputType.Password, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) -} - -@Composable -@NonRestartableComposable -fun RadioInput(checked: Boolean = false, attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { - Input( - type = InputType.Radio, - attrs = { - if (checked) checked() - attrsBuilder() - } - ) -} - -@Composable -@NonRestartableComposable -fun RangeInput( - value: Number? = null, - min: Number? = null, - max: Number? = null, - step: Number = 1, - attrsBuilder: InputAttrsBuilder.() -> Unit = {} -) { - Input( - type = InputType.Range, - attrs = { - if (value != null) value(value.toString()) - if (min != null) min(min.toString()) - if (max != null) max(max.toString()) - step(step) - attrsBuilder() - } - ) -} - -@Composable -@NonRestartableComposable -fun SearchInput(value: String = "", attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { - Input(type = InputType.Search, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) -} - -@Composable -@NonRestartableComposable -fun SubmitInput(attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { - Input(type = InputType.Submit, attrs = attrsBuilder) -} - -@Composable -@NonRestartableComposable -fun TelInput(value: String = "", attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { - Input(type = InputType.Tel, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) -} - -@Composable -@NonRestartableComposable -fun TextInput(value: String = "", attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { - Input(type = InputType.Text, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) -} - -@Composable -@NonRestartableComposable -fun TimeInput(value: String = "", attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { - Input(type = InputType.Time, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) -} - -@Composable -@NonRestartableComposable -fun UrlInput(value: String = "", attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { - Input(type = InputType.Url, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) -} - -@Composable -@NonRestartableComposable -fun WeekInput(value: String = "", attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { - Input(type = InputType.Week, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) -} - -@Composable -fun Input( - type: InputType, - attrs: InputAttrsBuilder.() -> Unit -) { - TagElement( - elementBuilder = ElementBuilder.Input, - applyAttrs = { - val inputAttrsBuilder = InputAttrsBuilder(type) - inputAttrsBuilder.type(type) - inputAttrsBuilder.attrs() - this.copyFrom(inputAttrsBuilder) - }, - content = null - ) -} - -@Composable -fun Input(type: InputType) { - TagElement( - elementBuilder = ElementBuilder.Input, - applyAttrs = { - val inputAttrsBuilder = InputAttrsBuilder(type) - inputAttrsBuilder.type(type) - this.copyFrom(inputAttrsBuilder) - }, - content = null - ) -} diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/events/WrappedEvent.kt b/web/core/src/jsMain/kotlin/androidx/compose/web/events/WrappedEvent.kt deleted file mode 100644 index 4c967234ce..0000000000 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/events/WrappedEvent.kt +++ /dev/null @@ -1,107 +0,0 @@ -package org.jetbrains.compose.web.events - -import org.w3c.dom.DragEvent -import org.w3c.dom.TouchEvent -import org.w3c.dom.clipboard.ClipboardEvent -import org.w3c.dom.events.CompositionEvent -import org.w3c.dom.events.Event -import org.w3c.dom.events.FocusEvent -import org.w3c.dom.events.InputEvent -import org.w3c.dom.events.KeyboardEvent -import org.w3c.dom.events.MouseEvent -import org.w3c.dom.events.WheelEvent -import org.w3c.dom.pointerevents.PointerEvent - -interface GenericWrappedEvent { - val nativeEvent: T -} - -interface WrappedEvent : GenericWrappedEvent - -open class WrappedMouseEvent( - override val nativeEvent: MouseEvent -) : GenericWrappedEvent { - - // MouseEvent doesn't support movementX and movementY on IE6-11, and it's OK for now. - val movementX: Double - get() = nativeEvent.asDynamic().movementX as Double - val movementY: Double - get() = nativeEvent.asDynamic().movementY as Double -} - -open class WrappedWheelEvent( - override val nativeEvent: WheelEvent -) : GenericWrappedEvent - -open class WrappedInputEvent( - override val nativeEvent: Event -) : GenericWrappedEvent - -open class WrappedKeyboardEvent( - override val nativeEvent: KeyboardEvent -) : GenericWrappedEvent { - - fun getNormalizedKey(): String = nativeEvent.key.let { - normalizedKeys[it] ?: it - } - - companion object { - private val normalizedKeys = mapOf( - "Esc" to "Escape", - "Spacebar" to " ", - "Left" to "ArrowLeft", - "Up" to "ArrowUp", - "Right" to "ArrowRight", - "Down" to "ArrowDown", - "Del" to "Delete", - "Apps" to "ContextMenu", - "Menu" to "ContextMenu", - "Scroll" to "ScrollLock", - "MozPrintableKey" to "Unidentified", - ) - // Firefox bug for Windows key https://bugzilla.mozilla.org/show_bug.cgi?id=1232918 - } -} - -open class WrappedFocusEvent( - override val nativeEvent: FocusEvent -) : GenericWrappedEvent - -open class WrappedTouchEvent( - override val nativeEvent: TouchEvent -) : GenericWrappedEvent - -open class WrappedCompositionEvent( - override val nativeEvent: CompositionEvent -) : GenericWrappedEvent - -open class WrappedDragEvent( - override val nativeEvent: DragEvent -) : GenericWrappedEvent - -open class WrappedPointerEvent( - override val nativeEvent: PointerEvent -) : GenericWrappedEvent - -open class WrappedClipboardEvent( - override val nativeEvent: ClipboardEvent -) : GenericWrappedEvent - -class WrappedTextInputEvent( - nativeEvent: Event, - val inputValue: String -) : WrappedInputEvent(nativeEvent) - -class WrappedCheckBoxInputEvent( - override val nativeEvent: Event, - val checked: Boolean -) : GenericWrappedEvent - -class WrappedRadioInputEvent( - override val nativeEvent: Event, - val checked: Boolean -) : GenericWrappedEvent - -class WrappedEventImpl( - override val nativeEvent: Event -) : WrappedEvent diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/DomApplier.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/DomApplier.kt similarity index 88% rename from web/core/src/jsMain/kotlin/androidx/compose/web/DomApplier.kt rename to web/core/src/jsMain/kotlin/org/jetbrains/compose/web/DomApplier.kt index fc81be01b7..411477e74d 100644 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/DomApplier.kt +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/DomApplier.kt @@ -1,13 +1,11 @@ package org.jetbrains.compose.web import androidx.compose.runtime.AbstractApplier -import org.jetbrains.compose.web.attributes.WrappedEventListener +import org.jetbrains.compose.web.attributes.SyntheticEventListener import org.jetbrains.compose.web.css.StyleHolder import org.jetbrains.compose.web.dom.setProperty import org.jetbrains.compose.web.dom.setVariable import kotlinx.dom.clear -import org.jetbrains.compose.web.attributes.Options -import org.jetbrains.compose.web.css.jsObject import org.w3c.dom.Element import org.w3c.dom.HTMLElement import org.w3c.dom.Node @@ -46,9 +44,9 @@ external interface EventListenerOptions { } open class DomNodeWrapper(open val node: Node) { - private var currentListeners = emptyList>() + private var currentListeners = emptyList>() - fun updateEventListeners(list: List>) { + fun updateEventListeners(list: List>) { val htmlElement = node as? HTMLElement ?: return currentListeners.forEach { @@ -95,14 +93,17 @@ open class DomNodeWrapper(open val node: Node) { class DomElementWrapper(override val node: HTMLElement): DomNodeWrapper(node) { + private var currentAttrs: Map? = null + fun updateAttrs(attrs: Map) { - while (node.attributes.length > 0) { - node.removeAttributeNode(node.attributes[0]!!) + currentAttrs?.forEach { + node.removeAttribute(it.key) } attrs.forEach { node.setAttribute(it.key, it.value) } + currentAttrs = attrs } fun updateProperties(list: List Unit, Any>>) { diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/GlobalSnapshotManager.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/GlobalSnapshotManager.kt similarity index 100% rename from web/core/src/jsMain/kotlin/androidx/compose/web/GlobalSnapshotManager.kt rename to web/core/src/jsMain/kotlin/org/jetbrains/compose/web/GlobalSnapshotManager.kt diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/JsMicrotasksDispatcher.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/JsMicrotasksDispatcher.kt similarity index 100% rename from web/core/src/jsMain/kotlin/androidx/compose/web/JsMicrotasksDispatcher.kt rename to web/core/src/jsMain/kotlin/org/jetbrains/compose/web/JsMicrotasksDispatcher.kt diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/RenderComposable.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/RenderComposable.kt similarity index 100% rename from web/core/src/jsMain/kotlin/androidx/compose/web/RenderComposable.kt rename to web/core/src/jsMain/kotlin/org/jetbrains/compose/web/RenderComposable.kt diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/attributes/Attrs.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/Attrs.kt similarity index 89% rename from web/core/src/jsMain/kotlin/androidx/compose/web/attributes/Attrs.kt rename to web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/Attrs.kt index a8fdc106ab..38224a2a45 100644 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/attributes/Attrs.kt +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/Attrs.kt @@ -1,5 +1,6 @@ package org.jetbrains.compose.web.attributes +import org.jetbrains.compose.web.events.SyntheticSubmitEvent import org.w3c.dom.HTMLAnchorElement import org.w3c.dom.HTMLButtonElement import org.w3c.dom.HTMLFormElement @@ -77,8 +78,8 @@ fun AttrsBuilder.action(value: String) = fun AttrsBuilder.acceptCharset(value: String) = attr("accept-charset", value) -fun AttrsBuilder.autoComplete() = - attr("autocomplete", "") +fun AttrsBuilder.autoComplete(value: Boolean = true) = + attr("autocomplete", if(value) "on" else "off") fun AttrsBuilder.encType(value: FormEncType) = attr("enctype", value.typeStr) @@ -92,6 +93,20 @@ fun AttrsBuilder.noValidate() = fun AttrsBuilder.target(value: FormTarget) = attr("target", value.targetStr) +fun AttrsBuilder.onSubmit( + options: Options = Options.DEFAULT, + listener: (SyntheticSubmitEvent) -> Unit +) { + addEventListener(eventName = EventsListenerBuilder.SUBMIT, options = options, listener = listener) +} + +fun AttrsBuilder.onReset( + options: Options = Options.DEFAULT, + listener: (SyntheticSubmitEvent) -> Unit +) { + addEventListener(eventName = EventsListenerBuilder.RESET, options = options, listener = listener) +} + /* Input attributes */ fun AttrsBuilder.type(value: InputType<*>) = @@ -103,8 +118,8 @@ fun AttrsBuilder.accept(value: String) = fun AttrsBuilder.alt(value: String) = attr("alt", value) // type: image only -fun AttrsBuilder.autoComplete() = - attr("autocomplete", "") +fun AttrsBuilder.autoComplete(value: AutoComplete) = + attr("autocomplete", value.unsafeCast()) fun AttrsBuilder.autoFocus() = attr("autofocus", "") @@ -211,8 +226,8 @@ fun AttrsBuilder.label(value: String) = /* Select attributes */ -fun AttrsBuilder.autocomplete(value: String) = - attr("autocomplete", value) +fun AttrsBuilder.autoComplete(value: AutoComplete) = + attr("autocomplete", value.unsafeCast()) fun AttrsBuilder.autofocus() = attr("autofocus", "") @@ -245,8 +260,8 @@ fun AttrsBuilder.disabled() = /* TextArea attributes */ -fun AttrsBuilder.autoComplete(value: Boolean = true) = - attr("autocomplete", if (value) "on" else "off") +fun AttrsBuilder.autoComplete(value: AutoComplete) = + attr("autocomplete", value.unsafeCast()) fun AttrsBuilder.autoFocus() = attr("autofocus", "") diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/AttrsBuilder.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/AttrsBuilder.kt new file mode 100644 index 0000000000..fe2ae3470c --- /dev/null +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/AttrsBuilder.kt @@ -0,0 +1,136 @@ +package org.jetbrains.compose.web.attributes + +import androidx.compose.runtime.DisposableEffectResult +import androidx.compose.runtime.DisposableEffectScope +import org.jetbrains.compose.web.css.StyleBuilder +import org.jetbrains.compose.web.css.StyleBuilderImpl +import org.w3c.dom.Element +import org.w3c.dom.HTMLElement + +/** + * [AttrsBuilder] is a class that is used (as a builder context, that is as AttrsBuilder.() -> Unit) + * in all DOM-element creating API calls. It's used for adding attributes to the element created, + * adding inline style values (via [style]) and attaching events to the element (since AttrsBuilder + * is an [EventsListenerBuilder]) + * + * In that aspect the most important method is [attr]. Setting the most frequently attributes, like [id], [tabIndex] + * are extracted to a separate methods. + * + */ +open class AttrsBuilder : EventsListenerBuilder() { + internal val attributesMap = mutableMapOf() + val styleBuilder = StyleBuilderImpl() + + val propertyUpdates = mutableListOf Unit, Any>>() + var refEffect: (DisposableEffectScope.(TElement) -> DisposableEffectResult)? = null + + /** + * [style] add inline CSS-style properties to the element via [StyleBuilder] context + * + * Example: + * ``` + * Div({ + * style { maxWidth(5.px) } + * }) + * ``` + */ + fun style(builder: StyleBuilder.() -> Unit) { + styleBuilder.apply(builder) + } + + /** + * [classes] adds all values passed as params to the element's classList. + * This method acts cumulatively, that is, each call adds values to the classList. + * In the ideology of Composable functions and their recomposition one just don't need to remove classes, + * since if your classList is, for instance, condition-dependent, you can always just call this method conditionally. + */ + fun classes(vararg classes: String) = prop(setClassList, classes) + + fun id(value: String) = attr(ID, value) + fun hidden() = attr(HIDDEN, true.toString()) + fun title(value: String) = attr(TITLE, value) + fun dir(value: DirType) = attr(DIR, value.dirStr) + fun draggable(value: Draggable) = attr(DRAGGABLE, value.str) + fun contentEditable(value: Boolean) = attr(CONTENT_EDITABLE, value.toString()) + fun lang(value: String) = attr(LANG, value) + fun tabIndex(value: Int) = attr(TAB_INDEX, value.toString()) + fun spellCheck(value: Boolean) = attr(SPELLCHECK, value.toString()) + + /** + * [ref] can be used to retrieve a reference to a html element. + * The lambda that `ref` takes in is not Composable. It will be called only once when an element added into a composition. + * Likewise, the lambda passed in `onDispose` will be called only once when an element leaves the composition. + * + * Under the hood, `ref` uses [DisposableEffect](https://developer.android.com/jetpack/compose/side-effects#disposableeffect) + */ + fun ref(effect: DisposableEffectScope.(TElement) -> DisposableEffectResult) { + this.refEffect = effect + } + + /** + * [attr] adds arbitrary attribute to the Element. + * If it called twice for the same attribute name, attribute value will be resolved to the last call. + * + * @param attr - the name of the attribute + * @param value - the value of the attribute + * + * For boolean attributes cast boolean value to String and pass it as value. + */ + fun attr(attr: String, value: String): AttrsBuilder { + attributesMap[attr] = value + return this + } + + /** + * [prop] allows setting values of element's properties which can't be set by ussing [attr]. + * [update] is a lambda with two parameters: `element` and `value`. `element` is a reference to a native element. + * Some examples of properties that can set using [prop]: `value`, `checked`, `innerText`. + * + * Unlike [ref], lambda passed to [prop] will be invoked every time when AttrsBuilder being called during recomposition. + * [prop] is not supposed to be used for adding listeners, subscriptions, etc. + * Also see [ref]. + * + * Code Example: + * ``` + * Input(type = InputType.Text, attrs = { + * // This is only an example. One doesn't need to set `value` like this, since [Input] has `value(v: String)` + * prop({ element: HTMLInputElement, value: String -> element.value = value }, "someTextInputValue") + * }) + * ``` + */ + @Suppress("UNCHECKED_CAST") + fun prop(update: (E, V) -> Unit, value: V) { + propertyUpdates.add((update to value) as Pair<(Element, Any) -> Unit, Any>) + } + + fun collect(): Map { + return attributesMap + } + + internal fun copyFrom(attrsBuilder: AttrsBuilder) { + refEffect = attrsBuilder.refEffect + styleBuilder.copyFrom(attrsBuilder.styleBuilder) + + attributesMap.putAll(attrsBuilder.attributesMap) + propertyUpdates.addAll(attrsBuilder.propertyUpdates) + + copyListenersFrom(attrsBuilder) + } + + companion object { + const val CLASS = "class" + const val ID = "id" + const val HIDDEN = "hidden" + const val TITLE = "title" + const val DIR = "dir" + const val DRAGGABLE = "draggable" + const val CONTENT_EDITABLE = "contenteditable" + const val LANG = "lang" + const val TAB_INDEX = "tabindex" + const val SPELLCHECK = "spellcheck" + } +} + +val setClassList: (HTMLElement, Array) -> Unit = { e, classList -> + e.classList.add(*classList) +} diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/attributes/EventsListenerBuilder.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/EventsListenerBuilder.kt similarity index 59% rename from web/core/src/jsMain/kotlin/androidx/compose/web/attributes/EventsListenerBuilder.kt rename to web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/EventsListenerBuilder.kt index 31ac0119d2..a0ddecdf6c 100644 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/attributes/EventsListenerBuilder.kt +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/EventsListenerBuilder.kt @@ -1,198 +1,215 @@ package org.jetbrains.compose.web.attributes -import org.jetbrains.compose.web.events.WrappedCheckBoxInputEvent -import org.jetbrains.compose.web.events.WrappedClipboardEvent -import org.jetbrains.compose.web.events.WrappedDragEvent -import org.jetbrains.compose.web.events.WrappedEvent -import org.jetbrains.compose.web.events.WrappedFocusEvent -import org.jetbrains.compose.web.events.WrappedInputEvent -import org.jetbrains.compose.web.events.WrappedKeyboardEvent -import org.jetbrains.compose.web.events.WrappedMouseEvent -import org.jetbrains.compose.web.events.WrappedRadioInputEvent -import org.jetbrains.compose.web.events.WrappedTextInputEvent -import org.jetbrains.compose.web.events.WrappedTouchEvent -import org.jetbrains.compose.web.events.WrappedWheelEvent -import org.jetbrains.compose.web.events.GenericWrappedEvent - +import androidx.compose.web.events.SyntheticDragEvent +import androidx.compose.web.events.SyntheticEvent +import androidx.compose.web.events.SyntheticMouseEvent +import androidx.compose.web.events.SyntheticWheelEvent +import org.jetbrains.compose.web.events.* +import org.w3c.dom.HTMLFormElement +import org.w3c.dom.events.EventTarget + +private typealias SyntheticMouseEventListener = (SyntheticMouseEvent) -> Unit +private typealias SyntheticWheelEventListener = (SyntheticWheelEvent) -> Unit +private typealias SyntheticDragEventListener = (SyntheticDragEvent) -> Unit + +/** + * [EventsListenerBuilder] is used most often not directly but via [AttrsBuilder]. + * Its purpose is to add events to the element. For all most frequently used events there + * exist dedicated method. In case you need to support event that doesn't have such method, + * use [addEventListener] + */ open class EventsListenerBuilder { - protected val listeners = mutableListOf>() - - fun onCopy(options: Options = Options.DEFAULT, listener: (WrappedClipboardEvent) -> Unit) { - listeners.add(ClipboardEventListener(COPY, options, listener)) - } + protected val listeners = mutableListOf>() - fun onCut(options: Options = Options.DEFAULT, listener: (WrappedClipboardEvent) -> Unit) { - listeners.add(ClipboardEventListener(CUT, options, listener)) - } + /* Mouse Events */ - fun onPaste(options: Options = Options.DEFAULT, listener: (WrappedClipboardEvent) -> Unit) { - listeners.add(ClipboardEventListener(PASTE, options, listener)) - } - - fun onContextMenu(options: Options = Options.DEFAULT, listener: (WrappedMouseEvent) -> Unit) { + fun onContextMenu(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) { listeners.add(MouseEventListener(CONTEXTMENU, options, listener)) } - fun onClick(options: Options = Options.DEFAULT, listener: (WrappedMouseEvent) -> Unit) { + fun onClick(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) { listeners.add(MouseEventListener(CLICK, options, listener)) } - fun onDoubleClick(options: Options = Options.DEFAULT, listener: (WrappedMouseEvent) -> Unit) { + fun onDoubleClick(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) { listeners.add(MouseEventListener(DBLCLICK, options, listener)) } - fun onGenericInput( - options: Options = Options.DEFAULT, - listener: (GenericWrappedEvent<*>) -> Unit - ) { - listeners.add(WrappedEventListener(INPUT, options, listener)) + fun onMouseDown(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) { + listeners.add(MouseEventListener(MOUSEDOWN, options, listener)) } - fun onChange(options: Options = Options.DEFAULT, listener: (WrappedEvent) -> Unit) { - listeners.add(WrappedEventListener(CHANGE, options, listener)) + fun onMouseUp(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) { + listeners.add(MouseEventListener(MOUSEUP, options, listener)) } - fun onInvalid(options: Options = Options.DEFAULT, listener: (WrappedEvent) -> Unit) { - listeners.add(WrappedEventListener(INVALID, options, listener)) + fun onMouseEnter(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) { + listeners.add(MouseEventListener(MOUSEENTER, options, listener)) } - fun onSearch(options: Options = Options.DEFAULT, listener: (WrappedEvent) -> Unit) { - listeners.add(WrappedEventListener(SEARCH, options, listener)) + fun onMouseLeave(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) { + listeners.add(MouseEventListener(MOUSELEAVE, options, listener)) } - fun onFocus(options: Options = Options.DEFAULT, listener: (WrappedFocusEvent) -> Unit) { - listeners.add(FocusEventListener(FOCUS, options, listener)) + fun onMouseMove(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) { + listeners.add(MouseEventListener(MOUSEMOVE, options, listener)) } - fun onBlur(options: Options = Options.DEFAULT, listener: (WrappedFocusEvent) -> Unit) { - listeners.add(FocusEventListener(BLUR, options, listener)) + fun onMouseOut(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) { + listeners.add(MouseEventListener(MOUSEOUT, options, listener)) } - fun onFocusIn(options: Options = Options.DEFAULT, listener: (WrappedFocusEvent) -> Unit) { - listeners.add(FocusEventListener(FOCUSIN, options, listener)) + fun onMouseOver(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) { + listeners.add(MouseEventListener(MOUSEOVER, options, listener)) } - fun onFocusOut(options: Options = Options.DEFAULT, listener: (WrappedFocusEvent) -> Unit) { - listeners.add(FocusEventListener(FOCUSOUT, options, listener)) + fun onWheel(options: Options = Options.DEFAULT, listener: SyntheticWheelEventListener) { + listeners.add(MouseWheelEventListener(WHEEL, options, listener)) } - fun onKeyDown(options: Options = Options.DEFAULT, listener: (WrappedKeyboardEvent) -> Unit) { - listeners.add(KeyboardEventListener(KEYDOWN, options, listener)) - } + /* Drag Events */ - fun onKeyUp(options: Options = Options.DEFAULT, listener: (WrappedKeyboardEvent) -> Unit) { - listeners.add(KeyboardEventListener(KEYUP, options, listener)) + fun onDrag(options: Options = Options.DEFAULT, listener: SyntheticDragEventListener) { + listeners.add(DragEventListener(DRAG, options, listener)) } - fun onMouseDown(options: Options = Options.DEFAULT, listener: (WrappedMouseEvent) -> Unit) { - listeners.add(MouseEventListener(MOUSEDOWN, options, listener)) + fun onDrop(options: Options = Options.DEFAULT, listener: SyntheticDragEventListener) { + listeners.add(DragEventListener(DROP, options, listener)) } - fun onMouseUp(options: Options = Options.DEFAULT, listener: (WrappedMouseEvent) -> Unit) { - listeners.add(MouseEventListener(MOUSEUP, options, listener)) + fun onDragStart(options: Options = Options.DEFAULT, listener: SyntheticDragEventListener) { + listeners.add(DragEventListener(DRAGSTART, options, listener)) } - fun onMouseEnter(options: Options = Options.DEFAULT, listener: (WrappedMouseEvent) -> Unit) { - listeners.add(MouseEventListener(MOUSEENTER, options, listener)) + fun onDragEnd(options: Options = Options.DEFAULT, listener: SyntheticDragEventListener) { + listeners.add(DragEventListener(DRAGEND, options, listener)) } - fun onMouseLeave(options: Options = Options.DEFAULT, listener: (WrappedMouseEvent) -> Unit) { - listeners.add(MouseEventListener(MOUSELEAVE, options, listener)) + fun onDragOver(options: Options = Options.DEFAULT, listener: SyntheticDragEventListener) { + listeners.add(DragEventListener(DRAGOVER, options, listener)) } - fun onMouseMove(options: Options = Options.DEFAULT, listener: (WrappedMouseEvent) -> Unit) { - listeners.add(MouseEventListener(MOUSEMOVE, options, listener)) + fun onDragEnter(options: Options = Options.DEFAULT, listener: SyntheticDragEventListener) { + listeners.add(DragEventListener(DRAGENTER, options, listener)) } - fun onMouseOut(options: Options = Options.DEFAULT, listener: (WrappedMouseEvent) -> Unit) { - listeners.add(MouseEventListener(MOUSEOUT, options, listener)) + fun onDragLeave(options: Options = Options.DEFAULT, listener: SyntheticDragEventListener) { + listeners.add(DragEventListener(DRAGLEAVE, options, listener)) } - fun onMouseOver(options: Options = Options.DEFAULT, listener: (WrappedMouseEvent) -> Unit) { - listeners.add(MouseEventListener(MOUSEOVER, options, listener)) - } + /* End of Drag Events */ - fun onWheel(options: Options = Options.DEFAULT, listener: (WrappedWheelEvent) -> Unit) { - listeners.add(MouseWheelEventListener(WHEEL, options, listener)) - } + /* Clipboard Events */ - fun onScroll(options: Options = Options.DEFAULT, listener: (WrappedEvent) -> Unit) { - listeners.add(WrappedEventListener(SCROLL, options, listener)) + fun onCopy(options: Options = Options.DEFAULT, listener: (SyntheticClipboardEvent) -> Unit) { + listeners.add(ClipboardEventListener(COPY, options, listener)) } - fun onSelect(options: Options = Options.DEFAULT, listener: (WrappedEvent) -> Unit) { - listeners.add(WrappedEventListener(SELECT, options, listener)) + fun onCut(options: Options = Options.DEFAULT, listener: (SyntheticClipboardEvent) -> Unit) { + listeners.add(ClipboardEventListener(CUT, options, listener)) } - fun onTouchCancel(options: Options = Options.DEFAULT, listener: (WrappedTouchEvent) -> Unit) { - listeners.add(TouchEventListener(TOUCHCANCEL, options, listener)) + fun onPaste(options: Options = Options.DEFAULT, listener: (SyntheticClipboardEvent) -> Unit) { + listeners.add(ClipboardEventListener(PASTE, options, listener)) } - fun onTouchMove(options: Options = Options.DEFAULT, listener: (WrappedTouchEvent) -> Unit) { - listeners.add(TouchEventListener(TOUCHMOVE, options, listener)) + /* End of Clipboard Events */ + + /* Keyboard Events */ + + fun onKeyDown(options: Options = Options.DEFAULT, listener: (SyntheticKeyboardEvent) -> Unit) { + listeners.add(KeyboardEventListener(KEYDOWN, options, listener)) } - fun onTouchEnd(options: Options = Options.DEFAULT, listener: (WrappedTouchEvent) -> Unit) { - listeners.add(TouchEventListener(TOUCHEND, options, listener)) + fun onKeyUp(options: Options = Options.DEFAULT, listener: (SyntheticKeyboardEvent) -> Unit) { + listeners.add(KeyboardEventListener(KEYUP, options, listener)) } - fun onTouchStart(options: Options = Options.DEFAULT, listener: (WrappedTouchEvent) -> Unit) { - listeners.add(TouchEventListener(TOUCHSTART, options, listener)) + /* End of Keyboard Events */ + + /* Focus Events */ + + fun onFocus(options: Options = Options.DEFAULT, listener: (SyntheticFocusEvent) -> Unit) { + listeners.add(FocusEventListener(FOCUS, options, listener)) } - fun onAnimationEnd(options: Options = Options.DEFAULT, listener: (WrappedTouchEvent) -> Unit) { - listeners.add(WrappedEventListener(ANIMATIONEND, options, listener)) + fun onBlur(options: Options = Options.DEFAULT, listener: (SyntheticFocusEvent) -> Unit) { + listeners.add(FocusEventListener(BLUR, options, listener)) } - fun onAnimationIteration(options: Options = Options.DEFAULT, listener: (WrappedEvent) -> Unit) { - listeners.add(WrappedEventListener(ANIMATIONITERATION, options, listener)) + fun onFocusIn(options: Options = Options.DEFAULT, listener: (SyntheticFocusEvent) -> Unit) { + listeners.add(FocusEventListener(FOCUSIN, options, listener)) } - fun onAnimationStart(options: Options = Options.DEFAULT, listener: (WrappedEvent) -> Unit) { - listeners.add(WrappedEventListener(ANIMATIONSTART, options, listener)) + fun onFocusOut(options: Options = Options.DEFAULT, listener: (SyntheticFocusEvent) -> Unit) { + listeners.add(FocusEventListener(FOCUSOUT, options, listener)) } - fun onBeforeInput(options: Options = Options.DEFAULT, listener: (WrappedInputEvent) -> Unit) { - listeners.add(InputEventListener(BEFOREINPUT, options, listener)) + /* End of Focus Events */ + + /* Touch Events */ + + fun onTouchCancel(options: Options = Options.DEFAULT, listener: (SyntheticTouchEvent) -> Unit) { + listeners.add(TouchEventListener(TOUCHCANCEL, options, listener)) } - fun onDrag(options: Options = Options.DEFAULT, listener: (WrappedDragEvent) -> Unit) { - listeners.add(DragEventListener(DRAG, options, listener)) + fun onTouchMove(options: Options = Options.DEFAULT, listener: (SyntheticTouchEvent) -> Unit) { + listeners.add(TouchEventListener(TOUCHMOVE, options, listener)) } - fun onDrop(options: Options = Options.DEFAULT, listener: (WrappedDragEvent) -> Unit) { - listeners.add(DragEventListener(DROP, options, listener)) + fun onTouchEnd(options: Options = Options.DEFAULT, listener: (SyntheticTouchEvent) -> Unit) { + listeners.add(TouchEventListener(TOUCHEND, options, listener)) } - fun onDragStart(options: Options = Options.DEFAULT, listener: (WrappedDragEvent) -> Unit) { - listeners.add(DragEventListener(DRAGSTART, options, listener)) + fun onTouchStart(options: Options = Options.DEFAULT, listener: (SyntheticTouchEvent) -> Unit) { + listeners.add(TouchEventListener(TOUCHSTART, options, listener)) } - fun onDragEnd(options: Options = Options.DEFAULT, listener: (WrappedDragEvent) -> Unit) { - listeners.add(DragEventListener(DRAGEND, options, listener)) + /* End of Touch Events */ + + /* Animation Events */ + + fun onAnimationEnd(options: Options = Options.DEFAULT, listener: (SyntheticAnimationEvent) -> Unit) { + listeners.add(AnimationEventListener(ANIMATIONEND, options, listener)) } - fun onDragOver(options: Options = Options.DEFAULT, listener: (WrappedDragEvent) -> Unit) { - listeners.add(DragEventListener(DRAGOVER, options, listener)) + fun onAnimationIteration(options: Options = Options.DEFAULT, listener: (SyntheticAnimationEvent) -> Unit) { + listeners.add(AnimationEventListener(ANIMATIONITERATION, options, listener)) } - fun onDragEnter(options: Options = Options.DEFAULT, listener: (WrappedDragEvent) -> Unit) { - listeners.add(DragEventListener(DRAGENTER, options, listener)) + fun onAnimationStart(options: Options = Options.DEFAULT, listener: (SyntheticAnimationEvent) -> Unit) { + listeners.add(AnimationEventListener(ANIMATIONSTART, options, listener)) } - fun onDragLeave(options: Options = Options.DEFAULT, listener: (WrappedDragEvent) -> Unit) { - listeners.add(DragEventListener(DRAGLEAVE, options, listener)) + /* End of Animation Events */ + + fun onScroll(options: Options = Options.DEFAULT, listener: (SyntheticEvent) -> Unit) { + listeners.add(SyntheticEventListener(SCROLL, options, listener)) } - fun collectListeners(): List> = listeners + fun collectListeners(): List> = listeners + + /** + * [addEventListener] used for adding arbitrary events to the element. It resembles the standard DOM addEventListener method + * @param eventName - the name of the event + * @param options - as of now this param is always equal to Options.DEFAULT + * @listener - event handler + */ + fun > addEventListener( + eventName: String, + options: Options = Options.DEFAULT, + listener: (T) -> Unit + ) { + listeners.add(SyntheticEventListener(eventName, options, listener)) + } fun addEventListener( eventName: String, options: Options = Options.DEFAULT, - listener: (WrappedEvent) -> Unit + listener: (SyntheticEvent) -> Unit ) { - listeners.add(WrappedEventListener(eventName, options, listener)) + listeners.add(SyntheticEventListener(eventName, options, listener)) } internal fun copyListenersFrom(from: EventsListenerBuilder) { @@ -239,7 +256,6 @@ open class EventsListenerBuilder { const val INPUT = "input" const val CHANGE = "change" const val INVALID = "invalid" - const val SEARCH = "search" const val DRAG = "drag" const val DROP = "drop" @@ -248,5 +264,8 @@ open class EventsListenerBuilder { const val DRAGOVER = "dragover" const val DRAGENTER = "dragenter" const val DRAGLEAVE = "dragleave" + + const val SUBMIT = "submit" + const val RESET = "reset" } } diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/PredefinedAttrValues.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/PredefinedAttrValues.kt new file mode 100644 index 0000000000..f8baddf11a --- /dev/null +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/PredefinedAttrValues.kt @@ -0,0 +1,439 @@ +package org.jetbrains.compose.web.attributes + +import org.w3c.dom.events.Event + +sealed class InputType(val typeStr: String) { + + object Button : InputTypeWithUnitValue("button") + object Checkbox : InputTypeCheckedValue("checkbox") + object Color : InputTypeWithStringValue("color") + object Date : InputTypeWithStringValue("date") + object DateTimeLocal : InputTypeWithStringValue("datetime-local") + object Email : InputTypeWithStringValue("email") + object File : InputTypeWithStringValue("file") + object Hidden : InputTypeWithStringValue("hidden") + object Month : InputTypeWithStringValue("month") + object Number : InputTypeNumberValue("number") + object Password : InputTypeWithStringValue("password") + object Radio : InputTypeCheckedValue("radio") + object Range : InputTypeNumberValue("range") + object Search : InputTypeWithStringValue("search") + object Submit : InputTypeWithUnitValue("submit") + object Tel : InputTypeWithStringValue("tel") + object Text : InputTypeWithStringValue("text") + object Time : InputTypeWithStringValue("time") + object Url : InputTypeWithStringValue("url") + object Week : InputTypeWithStringValue("week") + + open class InputTypeWithStringValue(name: String) : InputType(name) { + override fun inputValue(event: Event) = Week.valueAsString(event) + } + + open class InputTypeWithUnitValue(name: String) : InputType(name) { + override fun inputValue(event: Event) = Unit + } + + open class InputTypeCheckedValue(name: String) : InputType(name) { + override fun inputValue(event: Event): Boolean { + return event.target?.asDynamic()?.checked?.unsafeCast() ?: false + } + } + + open class InputTypeNumberValue(name: String) : InputType(name) { + override fun inputValue(event: Event): kotlin.Number? { + return event.target?.asDynamic()?.valueAsNumber ?: null + } + } + + abstract fun inputValue(event: Event): T + + protected fun valueAsString(event: Event): String { + return event.target?.asDynamic()?.value?.unsafeCast() ?: "" + } +} + +sealed class DirType(val dirStr: String) { + object Ltr : DirType("ltr") + object Rtl : DirType("rtl") + object Auto : DirType("auto") +} + +sealed class ATarget(val targetStr: String) { + object Blank : ATarget("_blank") + object Parent : ATarget("_parent") + object Self : ATarget("_self") + object Top : ATarget("_top") +} + +sealed class ARel(val relStr: String) { + object Alternate : ARel("alternate") + object Author : ARel("author") + object Bookmark : ARel("bookmark") + object External : ARel("external") + object Help : ARel("help") + object License : ARel("license") + object Next : ARel("next") + object First : ARel("first") + object Prev : ARel("prev") + object Last : ARel("last") + object NoFollow : ARel("nofollow") + object NoOpener : ARel("noopener") + object NoReferrer : ARel("noreferrer") + object Opener : ARel("opener") + object Search : ARel("search") + object Tag : ARel("tag") + + class CustomARel(value: String) : ARel(value) +} + +enum class Draggable(val str: String) { + True("true"), False("false"), Auto("auto"); +} + +enum class ButtonType(val str: String) { + Button("button"), Reset("reset"), Submit("submit") +} + +sealed class ButtonFormTarget(val targetStr: String) { + object Blank : ButtonFormTarget("_blank") + object Parent : ButtonFormTarget("_parent") + object Self : ButtonFormTarget("_self") + object Top : ButtonFormTarget("_top") +} + +enum class ButtonFormMethod(val methodStr: String) { + Get("get"), Post("post") +} + +enum class ButtonFormEncType(val typeStr: String) { + MultipartFormData("multipart/form-data"), + ApplicationXWwwFormUrlEncoded("application/x-www-form-urlencoded"), + TextPlain("text/plain") +} + +enum class FormEncType(val typeStr: String) { + MultipartFormData("multipart/form-data"), + ApplicationXWwwFormUrlEncoded("application/x-www-form-urlencoded"), + TextPlain("text/plain") +} + +enum class FormMethod(val methodStr: String) { + Get("get"), + Post("post"), + Dialog("dialog") +} + +sealed class FormTarget(val targetStr: String) { + object Blank : FormTarget("_blank") + object Parent : FormTarget("_parent") + object Self : FormTarget("_self") + object Top : FormTarget("_top") +} + +enum class InputFormEncType(val typeStr: String) { + MultipartFormData("multipart/form-data"), + ApplicationXWwwFormUrlEncoded("application/x-www-form-urlencoded"), + TextPlain("text/plain") +} + +enum class InputFormMethod(val methodStr: String) { + Get("get"), + Post("post"), + Dialog("dialog") +} + +sealed class InputFormTarget(val targetStr: String) { + object Blank : InputFormTarget("_blank") + object Parent : InputFormTarget("_parent") + object Self : InputFormTarget("_self") + object Top : InputFormTarget("_top") +} + +enum class TextAreaWrap(val str: String) { + Hard("hard"), + Soft("soft"), + Off("off") +} + +enum class Scope(val str: String) { + Row("row"), + Rowgroup("rowgroup"), + Col("col"), + Colgroup("colgroup") +} + + +/** + * https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete + */ +@Suppress("Unused", "NOTHING_TO_INLINE", "NESTED_CLASS_IN_EXTERNAL_INTERFACE", "INLINE_EXTERNAL_DECLARATION", "WRONG_BODY_OF_EXTERNAL_DECLARATION", "NESTED_EXTERNAL_DECLARATION", "ClassName") +interface AutoComplete { + companion object { + /** + * The browser is not permitted to automatically enter or select a value for this field. It is possible that the document or application provides its own autocomplete feature, or that security concerns require that the field's value not be automatically entered. + * Note: In most modern browsers, setting autocomplete to "off" will not prevent a password manager from asking the user if they would like to save username and password information, or from automatically filling in those values in a site's login form. See the autocomplete attribute and login fields. + */ + inline val off get() = AutoComplete("off") + + /** + * The browser is allowed to automatically complete the input. No guidance is provided as to the type of data expected in the field, so the browser may use its own judgement. + */ + inline val on get() = AutoComplete("on") + + /** + *The field expects the value to be a person's full name. Using "name" rather than breaking the name down into its components is generally preferred because it avoids dealing with the wide diversity of human names and how they are structured; however, you can use the following autocomplete values if you do need to break the name down into its components: + */ + inline val name get() = AutoComplete("name") + + /** + * The prefix or title, such as "Mrs.", "Mr.", "Miss", "Ms.", "Dr.", or "Mlle.". + */ + inline val honorificPrefix get() = AutoComplete("honorific-prefix") + + /** + * The given (or "first") name. + */ + inline val givenName get() = AutoComplete("given-name") + + /** + * The middle name. + */ + inline val additionalName get() = AutoComplete("additional-name") + + /** + * The family (or "last") name. + */ + inline val familyName get() = AutoComplete("family-name") + + /** + * The suffix, such as "Jr.", "B.Sc.", "PhD.", "MBASW", or "IV". + */ + inline val honorificSuffix get() = AutoComplete("honorific-suffix") + + /** + * A nickname or handle. + */ + inline val nickname get() = AutoComplete("nickname") + + /** + * An email address. + */ + inline val email get() = AutoComplete("email") + + /** + * A username or account name. + */ + inline val username get() = AutoComplete("username") + + /** + * A new password. When creating a new account or changing passwords, this should be used for an "Enter your new password" or "Confirm new password" field, as opposed to a general "Enter your current password" field that might be present. This may be used by the browser both to avoid accidentally filling in an existing password and to offer assistance in creating a secure password (see also Preventing autofilling with autocomplete="new-password"). + */ + inline val newPassword get() = AutoComplete("new-password") + + /** + * The user's current password. + */ + inline val currentPassword get() = AutoComplete("current-password") + + /** + * A one-time code used for verifying user identity. + */ + inline val oneTimeCode get() = AutoComplete("one-time-code") + + /** + * A job title, or the title a person has within an organization, such as "Senior Technical Writer", "President", or "Assistant Troop Leader". + */ + inline val organizationTitle get() = AutoComplete("organization-title") + + /** + * A company or organization name, such as "Acme Widget Company" or "Girl Scouts of America". + */ + inline val organization get() = AutoComplete("organization") + + /** + * A street address. This can be multiple lines of text, and should fully identify the location of the address within its second administrative level (typically a city or town), but should not include the city name, ZIP or postal code, or country name. + */ + inline val streetAddress get() = AutoComplete("street-address") + + /** + * Each individual line of the street address. These should only be present if the "street-address" is not present. + */ + inline val addressLine1 get() = AutoComplete("address-line1") + inline val addressLine2 get() = AutoComplete("address-line2") + inline val addressLine3 get() = AutoComplete("address-line3") + + /** + * The first administrative level in the address. This is typically the province in which the address is located. In the United States, this would be the state. In Switzerland, the canton. In the United Kingdom, the post town. + */ + inline val addressLevel1 get() = AutoComplete("address-level1") + + /** + * The second administrative level, in addresses with at least two of them. In countries with two administrative levels, this would typically be the city, town, village, or other locality in which the address is located. + */ + inline val addressLevel2 get() = AutoComplete("address-level2") + + /** + * The third administrative level, in addresses with at least three administrative levels. + */ + inline val addressLevel3 get() = AutoComplete("address-level3") + + /** + * The finest-grained administrative level, in addresses which have four levels. + */ + inline val addressLevel4 get() = AutoComplete("address-level4") + + /** + * A country or territory code. + */ + inline val country get() = AutoComplete("country") + + /** + * A country or territory name. + */ + inline val countryName get() = AutoComplete("country-name") + + /** + * A postal code (in the United States, this is the ZIP code). + */ + inline val postalCode get() = AutoComplete("postal-code") + + /** + * The full name as printed on or associated with a payment instrument such as a credit card. Using a full name field is preferred, typically, over breaking the name into pieces. + */ + inline val ccName get() = AutoComplete("cc-name") + + /** + * A given (first) name as given on a payment instrument like a credit card. + */ + inline val ccGivenName get() = AutoComplete("cc-given-name") + + /** + * A middle name as given on a payment instrument or credit card. + */ + inline val ccAdditionalName get() = AutoComplete("cc-additional-name") + + /** + * A family name, as given on a credit card. + */ + inline val ccFamilyName get() = AutoComplete("cc-family-name") + + /** + * A credit card number or other number identifying a payment method, such as an account number. + */ + inline val ccNumber get() = AutoComplete("cc-number") + + /** + * A payment method expiration date, typically in the form "MM/YY" or "MM/YYYY". + */ + inline val ccExp get() = AutoComplete("cc-exp") + + /** + * The month in which the payment method expires. + */ + inline val ccExpMonth get() = AutoComplete("cc-exp-month") + + /** + * The year in which the payment method expires. + */ + inline val ccExpYear get() = AutoComplete("cc-exp-year") + + /** + * The security code for the payment instrument; on credit cards, this is the 3-digit verification number on the back of the card. + */ + inline val ccSecurityCode get() = AutoComplete("cc-csc") + + /** + * The type of payment instrument (such as "Visa" or "Master Card"). + */ + inline val ccType get() = AutoComplete("cc-type") + + /** + * The currency in which the transaction is to take place. + */ + inline val transactionCurrency get() = AutoComplete("transaction-currency") + + /** + * The amount, given in the currency specified by "transaction-currency", of the transaction, for a payment form. + */ + inline val transactionAmount get() = AutoComplete("transaction-amount") + + /** + * A preferred language, given as a valid BCP 47 language tag. + */ + inline val language get() = AutoComplete("language") + + /** + * A birth date, as a full date. + */ + inline val birthdate get() = AutoComplete("bday") + + /** + * The day of the month of a birth date. + */ + inline val birthdateDay get() = AutoComplete("bday-day") + + /** + * The month of the year of a birth date. + */ + inline val birthdateMonth get() = AutoComplete("bday-month") + + /** + * The year of a birth date. + */ + inline val birthdateYear get() = AutoComplete("bday-year") + + + /** + * A gender identity (such as "Female", "Fa'afafine", "Male"), as freeform text without newlines. + */ + inline val sex get() = AutoComplete("sex") + + /** + * A full telephone number, including the country code. If you need to break the phone number up into its components, you can use these values for those fields: + */ + inline val tel get() = AutoComplete("tel") + + /** + * The country code, such as "1" for the United States, Canada, and other areas in North America and parts of the Caribbean. + */ + inline val telCountryCode get() = AutoComplete("tel-country-code") + + /** + * The entire phone number without the country code component, including a country-internal prefix. For the phone number "1-855-555-6502", this field's value would be "855-555-6502". + */ + inline val telNational get() = AutoComplete("tel-national") + + /** + * The area code, with any country-internal prefix applied if appropriate. + */ + inline val telAreaCode get() = AutoComplete("tel-area-code") + + /** + * The phone number without the country or area code. This can be split further into two parts, for phone numbers which have an exchange number and then a number within the exchange. For the phone number "555-6502", use "tel-local-prefix" for "555" and "tel-local-suffix" for "6502". + */ + inline val telLocal get() = AutoComplete("tel-local") + + /** + * A telephone extension code within the phone number, such as a room or suite number in a hotel or an office extension in a company. + */ + inline val telExtension get() = AutoComplete("tel-extension") + + /** + * A URL for an instant messaging protocol endpoint, such as "xmpp:username@example.net". + */ + inline val impp get() = AutoComplete("impp") + + /** + * A URL, such as a home page or company web site address as appropriate given the context of the other fields in the form. + */ + inline val url get() = AutoComplete("url") + + /** + * The URL of an image representing the person, company, or contact information given in the other fields in the form. + */ + inline val photo get() = AutoComplete("photo") + } +} + +@Suppress("NOTHING_TO_INLINE") +inline fun AutoComplete(value: String) = value.unsafeCast() diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/SyntheticEventListener.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/SyntheticEventListener.kt new file mode 100644 index 0000000000..fa7926eaba --- /dev/null +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/SyntheticEventListener.kt @@ -0,0 +1,156 @@ +package org.jetbrains.compose.web.attributes + +import org.jetbrains.compose.web.events.SyntheticInputEvent +import androidx.compose.web.events.SyntheticDragEvent +import androidx.compose.web.events.SyntheticEvent +import androidx.compose.web.events.SyntheticMouseEvent +import androidx.compose.web.events.SyntheticWheelEvent +import org.jetbrains.compose.web.attributes.EventsListenerBuilder.Companion.CHANGE +import org.jetbrains.compose.web.attributes.EventsListenerBuilder.Companion.INPUT +import org.jetbrains.compose.web.attributes.EventsListenerBuilder.Companion.SELECT +import org.jetbrains.compose.web.events.* +import org.w3c.dom.DragEvent +import org.w3c.dom.TouchEvent +import org.w3c.dom.clipboard.ClipboardEvent +import org.w3c.dom.events.* + +open class SyntheticEventListener> internal constructor( + val event: String, + val options: Options, + val listener: (T) -> Unit +) : EventListener { + + @Suppress("UNCHECKED_CAST") + override fun handleEvent(event: Event) { + listener(SyntheticEvent(event).unsafeCast()) + } +} + +class Options { + // TODO: add options for addEventListener + + companion object { + val DEFAULT = Options() + } +} + +internal class AnimationEventListener( + event: String, + options: Options, + listener: (SyntheticAnimationEvent) -> Unit +) : SyntheticEventListener( + event, options, listener +) { + override fun handleEvent(event: Event) { + listener(SyntheticAnimationEvent(event, event.unsafeCast())) + } +} + +internal class MouseEventListener( + event: String, + options: Options, + listener: (SyntheticMouseEvent) -> Unit +) : SyntheticEventListener(event, options, listener) { + override fun handleEvent(event: Event) { + listener(SyntheticMouseEvent(event.unsafeCast())) + } +} + +internal class MouseWheelEventListener( + event: String, + options: Options, + listener: (SyntheticWheelEvent) -> Unit +) : SyntheticEventListener(event, options, listener) { + override fun handleEvent(event: Event) { + listener(SyntheticWheelEvent(event.unsafeCast())) + } +} + +internal class KeyboardEventListener( + event: String, + options: Options, + listener: (SyntheticKeyboardEvent) -> Unit +) : SyntheticEventListener(event, options, listener) { + override fun handleEvent(event: Event) { + listener(SyntheticKeyboardEvent(event.unsafeCast())) + } +} + +internal class FocusEventListener( + event: String, + options: Options, + listener: (SyntheticFocusEvent) -> Unit +) : SyntheticEventListener(event, options, listener) { + override fun handleEvent(event: Event) { + listener(SyntheticFocusEvent(event.unsafeCast())) + } +} + +internal class TouchEventListener( + event: String, + options: Options, + listener: (SyntheticTouchEvent) -> Unit +) : SyntheticEventListener(event, options, listener) { + override fun handleEvent(event: Event) { + listener(SyntheticTouchEvent(event.unsafeCast())) + } +} + +internal class DragEventListener( + event: String, + options: Options, + listener: (SyntheticDragEvent) -> Unit +) : SyntheticEventListener(event, options, listener) { + override fun handleEvent(event: Event) { + listener(SyntheticDragEvent(event.unsafeCast())) + } +} + +internal class ClipboardEventListener( + event: String, + options: Options, + listener: (SyntheticClipboardEvent) -> Unit +) : SyntheticEventListener(event, options, listener) { + override fun handleEvent(event: Event) { + listener(SyntheticClipboardEvent(event.unsafeCast())) + } +} + +internal class InputEventListener( + eventName: String = INPUT, + options: Options, + val inputType: InputType, + listener: (SyntheticInputEvent) -> Unit +) : SyntheticEventListener>( + eventName, options, listener +) { + override fun handleEvent(event: Event) { + val value = inputType.inputValue(event) + listener(SyntheticInputEvent(value, event)) + } +} + +internal class ChangeEventListener( + options: Options, + val inputType: InputType, + listener: (SyntheticChangeEvent) -> Unit +) : SyntheticEventListener>( + CHANGE, options, listener +) { + override fun handleEvent(event: Event) { + val value = inputType.inputValue(event) + listener(SyntheticChangeEvent(value, event)) + } +} + +internal class SelectEventListener( + options: Options, + listener: (SyntheticSelectEvent) -> Unit +) : SyntheticEventListener>( + SELECT, options, listener +) { + override fun handleEvent(event: Event) { + listener(SyntheticSelectEvent(event, event.target.unsafeCast())) + } +} + diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/InputAttrsBuilder.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/InputAttrsBuilder.kt new file mode 100644 index 0000000000..efbd88b53e --- /dev/null +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/InputAttrsBuilder.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.web.attributes.builders + +import androidx.compose.web.events.SyntheticEvent +import org.jetbrains.compose.web.attributes.* +import org.jetbrains.compose.web.events.SyntheticChangeEvent +import org.jetbrains.compose.web.events.SyntheticInputEvent +import org.jetbrains.compose.web.events.SyntheticSelectEvent +import org.w3c.dom.HTMLInputElement + +class InputAttrsBuilder( + val inputType: InputType +) : AttrsBuilder() { + + fun onInvalid( + options: Options = Options.DEFAULT, + listener: (SyntheticEvent) -> Unit + ) { + addEventListener(INVALID, options, listener) + } + + fun onInput( + options: Options = Options.DEFAULT, + listener: (SyntheticInputEvent) -> Unit + ) { + listeners.add(InputEventListener(eventName = INPUT, options, inputType, listener)) + } + + fun onChange( + options: Options = Options.DEFAULT, + listener: (SyntheticChangeEvent) -> Unit + ) { + listeners.add(ChangeEventListener(options, inputType, listener)) + } + + fun onBeforeInput( + options: Options = Options.DEFAULT, + listener: (SyntheticInputEvent) -> Unit + ) { + listeners.add(InputEventListener(eventName = BEFOREINPUT, options, inputType, listener)) + } + + fun onSelect( + options: Options = Options.DEFAULT, + listener: (SyntheticSelectEvent) -> Unit + ) { + listeners.add(SelectEventListener(options, listener)) + } +} diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/SelectAttrsBuilder.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/SelectAttrsBuilder.kt new file mode 100644 index 0000000000..8cbcf602f5 --- /dev/null +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/SelectAttrsBuilder.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package androidx.compose.web.attributes + +import org.jetbrains.compose.web.attributes.AttrsBuilder +import org.jetbrains.compose.web.attributes.EventsListenerBuilder.Companion.CHANGE +import org.jetbrains.compose.web.attributes.EventsListenerBuilder.Companion.INPUT +import org.jetbrains.compose.web.attributes.Options +import org.jetbrains.compose.web.attributes.SyntheticEventListener +import org.jetbrains.compose.web.events.SyntheticChangeEvent +import org.jetbrains.compose.web.events.SyntheticInputEvent +import org.w3c.dom.HTMLSelectElement +import org.w3c.dom.events.Event + +class SelectAttrsBuilder : AttrsBuilder() { + + fun onInput( + options: Options = Options.DEFAULT, + listener: (SyntheticInputEvent) -> Unit + ) { + listeners.add(SelectInputEventListener(INPUT, options, listener)) + } + + fun onChange( + options: Options = Options.DEFAULT, + listener: (SyntheticChangeEvent) -> Unit + ) { + listeners.add(SelectChangeEventListener(options, listener)) + } +} + +private class SelectInputEventListener( + eventName: String = INPUT, + options: Options = Options.DEFAULT, + listener: (SyntheticInputEvent) -> Unit +) : SyntheticEventListener>( + eventName, options, listener +) { + override fun handleEvent(event: Event) { + val value = event.target?.asDynamic().value?.toString() + listener(SyntheticInputEvent(value, event)) + } +} + +private class SelectChangeEventListener( + options: Options = Options.DEFAULT, + listener: (SyntheticChangeEvent) -> Unit +): SyntheticEventListener>( + CHANGE, options, listener +) { + override fun handleEvent(event: Event) { + val value = event.target?.asDynamic().value?.toString() + listener(SyntheticChangeEvent(value, event)) + } +} diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/TextAreaAttrsBuilder.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/TextAreaAttrsBuilder.kt new file mode 100644 index 0000000000..6dd7f6d49e --- /dev/null +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/TextAreaAttrsBuilder.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.web.attributes.builders + +import org.jetbrains.compose.web.attributes.* +import org.jetbrains.compose.web.events.SyntheticChangeEvent +import org.jetbrains.compose.web.events.SyntheticSelectEvent +import org.jetbrains.compose.web.events.SyntheticInputEvent +import org.w3c.dom.HTMLTextAreaElement + +class TextAreaAttrsBuilder : AttrsBuilder() { + + fun onInput( + options: Options = Options.DEFAULT, + listener: (SyntheticInputEvent) -> Unit + ) { + listeners.add(InputEventListener(INPUT, options, InputType.Text, listener)) + } + + fun onChange( + options: Options = Options.DEFAULT, + listener: (SyntheticChangeEvent) -> Unit + ) { + listeners.add(ChangeEventListener(options, InputType.Text, listener)) + } + + fun onBeforeInput( + options: Options = Options.DEFAULT, + listener: (SyntheticInputEvent) -> Unit + ) { + listeners.add(InputEventListener(BEFOREINPUT, options, InputType.Text, listener)) + } + + fun onSelect( + options: Options = Options.DEFAULT, + listener: (SyntheticSelectEvent) -> Unit + ) { + listeners.add(SelectEventListener(options, listener)) + } +} diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/css/BrowserAPI.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/BrowserAPI.kt similarity index 63% rename from web/core/src/jsMain/kotlin/androidx/compose/web/css/BrowserAPI.kt rename to web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/BrowserAPI.kt index 325dd02f41..7b1e44dc16 100644 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/css/BrowserAPI.kt +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/BrowserAPI.kt @@ -6,6 +6,19 @@ @file:Suppress("UNUSED", "NOTHING_TO_INLINE", "FunctionName") package org.jetbrains.compose.web.css +import org.w3c.dom.css.CSSRule +import org.w3c.dom.css.CSSRuleList + + +external class CSSKeyframesRule: CSSRule { + val name: String + val cssRules: CSSRuleList +} + +inline fun CSSKeyframesRule.appendRule(cssRule: String) { + this.asDynamic().appendRule(cssRule) +} + @Suppress("NOTHING_TO_INLINE") inline fun jsObject(): T = js("({})") diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSBuilder.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/CSSBuilder.kt similarity index 100% rename from web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSBuilder.kt rename to web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/CSSBuilder.kt diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSEnums.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/CSSEnums.kt similarity index 64% rename from web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSEnums.kt rename to web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/CSSEnums.kt index aa7b96a500..4cbdd24652 100644 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSEnums.kt +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/CSSEnums.kt @@ -2,12 +2,12 @@ package org.jetbrains.compose.web.css -external interface StylePropertyEnum: StylePropertyString +interface StylePropertyEnum: StylePropertyString inline val StylePropertyEnum.name get() = this.unsafeCast() inline val StylePropertyEnum.value get() = this.unsafeCast() -external interface LineStyle: StylePropertyEnum { - external companion object { +interface LineStyle: StylePropertyEnum { + companion object { inline val None get() = LineStyle("none") inline val Hidden get() = LineStyle("hidden") inline val Dotted get() = LineStyle("dotted") @@ -22,8 +22,8 @@ external interface LineStyle: StylePropertyEnum { } inline fun LineStyle(value: String) = value.unsafeCast() -external interface DisplayStyle: StylePropertyEnum { - external companion object { +interface DisplayStyle: StylePropertyEnum { + companion object { inline val Block get() = DisplayStyle("block") inline val Inline get() = DisplayStyle("inline") inline val InlineBlock get() = DisplayStyle("inline-block") @@ -57,7 +57,7 @@ external interface DisplayStyle: StylePropertyEnum { } inline fun DisplayStyle(value: String) = value.unsafeCast() -external interface FlexDirection: StylePropertyEnum { +interface FlexDirection: StylePropertyEnum { companion object { inline val Row get() = FlexDirection("row") inline val RowReverse get() = FlexDirection("row-reverse") @@ -67,7 +67,7 @@ external interface FlexDirection: StylePropertyEnum { } inline fun FlexDirection(value: String) = value.unsafeCast() -external interface FlexWrap: StylePropertyEnum { +interface FlexWrap: StylePropertyEnum { companion object { inline val Wrap get() = FlexWrap("wrap") inline val Nowrap get() = FlexWrap("nowrap") @@ -76,7 +76,7 @@ external interface FlexWrap: StylePropertyEnum { } inline fun FlexWrap(value: String) = value.unsafeCast() -external interface JustifyContent: StylePropertyEnum { +interface JustifyContent: StylePropertyEnum { companion object { inline val Center get() = JustifyContent("center") inline val Start get() = JustifyContent("start") @@ -99,7 +99,7 @@ external interface JustifyContent: StylePropertyEnum { } inline fun JustifyContent(value: String) = value.unsafeCast() -external interface AlignSelf: StylePropertyEnum { +interface AlignSelf: StylePropertyEnum { companion object { inline val Auto get() = AlignSelf("auto") inline val Normal get() = AlignSelf("normal") @@ -123,7 +123,7 @@ external interface AlignSelf: StylePropertyEnum { } inline fun AlignSelf(value: String) = value.unsafeCast() -external interface AlignItems: StylePropertyEnum { +interface AlignItems: StylePropertyEnum { companion object { inline val Normal get() = AlignItems("normal") inline val Stretch get() = AlignItems("stretch") @@ -145,7 +145,7 @@ external interface AlignItems: StylePropertyEnum { } inline fun AlignItems(value: String) = value.unsafeCast() -external interface AlignContent: StylePropertyEnum { +interface AlignContent: StylePropertyEnum { companion object { inline val Center get() = AlignContent("center") inline val Start get() = AlignContent("start") @@ -169,7 +169,7 @@ external interface AlignContent: StylePropertyEnum { } inline fun AlignContent(value: String) = value.unsafeCast() -external interface Position: StylePropertyEnum { +interface Position: StylePropertyEnum { companion object { inline val Static get() = Position("static") inline val Relative get() = Position("relative") @@ -181,3 +181,83 @@ external interface Position: StylePropertyEnum { inline fun Position(value: String) = value.unsafeCast() typealias LanguageCode = String + +interface StepPosition: StylePropertyEnum { + companion object { + inline val JumpStart get() = StepPosition("jump-start") + inline val JumpEnd get() = StepPosition("jump-end") + inline val JumpNone get() = StepPosition("jump-none") + inline val JumpBoth get() = StepPosition("jump-both") + inline val Start get() = StepPosition("start") + inline val End get() = StepPosition("end") + } +} +inline fun StepPosition(value: String) = value.unsafeCast() + +interface AnimationTimingFunction: StylePropertyEnum { + companion object { + inline val Ease get() = AnimationTimingFunction("ease") + inline val EaseIn get() = AnimationTimingFunction("ease-in") + inline val EaseOut get() = AnimationTimingFunction("ease-out") + inline val EaseInOut get() = AnimationTimingFunction("ease-in-out") + inline val Linear get() = AnimationTimingFunction("linear") + inline val StepStart get() = AnimationTimingFunction("step-start") + inline val StepEnd get() = AnimationTimingFunction("step-end") + + inline fun cubicBezier(x1: Double, y1: Double, x2: Double, y2: Double) = AnimationTimingFunction("cubic-bezier($x1, $y1, $x2, $y2)") + inline fun steps(count: Int, stepPosition: StepPosition) = AnimationTimingFunction("steps($count, $stepPosition)") + inline fun steps(count: Int) = AnimationTimingFunction("steps($count)") + + inline val Inherit get() = AnimationTimingFunction("inherit") + inline val Initial get() = AnimationTimingFunction("initial") + inline val Unset get() = AnimationTimingFunction("unset") + } +} +inline fun AnimationTimingFunction(value: String) = value.unsafeCast() + +interface AnimationDirection: StylePropertyEnum { + companion object { + inline val Normal get() = AnimationDirection("normal") + inline val Reverse get() = AnimationDirection("reverse") + inline val Alternate get() = AnimationDirection("alternate") + inline val AlternateReverse get() = AnimationDirection("alternate-reverse") + + inline val Inherit get() = AnimationDirection("inherit") + inline val Initial get() = AnimationDirection("initial") + inline val Unset get() = AnimationDirection("unset") + } +} +inline fun AnimationDirection(value: String) = value.unsafeCast() + +interface AnimationFillMode: StylePropertyEnum { + companion object { + inline val None get() = AnimationFillMode("none") + inline val Forwards get() = AnimationFillMode("forwards") + inline val Backwards get() = AnimationFillMode("backwards") + inline val Both get() = AnimationFillMode("both") + } +} +inline fun AnimationFillMode(value: String) = value.unsafeCast() + +interface AnimationPlayState: StylePropertyEnum { + companion object { + inline val Running get() = AnimationPlayState("running") + inline val Paused get() = AnimationPlayState("Paused") + inline val Backwards get() = AnimationPlayState("backwards") + inline val Both get() = AnimationPlayState("both") + + inline val Inherit get() = AnimationPlayState("inherit") + inline val Initial get() = AnimationPlayState("initial") + inline val Unset get() = AnimationPlayState("unset") + } +} +inline fun AnimationPlayState(value: String) = value.unsafeCast() + + +object GridAutoFlow : StylePropertyString { + inline val Row get() = "row".unsafeCast() + inline val Column get() = "column".unsafeCast() + inline val Dense get() = "dense".unsafeCast() + inline val RowDense get() = "row dense".unsafeCast() + inline val ColumnDense get() = "column dense".unsafeCast() +} \ No newline at end of file diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/CSSKeyframeRule.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/CSSKeyframeRule.kt new file mode 100644 index 0000000000..37ef76fcef --- /dev/null +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/CSSKeyframeRule.kt @@ -0,0 +1,73 @@ +@file:Suppress("unused", "MemberVisibilityCanBePrivate") + +package org.jetbrains.compose.web.css + +interface CSSNamedKeyframes { + val name: String +} + +data class CSSKeyframesRuleDeclaration( + override val name: String, + val keys: CSSKeyframeRuleDeclarationList +) : CSSRuleDeclaration, CSSNamedKeyframes { + override val header: String + get() = "@keyframes $name" +} + +typealias CSSKeyframeRuleDeclarationList = List + +abstract class CSSKeyframe { + abstract override fun toString(): String + + object From: CSSKeyframe() { + override fun toString(): String = "from" + } + + object To: CSSKeyframe() { + override fun toString(): String = "to" + } + + data class Percentage(val value: CSSSizeValue): CSSKeyframe() { + override fun toString(): String = value.toString() + } + + data class Combine(val values: List>): CSSKeyframe() { + override fun toString(): String = values.joinToString(", ") + } +} + +data class CSSKeyframeRuleDeclaration( + val keyframe: CSSKeyframe, + override val style: StyleHolder +) : CSSRuleDeclaration, CSSStyledRuleDeclaration { + override val header: String + get() = keyframe.toString() +} + +class CSSKeyframesBuilder() { + constructor(init: CSSKeyframesBuilder.() -> Unit) : this() { + init() + } + val frames: MutableList = mutableListOf() + + fun from(style: CSSStyleRuleBuilder.() -> Unit) { + frames += CSSKeyframeRuleDeclaration(CSSKeyframe.From, buildCSSStyleRule(style)) + } + + fun to(style: CSSStyleRuleBuilder.() -> Unit) { + frames += CSSKeyframeRuleDeclaration(CSSKeyframe.To, buildCSSStyleRule(style)) + } + + fun each(vararg keys: CSSSizeValue, style: CSSStyleRuleBuilder.() -> Unit) { + frames += CSSKeyframeRuleDeclaration(CSSKeyframe.Combine(keys.toList()), buildCSSStyleRule(style)) + } + + operator fun CSSSizeValue.invoke(style: CSSStyleRuleBuilder.() -> Unit) { + frames += CSSKeyframeRuleDeclaration(CSSKeyframe.Percentage(this), buildCSSStyleRule(style)) + } +} + +fun buildKeyframes(name: String, builder: CSSKeyframesBuilder.() -> Unit): CSSKeyframesRuleDeclaration { + val frames = CSSKeyframesBuilder(builder).frames + return CSSKeyframesRuleDeclaration(name, frames) +} diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSKeywords.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/CSSKeywords.kt similarity index 100% rename from web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSKeywords.kt rename to web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/CSSKeywords.kt diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSMediaRule.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/CSSMediaRule.kt similarity index 84% rename from web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSMediaRule.kt rename to web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/CSSMediaRule.kt index b25d447dd4..24fd0e4f31 100644 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSMediaRule.kt +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/CSSMediaRule.kt @@ -18,7 +18,7 @@ interface CSSMediaQuery { } @Suppress("EqualsOrHashCode") - data class MediaFeature( + class MediaFeature( val name: String, val value: StylePropertyValue? = null ) : CSSMediaQuery, Atomic { @@ -61,8 +61,8 @@ interface CSSMediaQuery { @Suppress("EqualsOrHashCode") class CSSMediaRuleDeclaration( val query: CSSMediaQuery, - rules: CSSRuleDeclarationList -) : CSSGroupingRuleDeclaration(rules) { + override val rules: CSSRuleDeclarationList +) : CSSGroupingRuleDeclaration { override val header: String get() = "@media $query" @@ -128,14 +128,33 @@ fun GenericStyleSheetBuilder.not( query: CSSMediaQuery.Invertible ) = CSSMediaQuery.Not(query) -fun GenericStyleSheetBuilder.minWidth(value: CSSUnitValue) = +/** + * A mediaQuery selector + * + * Example: + * ``` + * object CombinedMediaQueries : StyleSheet() { + * media(mediaMinWidth(200.px).and(mediaMaxWidth(400.px))) { ... } + * } + * ``` + */ +fun GenericStyleSheetBuilder.mediaMinWidth(value: CSSUnitValue) = CSSMediaQuery.MediaFeature("min-width", value) -fun GenericStyleSheetBuilder.maxWidth(value: CSSUnitValue) = +/** + * See [mediaMinWidth] + */ +fun GenericStyleSheetBuilder.mediaMaxWidth(value: CSSUnitValue) = CSSMediaQuery.MediaFeature("max-width", value) -fun GenericStyleSheetBuilder.minHeight(value: CSSUnitValue) = +/** + * See [mediaMinWidth] + */ +fun GenericStyleSheetBuilder.mediaMinHeight(value: CSSUnitValue) = CSSMediaQuery.MediaFeature("min-height", value) -fun GenericStyleSheetBuilder.maxHeight(value: CSSUnitValue) = +/** + * See [mediaMinWidth] + */ +fun GenericStyleSheetBuilder.mediaMaxHeight(value: CSSUnitValue) = CSSMediaQuery.MediaFeature("max-height", value) diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSOperations.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/CSSOperations.kt similarity index 94% rename from web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSOperations.kt rename to web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/CSSOperations.kt index 520a4e5cfc..2001b9a863 100644 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSOperations.kt +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/CSSOperations.kt @@ -12,6 +12,8 @@ operator fun CSSSizeValue.div(num: Number): CSSSizeValue = CS operator fun CSSSizeValue.plus(b: CSSSizeValue): CSSSizeValue = CSSUnitValueTyped(value + b.value, unit) operator fun CSSSizeValue.minus(b: CSSSizeValue): CSSSizeValue = CSSUnitValueTyped(value - b.value, unit) +operator fun CSSSizeValue.unaryMinus(): CSSSizeValue = CSSUnitValueTyped(-value, unit) +operator fun CSSSizeValue.unaryPlus(): CSSSizeValue = CSSUnitValueTyped(value, unit) external interface CSSCalcOperation: CSSNumericValue diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSRules.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/CSSRules.kt similarity index 69% rename from web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSRules.kt rename to web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/CSSRules.kt index 532655e3a7..515a81d42f 100644 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSRules.kt +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/CSSRules.kt @@ -6,23 +6,28 @@ interface CSSStyleRuleBuilder : StyleBuilder open class CSSRuleBuilderImpl : CSSStyleRuleBuilder, StyleBuilderImpl() -abstract class CSSRuleDeclaration { - abstract val header: String +@Suppress("EqualsOrHashCode") +interface CSSRuleDeclaration { + val header: String - abstract override fun equals(other: Any?): Boolean + override fun equals(other: Any?): Boolean +} + +interface CSSStyledRuleDeclaration { + val style: StyleHolder } data class CSSStyleRuleDeclaration( val selector: CSSSelector, - val style: StyleHolder -) : CSSRuleDeclaration() { + override val style: StyleHolder +) : CSSRuleDeclaration, CSSStyledRuleDeclaration { override val header get() = selector.toString() } -abstract class CSSGroupingRuleDeclaration( +interface CSSGroupingRuleDeclaration: CSSRuleDeclaration { val rules: CSSRuleDeclarationList -) : CSSRuleDeclaration() +} typealias CSSRuleDeclarationList = List typealias MutableCSSRuleDeclarationList = MutableList diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSUnits.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/CSSUnits.kt similarity index 75% rename from web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSUnits.kt rename to web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/CSSUnits.kt index e89f3729d1..b2ec469c50 100644 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/css/CSSUnits.kt +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/CSSUnits.kt @@ -16,16 +16,16 @@ data class CSSUnitValueTyped( override fun toString(): String = "$value$unit" } -external interface CSSUnitLengthOrPercentage: CSSUnit -external interface CSSUnitPercentage: CSSUnitLengthOrPercentage -external interface CSSUnitLength: CSSUnitLengthOrPercentage -external interface CSSUnitRel : CSSUnitLength -external interface CSSUnitAbs: CSSUnitLength -external interface CSSUnitAngle: CSSUnit -external interface CSSUnitTime: CSSUnit -external interface CSSUnitFrequency: CSSUnit -external interface CSSUnitResolution: CSSUnit -external interface CSSUnitFlex: CSSUnit +interface CSSUnitLengthOrPercentage: CSSUnit +interface CSSUnitPercentage: CSSUnitLengthOrPercentage +interface CSSUnitLength: CSSUnitLengthOrPercentage +interface CSSUnitRel : CSSUnitLength +interface CSSUnitAbs: CSSUnitLength +interface CSSUnitAngle: CSSUnit +interface CSSUnitTime: CSSUnit +interface CSSUnitFrequency: CSSUnit +interface CSSUnitResolution: CSSUnit +interface CSSUnitFlex: CSSUnit typealias CSSAngleValue = CSSSizeValue typealias CSSLengthOrPercentageValue = CSSSizeValue @@ -36,82 +36,82 @@ typealias CSSNumeric = CSSNumericValue typealias CSSpxValue = CSSSizeValue // fake interfaces to distinguish units -external interface CSSUnit { - external interface percent: CSSUnitPercentage +interface CSSUnit { + interface percent: CSSUnitPercentage - external interface em: CSSUnitRel + interface em: CSSUnitRel - external interface ex: CSSUnitRel + interface ex: CSSUnitRel - external interface ch: CSSUnitRel + interface ch: CSSUnitRel - external interface ic: CSSUnitRel + interface ic: CSSUnitRel - external interface rem: CSSUnitRel + interface rem: CSSUnitRel - external interface lh: CSSUnitRel + interface lh: CSSUnitRel - external interface rlh: CSSUnitRel + interface rlh: CSSUnitRel - external interface vw: CSSUnitRel + interface vw: CSSUnitRel - external interface vh: CSSUnitRel + interface vh: CSSUnitRel - external interface vi: CSSUnitRel + interface vi: CSSUnitRel - external interface vb: CSSUnitRel + interface vb: CSSUnitRel - external interface vmin: CSSUnitRel + interface vmin: CSSUnitRel - external interface vmax: CSSUnitRel + interface vmax: CSSUnitRel - external interface cm: CSSUnitRel + interface cm: CSSUnitRel - external interface mm: CSSUnitRel + interface mm: CSSUnitRel - external interface Q: CSSUnitRel + interface Q: CSSUnitRel - external interface pt: CSSUnitAbs + interface pt: CSSUnitAbs - external interface pc: CSSUnitAbs + interface pc: CSSUnitAbs - external interface px: CSSUnitAbs + interface px: CSSUnitAbs - external interface deg: CSSUnitAngle + interface deg: CSSUnitAngle - external interface grad: CSSUnitAngle + interface grad: CSSUnitAngle - external interface rad: CSSUnitAngle + interface rad: CSSUnitAngle - external interface turn: CSSUnitAngle + interface turn: CSSUnitAngle - external interface s: CSSUnitTime + interface s: CSSUnitTime - external interface ms: CSSUnitTime + interface ms: CSSUnitTime - external interface Hz: CSSUnitFrequency + interface Hz: CSSUnitFrequency - external interface kHz: CSSUnitFrequency + interface kHz: CSSUnitFrequency - external interface dpi: CSSUnitResolution + interface dpi: CSSUnitResolution - external interface dpcm: CSSUnitResolution + interface dpcm: CSSUnitResolution - external interface dppx: CSSUnitResolution + interface dppx: CSSUnitResolution - external interface fr: CSSUnitFlex + interface fr: CSSUnitFlex - external interface number: CSSUnit - - external companion object { + interface number: CSSUnit + + companion object { inline val percent get() = "%".unsafeCast() inline val em get() = "em".unsafeCast() - + inline val ex get() = "ex".unsafeCast() inline val ch get() = "ch".unsafeCast() - + inline val ic get() = "ic".unsafeCast() inline val rem get() = "rem".unsafeCast() @@ -119,7 +119,7 @@ external interface CSSUnit { inline val lh get() = "lh".unsafeCast() inline val rlh get() = "rlh".unsafeCast() - + inline val vw get() = "vw".unsafeCast() inline val vh get() = "vh".unsafeCast() diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/Color.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/Color.kt new file mode 100644 index 0000000000..d033b31f01 --- /dev/null +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/Color.kt @@ -0,0 +1,198 @@ +@file:Suppress("unused", "MemberVisibilityCanBePrivate") + +package org.jetbrains.compose.web.css + +external interface CSSColorValue : StylePropertyValue, CSSVariableValueAs + +object Color { + + @Deprecated("use org.jetbrains.compose.web.css.rgb", ReplaceWith("rgb(r, g, b)")) + data class RGB(val r: Number, val g: Number, val b: Number) : CSSColorValue { + override fun toString(): String = "rgb($r, $g, $b)" + } + + @Deprecated("use org.jetbrains.compose.web.css.rgba", ReplaceWith("rgba(r, g, b, a)")) + data class RGBA(val r: Number, val g: Number, val b: Number, val a: Number) : CSSColorValue { + override fun toString(): String = "rgba($r, $g, $b, $a)" + } + + @Deprecated("use org.jetbrains.compose.web.css.hsl", ReplaceWith("hsl(h, s, l)")) + data class HSL(val h: CSSAngleValue, val s: Number, val l: Number) : CSSColorValue { + constructor(h: Number, s: Number, l: Number) : this(h.deg, s, l) + + override fun toString(): String = "hsl($h, $s%, $l%)" + } + + @Deprecated("use org.jetbrains.compose.web.css.hsla", ReplaceWith("hsla(h, s, l, a)")) + data class HSLA(val h: CSSAngleValue, val s: Number, val l: Number, val a: Number) : CSSColorValue { + constructor(h: Number, s: Number, l: Number, a: Number) : this(h.deg, s, l, a) + + override fun toString(): String = "hsla($h, $s%, $l%, $a)" + } + + inline val aliceblue get() = Color("aliceblue") + inline val antiquewhite get() = Color("antiquewhite") + inline val aquamarine get() = Color("aquamarine") + inline val azure get() = Color("azure") + inline val beige get() = Color("beige") + inline val bisque get() = Color("bisque") + inline val black get() = Color("black") + inline val blanchedalmond get() = Color("blanchedalmond") + inline val blue get() = Color("blue") + inline val blueviolet get() = Color("blueviolet") + inline val brown get() = Color("brown") + inline val burlywood get() = Color("burlywood") + inline val cadetblue get() = Color("cadetblue") + inline val chartreuse get() = Color("chartreuse") + inline val chocolate get() = Color("chocolate") + inline val cornflowerblue get() = Color("cornflowerblue") + inline val cornsilk get() = Color("cornsilk") + inline val crimson get() = Color("crimson") + inline val cyan get() = Color("cyan") + inline val darkblue get() = Color("darkblue") + inline val darkcyan get() = Color("darkcyan") + inline val darkgoldenrod get() = Color("darkgoldenrod") + inline val darkgray get() = Color("darkgray") + inline val darkgreen get() = Color("darkgreen") + inline val darkkhaki get() = Color("darkkhaki") + inline val darkmagenta get() = Color("darkmagenta") + inline val darkolivegreen get() = Color("darkolivegreen") + inline val darkorange get() = Color("darkorange") + inline val darkorchid get() = Color("darkorchid") + inline val darkred get() = Color("darkred") + inline val darksalmon get() = Color("darksalmon") + inline val darkslateblue get() = Color("darkslateblue") + inline val darkslategray get() = Color("darkslategray") + inline val darkturquoise get() = Color("darkturquoise") + inline val darkviolet get() = Color("darkviolet") + inline val deeppink get() = Color("deeppink") + inline val deepskyblue get() = Color("deepskyblue") + inline val dimgray get() = Color("dimgray") + inline val dodgerblue get() = Color("dodgerblue") + inline val firebrick get() = Color("firebrick") + inline val floralwhite get() = Color("floralwhite") + inline val forestgreen get() = Color("forestgreen") + inline val fuchsia get() = Color("fuchsia") + inline val gainsboro get() = Color("gainsboro") + inline val ghostwhite get() = Color("ghostwhite") + inline val goldenrod get() = Color("goldenrod") + inline val gold get() = Color("gold") + inline val gray get() = Color("gray") + inline val green get() = Color("green") + inline val greenyellow get() = Color("greenyellow") + inline val honeydew get() = Color("honeydew") + inline val hotpink get() = Color("hotpink") + inline val indianred get() = Color("indianred") + inline val indigo get() = Color("indigo") + inline val ivory get() = Color("ivory") + inline val khaki get() = Color("khaki") + inline val lavenderblush get() = Color("lavenderblush") + inline val lavender get() = Color("lavender") + inline val lawngreen get() = Color("lawngreen") + inline val lemonchiffon get() = Color("lemonchiffon") + inline val lightblue get() = Color("lightblue") + inline val lightcoral get() = Color("lightcoral") + inline val lightcyan get() = Color("lightcyan") + inline val lightgoldenrodyellow get() = Color("lightgoldenrodyellow") + inline val lightgray get() = Color("lightgray") + inline val lightgreen get() = Color("lightgreen") + inline val lightpink get() = Color("lightpink") + inline val lightsalmon get() = Color("lightsalmon") + inline val lightseagreen get() = Color("lightseagreen") + inline val lightskyblue get() = Color("lightskyblue") + inline val lightslategray get() = Color("lightslategray") + inline val lightsteelblue get() = Color("lightsteelblue") + inline val lightyellow get() = Color("lightyellow") + inline val limegreen get() = Color("limegreen") + inline val lime get() = Color("lime") + inline val linen get() = Color("linen") + inline val magenta get() = Color("magenta") + inline val maroon get() = Color("maroon") + inline val mediumaquamarine get() = Color("mediumaquamarine") + inline val mediumblue get() = Color("mediumblue") + inline val mediumorchid get() = Color("mediumorchid") + inline val mediumpurple get() = Color("mediumpurple") + inline val mediumseagreen get() = Color("mediumseagreen") + inline val mediumslateblue get() = Color("mediumslateblue") + inline val mediumspringgreen get() = Color("mediumspringgreen") + inline val mediumturquoise get() = Color("mediumturquoise") + inline val mediumvioletred get() = Color("mediumvioletred") + inline val midnightblue get() = Color("midnightblue") + inline val mintcream get() = Color("mintcream") + inline val mistyrose get() = Color("mistyrose") + inline val moccasin get() = Color("moccasin") + inline val navajowhite get() = Color("navajowhite") + inline val navi get() = Color("navi") + inline val oldlace get() = Color("oldlace") + inline val olivedrab get() = Color("olivedrab") + inline val olive get() = Color("olive") + inline val orange get() = Color("orange") + inline val orangered get() = Color("orangered") + inline val orchid get() = Color("orchid") + inline val palegoldenrod get() = Color("palegoldenrod") + inline val palegreen get() = Color("palegreen") + inline val paleturquoise get() = Color("paleturquoise") + inline val palevioletred get() = Color("palevioletred") + inline val papayawhip get() = Color("papayawhip") + inline val peachpuff get() = Color("peachpuff") + inline val peru get() = Color("peru") + inline val pink get() = Color("pink") + inline val plum get() = Color("plum") + inline val powderblue get() = Color("powderblue") + inline val purple get() = Color("purple") + inline val rebeccapurple get() = Color("rebeccapurple") + inline val red get() = Color("red") + inline val rosybrown get() = Color("rosybrown") + inline val royalblue get() = Color("royalblue") + inline val saddlebrown get() = Color("saddlebrown") + inline val salmon get() = Color("salmon") + inline val sandybrown get() = Color("sandybrown") + inline val seagreen get() = Color("seagreen") + inline val seashell get() = Color("seashell") + inline val sienna get() = Color("sienna") + inline val silver get() = Color("silver") + inline val skyblue get() = Color("skyblue") + inline val slateblue get() = Color("slateblue") + inline val slategray get() = Color("slategray") + inline val snow get() = Color("snow") + inline val springgreen get() = Color("springgreen") + inline val steelblue get() = Color("steelblue") + inline val teal get() = Color("teal") + inline val thistle get() = Color("thistle") + inline val tomato get() = Color("tomato") + inline val turquoise get() = Color("turquoise") + inline val violet get() = Color("violet") + inline val wheat get() = Color("wheat") + inline val white get() = Color("white") + inline val whitesmoke get() = Color("whitesmoke") + inline val yellowgreen get() = Color("yellowgreen") + inline val yellow get() = Color("yellow") + + inline val transparent get() = Color("transparent") + inline val currentColor get() = Color("currentColor") +} + +fun Color(name: String): CSSColorValue = name.unsafeCast() + +private class RGB(val r: Number, val g: Number, val b: Number): CSSColorValue { + override fun toString(): String = "rgb($r, $g, $b)" +} + +private class RGBA(val r: Number, val g: Number, val b: Number, val a: Number) : CSSColorValue { + override fun toString(): String = "rgba($r, $g, $b, $a)" +} + +private class HSL(val h: CSSAngleValue, val s: Number, val l: Number) : CSSColorValue { + override fun toString(): String = "hsl($h, $s%, $l%)" +} + +private class HSLA(val h: CSSAngleValue, val s: Number, val l: Number, val a: Number) : CSSColorValue { + override fun toString(): String = "hsla($h, $s%, $l%, $a)" +} + +fun rgb(r: Number, g: Number, b: Number): CSSColorValue = RGB(r, g, b) +fun rgba(r: Number, g: Number, b: Number, a: Number): CSSColorValue = RGBA(r, g, b, a) +fun hsl(h: CSSAngleValue, s: Number, l: Number): CSSColorValue = HSL(h, s, l) +fun hsl(h: Number, s: Number, l: Number): CSSColorValue = HSL(h.deg, s, l) +fun hsla(h: CSSAngleValue, s: Number, l: Number, a: Number): CSSColorValue = HSLA(h, s, l, a) +fun hsla(h: Number, s: Number, l: Number, a: Number): CSSColorValue = HSLA(h.deg, s, l, a) diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/css/StyleBuilder.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/StyleBuilder.kt similarity index 65% rename from web/core/src/jsMain/kotlin/androidx/compose/web/css/StyleBuilder.kt rename to web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/StyleBuilder.kt index b781fdd4ec..4020e79e16 100644 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/css/StyleBuilder.kt +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/StyleBuilder.kt @@ -9,7 +9,30 @@ package org.jetbrains.compose.web.css import kotlin.properties.ReadOnlyProperty +/** + * StyleBuilder serves for two main purposes. Passed as a builder context (in [AttrsBuilder]), it + * makes it possible to: + * 1. Add inlined css properties to the element (@see [property]) + * 2. Set values to CSS variables (@see [variable]) + */ interface StyleBuilder { + /** + * Adds arbitrary CSS property to the inline style of the element + * @param propertyName - the name of css property as [per spec](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference) + * @param value - the value, it can be either String or specialized type like [CSSNumeric] or [CSSColorValue] + * + * Most frequent CSS property values can be set via specialized methods, like [width], [display] etc. + * + * Example: + * ``` + * Div({ + * style { + * property("some-exotic-css-property", "I am a string value") + * property("some-exotic-css-property-width", 5.px) + * } + * }) + * ``` + */ fun property(propertyName: String, value: StylePropertyValue) fun variable(variableName: String, value: StylePropertyValue) @@ -18,7 +41,7 @@ interface StyleBuilder { fun variable(variableName: String, value: String) = variable(variableName, StylePropertyValue(value)) fun variable(variableName: String, value: Number) = variable(variableName, StylePropertyValue(value)) - operator fun CSSStyleVariable.invoke(value: TValue) { + operator fun CSSStyleVariable.invoke(value: TValue) { variable(name, value.toString()) } @@ -34,7 +57,7 @@ interface StyleBuilder { inline fun variableValue(variableName: String, fallback: StylePropertyValue? = null) = "var(--$variableName${fallback?.let { ", $it" } ?: ""})" -external interface CSSVariableValueAs: StylePropertyValue +external interface CSSVariableValueAs inline fun CSSVariableValue(value: StylePropertyValue) = value.unsafeCast() @@ -52,15 +75,13 @@ fun StyleBuilder.add( value: StylePropertyValue ) = property(propertyName, value) -interface CSSVariables - interface CSSVariable { val name: String } -class CSSStyleVariable(override val name: String) : CSSVariable +class CSSStyleVariable(override val name: String) : CSSVariable -fun CSSStyleVariable.value(fallback: TValue? = null) = +fun CSSStyleVariable.value(fallback: TValue? = null) = CSSVariableValue( variableValue( name, @@ -68,7 +89,9 @@ fun CSSStyleVariable.value(fallback: TValue ) ) -fun > CSSStyleVariable.value(fallback: TValue? = null) = +fun CSSStyleVariable.value(fallback: TValue? = null) + where TValue : CSSVariableValueAs, + TValue : StylePropertyValue = CSSVariableValue( variableValue( name, @@ -76,7 +99,26 @@ fun > CSSStyleVariable.value(fallback ) ) -fun CSSVariables.variable() = +/** + * Introduces CSS variable that can be later referred anywhere in [StyleSheet] + * + * Example: + * ``` + * object AppCSSVariables { + * val width by variable() + * val stringHeight by variable() + * val order by variable() + * } + * + * object AppStylesheet : StyleSheet() { + * val classWithProperties by style { + * AppCSSVariables.width(100.px) + * property("width", AppCSSVariables.width.value()) + * } + *``` + * + */ +fun variable() = ReadOnlyProperty> { _, property -> CSSStyleVariable(property.name) } @@ -103,7 +145,7 @@ open class StyleBuilderImpl : StyleBuilder, StyleHolder { override fun equals(other: Any?): Boolean { return if (other is StyleHolder) { properties.nativeEquals(other.properties) && - variables.nativeEquals(other.variables) + variables.nativeEquals(other.variables) } else false } @@ -130,6 +172,6 @@ fun StylePropertyList.nativeEquals(properties: StylePropertyList): Boolean { return all { prop -> val otherProp = properties[index++] prop.name == otherProp.name && - prop.value.toString() == otherProp.value.toString() + prop.value.toString() == otherProp.value.toString() } } diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/css/StylePropertyValue.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/StylePropertyValue.kt similarity index 70% rename from web/core/src/jsMain/kotlin/androidx/compose/web/css/StylePropertyValue.kt rename to web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/StylePropertyValue.kt index cc9218f988..0e508d8877 100644 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/css/StylePropertyValue.kt +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/StylePropertyValue.kt @@ -14,12 +14,6 @@ external interface StylePropertyString: StylePropertyValue inline fun StylePropertyValue(value: String): StylePropertyString = value.unsafeCast() inline fun StylePropertyValue(value: Number): StylePropertyNumber = value.unsafeCast() -fun StylePropertyValue.asString(): String? = if (jsTypeOf(this) == "string") this.unsafeCast() else null - -fun StylePropertyValue.asNumber(): Number? = if (jsTypeOf(this) == "number") this.unsafeCast() else null - -fun StylePropertyValue.asCSSStyleValue(): CSSStyleValue? = if (jsTypeOf(this) == "object") this.unsafeCast() else null - external interface CSSStyleValue: StylePropertyValue { override fun toString(): String } diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/css/StyleSheet.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/StyleSheet.kt similarity index 64% rename from web/core/src/jsMain/kotlin/androidx/compose/web/css/StyleSheet.kt rename to web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/StyleSheet.kt index a6b7c64b0e..80a90a51c8 100644 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/css/StyleSheet.kt +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/StyleSheet.kt @@ -14,6 +14,7 @@ class CSSRulesHolderState : CSSRulesHolder { override var cssRules: CSSRuleDeclarationList by mutableStateOf(listOf()) override fun add(cssRule: CSSRuleDeclaration) { + @Suppress("SuspiciousCollectionReassignment") cssRules += cssRule } } @@ -39,17 +40,45 @@ class CSSRulesHolderState : CSSRulesHolder { * ``` */ open class StyleSheet( - private val rulesHolder: CSSRulesHolder = CSSRulesHolderState() + private val rulesHolder: CSSRulesHolder = CSSRulesHolderState(), + val usePrefix: Boolean = true, ) : StyleSheetBuilder, CSSRulesHolder by rulesHolder { private val boundClasses = mutableMapOf() - protected fun style(cssRule: CSSBuilder.() -> Unit) = CSSHolder(cssRule) + protected fun style(cssRule: CSSBuilder.() -> Unit) = CSSHolder(usePrefix, cssRule) + + /** + * Example: + * ``` + * object AppStyleSheet : StyleSheet() { + * val bounce by keyframes { + * from { + * property("transform", "translateX(50%)") + * } + * + * to { + * property("transform", "translateX(-50%)") + * } + * } + * + * val myClass by style { + * animation(bounce) { + * duration(2.s) + * timingFunction(AnimationTimingFunction.EaseIn) + * direction(AnimationDirection.Alternate) + * } + * } + * } + * ``` + */ + protected fun keyframes(cssKeyframes: CSSKeyframesBuilder.() -> Unit) = CSSKeyframesHolder(usePrefix, cssKeyframes) companion object { var counter = 0 } - data class CSSSelfSelector(var selector: CSSSelector? = null) : CSSSelector() { + @Suppress("EqualsOrHashCode") + class CSSSelfSelector(var selector: CSSSelector? = null) : CSSSelector() { override fun toString(): String = selector.toString() override fun equals(other: Any?): Boolean { return other is CSSSelfSelector @@ -62,8 +91,8 @@ open class StyleSheet( val (style, newCssRules) = buildCSS(selfSelector, selfSelector, cssBuild) val cssRule = cssRules.find { it is CSSStyleRuleDeclaration && - it.selector is CSSSelector.CSSClass && it.style == style && - (boundClasses[it.selector.className] ?: emptyList()) == newCssRules + it.selector is CSSSelector.CSSClass && it.style == style && + (boundClasses[it.selector.className] ?: emptyList()) == newCssRules }.unsafeCast() return if (cssRule != null) { cssRule.selector.unsafeCast().className @@ -77,12 +106,12 @@ open class StyleSheet( } } - protected class CSSHolder(val cssBuilder: CSSBuilder.() -> Unit) { + protected class CSSHolder(private val usePrefix: Boolean, private val cssBuilder: CSSBuilder.() -> Unit) { operator fun provideDelegate( sheet: StyleSheet, property: KProperty<*> ): ReadOnlyProperty { - val sheetName = "${sheet::class.simpleName}-" + val sheetName = if (usePrefix) "${sheet::class.simpleName}-" else "" val selector = className("$sheetName${property.name}") val (properties, rules) = buildCSS(selector, selector, cssBuilder) sheet.add(selector, properties) @@ -94,6 +123,28 @@ open class StyleSheet( } } + /** + * See [keyframes] + */ + protected class CSSKeyframesHolder( + private val usePrefix: Boolean, + private val keyframesBuilder: CSSKeyframesBuilder.() -> Unit + ) { + operator fun provideDelegate( + sheet: StyleSheet, + property: KProperty<*> + ): ReadOnlyProperty { + val sheetName = if (usePrefix) "${sheet::class.simpleName}-" else "" + val keyframesName = "$sheetName${property.name}" + val rule = buildKeyframes(keyframesName, keyframesBuilder) + sheet.add(rule) + + return ReadOnlyProperty { _, _ -> + rule + } + } + } + override fun buildRules(rulesBuild: GenericStyleSheetBuilder.() -> Unit) = StyleSheet().apply(rulesBuild).cssRules } diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/css/StyleSheetBuilder.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/StyleSheetBuilder.kt similarity index 100% rename from web/core/src/jsMain/kotlin/androidx/compose/web/css/StyleSheetBuilder.kt rename to web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/StyleSheetBuilder.kt diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/css/keywords/Keywords.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/keywords/Keywords.kt similarity index 100% rename from web/core/src/jsMain/kotlin/androidx/compose/web/css/keywords/Keywords.kt rename to web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/keywords/Keywords.kt diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/animation.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/animation.kt new file mode 100644 index 0000000000..9043714f2c --- /dev/null +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/animation.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.web.css + +@Suppress("EqualsOrHashCode") +data class CSSAnimation( + val keyframesName: String, + var duration: List>? = null, + var timingFunction: List? = null, + var delay: List>? = null, + var iterationCount: List? = null, + var direction: List? = null, + var fillMode: List? = null, + var playState: List? = null +) : CSSStyleValue { + override fun toString(): String { + val values = listOfNotNull( + keyframesName, + duration?.joinToString(", "), + timingFunction?.joinToString(", "), + delay?.joinToString(", "), + iterationCount?.joinToString(", ") { it?.toString() ?: "infinite" }, + direction?.joinToString(", "), + fillMode?.joinToString(", "), + playState?.joinToString(", ") + ) + return values.joinToString(" ") + } +} + +inline fun CSSAnimation.duration(vararg values: CSSSizeValue) { + this.duration = values.toList() +} + +inline fun CSSAnimation.timingFunction(vararg values: AnimationTimingFunction) { + this.timingFunction = values.toList() +} + +inline fun CSSAnimation.delay(vararg values: CSSSizeValue) { + this.delay = values.toList() +} + +inline fun CSSAnimation.iterationCount(vararg values: Int?) { + this.iterationCount = values.toList() +} + +inline fun CSSAnimation.direction(vararg values: AnimationDirection) { + this.direction = values.toList() +} + +inline fun CSSAnimation.fillMode(vararg values: AnimationFillMode) { + this.fillMode = values.toList() +} + +inline fun CSSAnimation.playState(vararg values: AnimationPlayState) { + this.playState = values.toList() +} + +fun StyleBuilder.animation( + keyframesName: String, + builder: CSSAnimation.() -> Unit +) { + val animation = CSSAnimation(keyframesName).apply(builder) + property("animation", animation) +} + +inline fun StyleBuilder.animation( + keyframes: CSSNamedKeyframes, + noinline builder: CSSAnimation.() -> Unit +) = animation(keyframes.name, builder) + + diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/background.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/background.kt new file mode 100644 index 0000000000..5e1285e335 --- /dev/null +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/background.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.web.css + +// https://developer.mozilla.org/en-US/docs/Web/CSS/background-attachment +fun StyleBuilder.backgroundAttachment(value: String) { + property("background-attachment", value) +} + +fun StyleBuilder.backgroundClip(value: String) { + property("background-clip", value) +} + +fun StyleBuilder.backgroundColor(value: CSSColorValue) { + property("background-color", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/background-image +fun StyleBuilder.backgroundImage(value: String) { + property("background-image", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/background-origin +fun StyleBuilder.backgroundOrigin(value: String) { + property("background-origin", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/background-position +fun StyleBuilder.backgroundPosition(value: String) { + property("background-position", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/background-repeat +fun StyleBuilder.backgroundRepeat(value: String) { + property("background-repeat", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/background-size +fun StyleBuilder.backgroundSize(value: String) { + property("background-size", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/background +fun StyleBuilder.background(value: String) { + property("background", value) +} + diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/border.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/border.kt new file mode 100644 index 0000000000..c8d74432e8 --- /dev/null +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/border.kt @@ -0,0 +1,108 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.web.css + +@Suppress("EqualsOrHashCode") +class CSSBorder : CSSStyleValue { + var width: CSSNumeric? = null + var style: LineStyle? = null + var color: CSSColorValue? = null + + override fun equals(other: Any?): Boolean { + return if (other is CSSBorder) { + width == other.width && style == other.style && color == other.color + } else false + } + + override fun toString(): String { + val values = listOfNotNull(width, style, color) + return values.joinToString(" ") + } +} + +inline fun CSSBorder.width(size: CSSNumeric) { + width = size +} + +inline fun CSSBorder.style(style: LineStyle) { + this.style = style +} + +inline fun CSSBorder.color(color: CSSColorValue) { + this.color = color +} + +inline fun StyleBuilder.border(crossinline borderBuild: CSSBorder.() -> Unit) { + property("border", CSSBorder().apply(borderBuild)) +} + +fun StyleBuilder.border( + width: CSSLengthValue? = null, + style: LineStyle? = null, + color: CSSColorValue? = null +) { + border { + width?.let { width(it) } + style?.let { style(it) } + color?.let { color(it) } + } +} + +fun StyleBuilder.borderRadius(r: CSSNumeric) { + property("border-radius", r) +} + +fun StyleBuilder.borderRadius(topLeft: CSSNumeric, bottomRight: CSSNumeric) { + property("border-radius", "$topLeft $bottomRight") +} + +fun StyleBuilder.borderRadius( + topLeft: CSSNumeric, + topRightAndBottomLeft: CSSNumeric, + bottomRight: CSSNumeric +) { + property("border-radius", "$topLeft $topRightAndBottomLeft $bottomRight") +} + +fun StyleBuilder.borderRadius( + topLeft: CSSNumeric, + topRight: CSSNumeric, + bottomRight: CSSNumeric, + bottomLeft: CSSNumeric +) { + property( + "border-radius", + "$topLeft $topRight $bottomRight $bottomLeft" + ) +} + +fun StyleBuilder.borderWidth(width: CSSNumeric) { + property("border-width", width) +} + +fun StyleBuilder.borderWidth(topLeft: CSSNumeric, bottomRight: CSSNumeric) { + property("border-width", "$topLeft $bottomRight") +} + +fun StyleBuilder.borderWidth( + topLeft: CSSNumeric, + topRightAndBottomLeft: CSSNumeric, + bottomRight: CSSNumeric +) { + property("border-width", "$topLeft $topRightAndBottomLeft $bottomRight") +} + +fun StyleBuilder.borderWidth( + topLeft: CSSNumeric, + topRight: CSSNumeric, + bottomRight: CSSNumeric, + bottomLeft: CSSNumeric +) { + property( + "border-width", + "$topLeft $topRight $bottomRight $bottomLeft" + ) +} diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/box.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/box.kt new file mode 100644 index 0000000000..6f0c896fff --- /dev/null +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/box.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.web.css + +import org.jetbrains.compose.web.css.keywords.CSSAutoKeyword + +fun StyleBuilder.width(value: CSSNumeric) { + property("width", value) +} + +fun StyleBuilder.width(value: CSSAutoKeyword) { + property("width", value) +} + +fun StyleBuilder.height(value: CSSNumeric) { + property("height", value) +} + +fun StyleBuilder.height(value: CSSAutoKeyword) { + property("height", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/box-sizing +fun StyleBuilder.boxSizing(value: String) { + property("box-sizing", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/outline-width +fun StyleBuilder.outlineWidth(value: String) { + property("outline-width", value) +} + +fun StyleBuilder.outlineWidth(value: CSSNumeric) { + property("outline-width", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/outline-color +fun StyleBuilder.outlineColor(value: CSSColorValue) { + property("outline-color", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/outline-style +fun StyleBuilder.outlineStyle(value: String) { + property("outline-style", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/outline +fun StyleBuilder.outline(style: String) { + property("outline", style) +} + +fun StyleBuilder.outline(colorOrStyle: String, styleOrWidth: String) { + property("outline", "$colorOrStyle $styleOrWidth") +} + +fun StyleBuilder.outline(style: String, width: CSSNumeric) { + property("outline", "$style $width") +} + +fun StyleBuilder.outline(color: CSSColorValue, style: String, width: String) { + property("outline", "$color $style $width") +} + +fun StyleBuilder.outline(color: CSSColorValue, style: String, width: CSSNumeric) { + property("outline", "$color $style $width") +} + +fun StyleBuilder.outline(color: String, style: String, width: String) { + property("outline", "$color $style $width") +} + +fun StyleBuilder.outline(color: String, style: String, width: CSSNumeric) { + property("outline", "$color $style $width") +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/min-width +fun StyleBuilder.minWidth(value: String) { + property("min-width", value) +} + +fun StyleBuilder.minWidth(value: CSSNumeric) { + property("min-width", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/max-width +fun StyleBuilder.maxWidth(value: String) { + property("max-width", value) +} + +fun StyleBuilder.maxWidth(value: CSSNumeric) { + property("max-width", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/min-height +fun StyleBuilder.minHeight(value: String) { + property("min-height", value) +} + +fun StyleBuilder.minHeight(value: CSSNumeric) { + property("min-height", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/max-height +fun StyleBuilder.maxHeight(value: String) { + property("max-height", value) +} + +fun StyleBuilder.maxHeight(value: CSSNumeric) { + property("max-height", value) +} \ No newline at end of file diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/color.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/color.kt new file mode 100644 index 0000000000..bbaf5a36dc --- /dev/null +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/color.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.web.css + +fun StyleBuilder.color(value: CSSColorValue) { + // color hasn't Typed OM yet + property("color", value) +} \ No newline at end of file diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/flex.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/flex.kt new file mode 100644 index 0000000000..0ef2eb9aa2 --- /dev/null +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/flex.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.web.css + +import org.w3c.dom.css.CSS + +fun StyleBuilder.flexDirection(flexDirection: FlexDirection) { + property("flex-direction", flexDirection.value) +} + +fun StyleBuilder.flexWrap(flexWrap: FlexWrap) { + property("flex-wrap", flexWrap.value) +} + +fun StyleBuilder.flexFlow(flexDirection: FlexDirection, flexWrap: FlexWrap) { + property( + "flex-flow", + "${flexDirection.value} ${flexWrap.value}" + ) +} + +fun StyleBuilder.justifyContent(justifyContent: JustifyContent) { + property( + "justify-content", + justifyContent.value + ) +} +fun StyleBuilder.alignSelf(alignSelf: AlignSelf) { + property( + "align-self", + alignSelf.value + ) +} + +fun StyleBuilder.alignItems(alignItems: AlignItems) { + property( + "align-items", + alignItems.value + ) +} + +fun StyleBuilder.alignContent(alignContent: AlignContent) { + property( + "align-content", + alignContent.value + ) +} + +fun StyleBuilder.order(value: Int) { + property("order", value) +} + +fun StyleBuilder.flexGrow(value: Number) { + property("flex-grow", value) +} + +fun StyleBuilder.flexShrink(value: Number) { + property("flex-shrink", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/flex-basis +fun StyleBuilder.flexBasis(value: String) { + property("flex-basis", value) +} + +fun StyleBuilder.flexBasis(value: CSSNumeric) { + property("flex-basis", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/flex +fun StyleBuilder.flex(value: String) { + property("flex", value) +} + +fun StyleBuilder.flex(value: Int) { + property("flex", value) +} + +fun StyleBuilder.flex(value: CSSNumeric) { + property("flex", value) +} + +fun StyleBuilder.flex(flexGrow: Int, flexBasis: CSSNumeric) { + property("flex", "${flexGrow} ${flexBasis}") +} + +fun StyleBuilder.flex(flexGrow: Int, flexShrink: Int) { + property("flex", "${flexGrow} ${flexShrink}") +} + +fun StyleBuilder.flex(flexGrow: Int, flexShrink: Int, flexBasis: CSSNumeric) { + property("flex", "${flexGrow} ${flexShrink} ${flexBasis}") +} + diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/grid.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/grid.kt new file mode 100644 index 0000000000..c064bceadc --- /dev/null +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/grid.kt @@ -0,0 +1,179 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.web.css + +// https://developer.mozilla.org/en-US/docs/Web/CSS/grid-column +fun StyleBuilder.gridColumn(value: String) { + property("grid-column", value) +} + +fun StyleBuilder.gridColumn(start: String, end: String) { + property("grid-column", "$start / $end") +} + +fun StyleBuilder.gridColumn(start: String, end: Int) { + property("grid-column", "$start / $end") +} + +fun StyleBuilder.gridColumn(start: Int, end: String) { + property("grid-column", "$start / $end") +} + +fun StyleBuilder.gridColumn(start: Int, end: Int) { + property("grid-column", "$start / $end") +} + + +// https://developer.mozilla.org/en-US/docs/Web/CSS/grid-column-start +fun StyleBuilder.gridColumnStart(value: String) { + property("grid-column-start", value) +} + +fun StyleBuilder.gridColumnStart(value: Int) { + property("grid-column-start", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/grid-column-end +fun StyleBuilder.gridColumnEnd(value: String) { + property("grid-column-end", value) +} + +fun StyleBuilder.gridColumnEnd(value: Int) { + property("grid-column-end", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/grid-row +fun StyleBuilder.gridRow(value: String) { + property("grid-row", value) +} + +fun StyleBuilder.gridRow(start: String, end: String) { + property("grid-row", "$start / $end") +} + +fun StyleBuilder.gridRow(start: String, end: Int) { + property("grid-row", "$start / $end") +} + +fun StyleBuilder.gridRow(start: Int, end: String) { + property("grid-row", "$start / $end") +} + +fun StyleBuilder.gridRow(start: Int, end: Int) { + property("grid-row", "$start / $end") +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/grid-row-start +fun StyleBuilder.gridRowStart(value: String) { + property("grid-row-start", value) +} + +fun StyleBuilder.gridRowStart(value: Int) { + property("grid-row-start", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/grid-row-end +fun StyleBuilder.gridRowEnd(value: String) { + property("grid-row-end", value) +} + +fun StyleBuilder.gridRowEnd(value: Int) { + property("grid-row-end", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template-columns +fun StyleBuilder.gridTemplateColumns(value: String) { + property("grid-template-columns", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/grid-auto-columns +fun StyleBuilder.gridAutoColumns(value: String) { + property("grid-auto-columns", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/grid-auto-flow +fun StyleBuilder.gridAutoFlow(value: GridAutoFlow) { + property("grid-auto-flow", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template-rows +fun StyleBuilder.gridTemplateRows(value: String) { + property("grid-template-rows", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/grid-auto-rows +fun StyleBuilder.gridAutoRows(value: String) { + property("grid-auto-rows", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/grid-area +fun StyleBuilder.gridArea(rowStart: String) { + property("grid-area", rowStart) +} + +fun StyleBuilder.gridArea(rowStart: String, columnStart: String) { + property("grid-area", "$rowStart / $columnStart") +} + +fun StyleBuilder.gridArea(rowStart: String, columnStart: String, rowEnd: String) { + property("grid-area", "$rowStart / $columnStart / $rowEnd") +} + +fun StyleBuilder.gridArea(rowStart: String, columnStart: String, rowEnd: String, columnEnd: String) { + property("grid-area", "$rowStart / $columnStart / $rowEnd / $columnEnd") +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template-areas +fun StyleBuilder.gridTemplateAreas(vararg rows: String) { + property("grid-template-areas", rows.joinToString(" ") { "\"$it\"" }) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/justify-self +fun StyleBuilder.justifySelf(value: String) { + property("justify-self", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/justify-items +fun StyleBuilder.justifyItems(value: String) { + property("justify-items", value) +} + + +// https://developer.mozilla.org/en-US/docs/Web/CSS/align-self +fun StyleBuilder.alignSelf(value: String) { + property("align-self", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/align-items +fun StyleBuilder.alignItems(value: String) { + property("align-items", value) +} + + +// https://developer.mozilla.org/en-US/docs/Web/CSS/place-self +fun StyleBuilder.placeSelf(value: String) { + property("place-self", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/row-gap +fun StyleBuilder.rowGap(value: CSSNumeric) { + property("row-gap", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/column-gap +fun StyleBuilder.columnGap(value: CSSNumeric) { + property("column-gap", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/gap +fun StyleBuilder.gap(value: CSSNumeric) { + property("gap", value) +} + +fun StyleBuilder.gap(rowGap: CSSNumeric, columnGap: CSSNumeric) { + property("gap", "$rowGap $columnGap") +} + diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/listStyle.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/listStyle.kt new file mode 100644 index 0000000000..8541881d5b --- /dev/null +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/listStyle.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.web.css + +// https://developer.mozilla.org/en-US/docs/Web/CSS/list-style-image +fun StyleBuilder.listStyleImage(value: String) { + property("list-style-image", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/list-style-position +fun StyleBuilder.listStylePosition(value: String) { + property("list-style-position", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/list-style-type +fun StyleBuilder.listStyleType(value: String) { + property("list-style-type", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/list-style +fun StyleBuilder.listStyle(value: String) { + property("list-style", value) +} + diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/margin.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/margin.kt new file mode 100644 index 0000000000..59390ac83d --- /dev/null +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/margin.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.web.css + +// https://developer.mozilla.org/en-US/docs/Web/CSS/margin +fun StyleBuilder.margin(vararg value: CSSNumeric) { + // margin hasn't Typed OM yet + property("margin", value.joinToString(" ")) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/margin-bottom +fun StyleBuilder.marginBottom(value: CSSNumeric) { + property("margin-bottom", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/margin-left +fun StyleBuilder.marginLeft(value: CSSNumeric) { + property("margin-left", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/margin-right +fun StyleBuilder.marginRight(value: CSSNumeric) { + property("margin-right", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/margin-top +fun StyleBuilder.marginTop(value: CSSNumeric) { + property("margin-top", value) +} + + + diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/overflow.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/overflow.kt new file mode 100644 index 0000000000..9785d00f2e --- /dev/null +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/overflow.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.web.css + +// https://developer.mozilla.org/en-US/docs/Web/CSS/overflow-x +fun StyleBuilder.overflowX(value: String) { + property("overflow-x", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/overflow-y +fun StyleBuilder.overflowY(value: String) { + property("overflow-y", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/overflow +fun StyleBuilder.overflow(value: String) { + property("overflow", value) +} + + diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/padding.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/padding.kt new file mode 100644 index 0000000000..312900bab6 --- /dev/null +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/padding.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.web.css + +// https://developer.mozilla.org/en-US/docs/Web/CSS/padding +fun StyleBuilder.padding(vararg value: CSSNumeric) { + // padding hasn't Typed OM yet + property("padding", value.joinToString(" ")) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/padding-bottom +fun StyleBuilder.paddingBottom(value: CSSNumeric) { + property("padding-bottom", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/padding-left +fun StyleBuilder.paddingLeft(value: CSSNumeric) { + property("padding-left", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/padding-right +fun StyleBuilder.paddingRight(value: CSSNumeric) { + property("padding-right", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/padding-top +fun StyleBuilder.paddingTop(value: CSSNumeric) { + property("padding-top", value) +} \ No newline at end of file diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/position.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/position.kt new file mode 100644 index 0000000000..7005b983b7 --- /dev/null +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/position.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.web.css + +import org.jetbrains.compose.web.css.keywords.CSSAutoKeyword + +fun StyleBuilder.position(position: Position) { + property( + "position", + position.value + ) +} + +fun StyleBuilder.top(value: CSSLengthOrPercentageValue) { + property("top", value) +} + +fun StyleBuilder.top(value: CSSAutoKeyword) { + property("top", value) +} + +fun StyleBuilder.bottom(value: CSSLengthOrPercentageValue) { + property("bottom", value) +} + +fun StyleBuilder.bottom(value: CSSAutoKeyword) { + property("bottom", value) +} + +fun StyleBuilder.left(value: CSSLengthOrPercentageValue) { + property("left", value) +} + +fun StyleBuilder.left(value: CSSAutoKeyword) { + property("left", value) +} + +fun StyleBuilder.right(value: CSSLengthOrPercentageValue) { + property("right", value) +} + +fun StyleBuilder.right(value: CSSAutoKeyword) { + property("right", value) +} + diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/properties.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/properties.kt new file mode 100644 index 0000000000..3439d1ca88 --- /dev/null +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/properties.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.web.css + +fun StyleBuilder.opacity(value: Number) { + property("opacity", value) +} + +fun StyleBuilder.opacity(value: CSSSizeValue) { + property("opacity", (value.value / 100)) +} + +fun StyleBuilder.display(displayStyle: DisplayStyle) { + property("display", displayStyle.value) +} + diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/text.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/text.kt new file mode 100644 index 0000000000..3b16bfb47c --- /dev/null +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/text.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.web.css + +// https://developer.mozilla.org/en-US/docs/Web/CSS/font-family +fun StyleBuilder.fontFamily(vararg value: String) { + property("font-family", value.joinToString(", ") { if (it.contains(" ")) "\"$it\"" else it }) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/font-size +fun StyleBuilder.fontSize(value: CSSNumeric) { + property("font-size", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/font-style +fun StyleBuilder.fontStyle(value: String) { + property("font-style", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight +fun StyleBuilder.fontWeight(value: String) { + property("font-weight", value) +} + +fun StyleBuilder.fontWeight(value: Int) { + property("font-weight", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/line-height +fun StyleBuilder.lineHeight(value: String) { + property("line-height", value) +} + +fun StyleBuilder.lineHeight(value: CSSNumeric) { + property("line-height", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/font +fun StyleBuilder.font(value: String) { + property("font", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/letter-spacing +fun StyleBuilder.letterSpacing(value: String) { + property("letter-spacing", value) +} + +fun StyleBuilder.letterSpacing(value: CSSNumeric) { + property("letter-spacing", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/text-align +fun StyleBuilder.textAlign(value: String) { + property("text-align", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/text-decoration-color +fun StyleBuilder.textDecorationColor(value: CSSColorValue) { + property("text-decoration-color", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/text-decoration-style +fun StyleBuilder.textDecorationStyle(value: String) { + property("text-decoration-style", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/text-decoration-thickness +fun StyleBuilder.textDecorationThickness(value: String) { + property("text-decoration-thickness", value) +} + +fun StyleBuilder.textDecorationThickness(value: CSSNumeric) { + property("text-decoration-thickness", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/text-decoration-line +fun StyleBuilder.textDecorationLine(value: String) { + property("text-decoration-line", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/text-decoration +fun StyleBuilder.textDecoration(value: String) { + property("text-decoration", value) +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/white-space +fun StyleBuilder.whiteSpace(value: String) { + property("white-space", value) +} \ No newline at end of file diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/ui.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/ui.kt new file mode 100644 index 0000000000..8d713e991f --- /dev/null +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/properties/ui.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.web.css + +// https://developer.mozilla.org/en-US/docs/Web/CSS/cursor +fun StyleBuilder.cursor(vararg value: String) { + property("cursor", value.joinToString(", ")) +} \ No newline at end of file diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/css/selectors/CSSSelectors.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/selectors/CSSSelectors.kt similarity index 100% rename from web/core/src/jsMain/kotlin/androidx/compose/web/css/selectors/CSSSelectors.kt rename to web/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/selectors/CSSSelectors.kt diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Base.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Base.kt new file mode 100644 index 0000000000..005b486ab3 --- /dev/null +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Base.kt @@ -0,0 +1,146 @@ +package org.jetbrains.compose.web.dom + +import androidx.compose.runtime.Applier +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ComposeCompilerApi +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.DisposableEffectResult +import androidx.compose.runtime.DisposableEffectScope +import androidx.compose.runtime.ExplicitGroupsComposable +import androidx.compose.runtime.SkippableUpdater +import androidx.compose.runtime.currentComposer +import androidx.compose.runtime.remember +import org.jetbrains.compose.web.DomApplier +import org.jetbrains.compose.web.DomElementWrapper +import org.jetbrains.compose.web.attributes.AttrsBuilder +import kotlinx.browser.document +import org.w3c.dom.Audio +import org.w3c.dom.Element +import org.w3c.dom.HTMLAnchorElement +import org.w3c.dom.HTMLAreaElement +import org.w3c.dom.HTMLAudioElement +import org.w3c.dom.HTMLBRElement +import org.w3c.dom.HTMLButtonElement +import org.w3c.dom.HTMLDataListElement +import org.w3c.dom.HTMLDivElement +import org.w3c.dom.HTMLElement +import org.w3c.dom.HTMLEmbedElement +import org.w3c.dom.HTMLFieldSetElement +import org.w3c.dom.HTMLFormElement +import org.w3c.dom.HTMLHRElement +import org.w3c.dom.HTMLHeadingElement +import org.w3c.dom.HTMLIFrameElement +import org.w3c.dom.HTMLImageElement +import org.w3c.dom.HTMLInputElement +import org.w3c.dom.HTMLLIElement +import org.w3c.dom.HTMLLabelElement +import org.w3c.dom.HTMLLegendElement +import org.w3c.dom.HTMLMapElement +import org.w3c.dom.HTMLMeterElement +import org.w3c.dom.HTMLOListElement +import org.w3c.dom.HTMLObjectElement +import org.w3c.dom.HTMLOptGroupElement +import org.w3c.dom.HTMLOptionElement +import org.w3c.dom.HTMLOutputElement +import org.w3c.dom.HTMLParagraphElement +import org.w3c.dom.HTMLParamElement +import org.w3c.dom.HTMLPictureElement +import org.w3c.dom.HTMLPreElement +import org.w3c.dom.HTMLProgressElement +import org.w3c.dom.HTMLSelectElement +import org.w3c.dom.HTMLSourceElement +import org.w3c.dom.HTMLSpanElement +import org.w3c.dom.HTMLStyleElement +import org.w3c.dom.HTMLTableCaptionElement +import org.w3c.dom.HTMLTableCellElement +import org.w3c.dom.HTMLTableColElement +import org.w3c.dom.HTMLTableElement +import org.w3c.dom.HTMLTableRowElement +import org.w3c.dom.HTMLTableSectionElement +import org.w3c.dom.HTMLTextAreaElement +import org.w3c.dom.HTMLTrackElement +import org.w3c.dom.HTMLUListElement +import org.w3c.dom.HTMLVideoElement + +@OptIn(ComposeCompilerApi::class) +@Composable +@ExplicitGroupsComposable +inline fun > ComposeDomNode( + noinline factory: () -> T, + elementScope: TScope, + noinline attrsSkippableUpdate: @Composable SkippableUpdater.() -> Unit, + noinline content: (@Composable TScope.() -> Unit)? +) { + if (currentComposer.applier !is E) error("Invalid applier") + currentComposer.startNode() + if (currentComposer.inserting) { + currentComposer.createNode(factory) + } else { + currentComposer.useNode() + } + + SkippableUpdater(currentComposer).apply { + attrsSkippableUpdate() + } + + currentComposer.startReplaceableGroup(0x7ab4aae9) + content?.invoke(elementScope) + currentComposer.endReplaceableGroup() + currentComposer.endNode() +} + +class DisposableEffectHolder( + var effect: (DisposableEffectScope.(TElement) -> DisposableEffectResult)? = null +) + +@Composable +fun TagElement( + elementBuilder: ElementBuilder, + applyAttrs: (AttrsBuilder.() -> Unit)?, + content: (@Composable ElementScope.() -> Unit)? +) { + val scope = remember { ElementScopeImpl() } + val refEffect = remember { DisposableEffectHolder() } + + ComposeDomNode, DomElementWrapper, DomApplier>( + factory = { + DomElementWrapper(elementBuilder.create() as HTMLElement).also { + scope.element = it.node.unsafeCast() + } + }, + attrsSkippableUpdate = { + val attrsApplied = AttrsBuilder().also { + if (applyAttrs != null) { + it.applyAttrs() + } + } + refEffect.effect = attrsApplied.refEffect + val attrsCollected = attrsApplied.collect() + val events = attrsApplied.collectListeners() + + update { + set(attrsCollected, DomElementWrapper::updateAttrs) + set(events, DomElementWrapper::updateEventListeners) + set(attrsApplied.propertyUpdates, DomElementWrapper::updateProperties) + set(attrsApplied.styleBuilder, DomElementWrapper::updateStyleDeclarations) + } + }, + elementScope = scope, + content = content + ) + + DisposableEffect(null) { + refEffect.effect?.invoke(this, scope.element) ?: onDispose {} + } +} + +@Composable +fun TagElement( + tagName: String, + applyAttrs: AttrsBuilder.() -> Unit, + content: (@Composable ElementScope.() -> Unit)? +) = TagElement( + elementBuilder = ElementBuilder.createBuilder(tagName), + applyAttrs = applyAttrs, + content = content +) diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/elements/ElementScope.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/ElementScope.kt similarity index 65% rename from web/core/src/jsMain/kotlin/androidx/compose/web/elements/ElementScope.kt rename to web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/ElementScope.kt index 1343bc5342..c13281068c 100644 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/elements/ElementScope.kt +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/ElementScope.kt @@ -15,8 +15,27 @@ import org.w3c.dom.HTMLElement interface DOMScope +/** + * ElementScope allows adding effects to the Composable representing html element. + * Also see a tutorial: https://github.com/JetBrains/compose-jb/tree/master/tutorials/Web/Using_Effects + * + * Example: + * ``` + * Div { + * DisposableRefEffect { htmlDivElement -> + * onDispose {} + * } + * } + * ``` + */ interface ElementScope : DOMScope { + /** + * A side effect of composition that must run for any new unique value of [key] + * and must be reversed or cleaned up if [key] changes or if the DisposableRefEffect leaves the composition. + * [effect] lambda provides a reference to a native element represented by Composable. + * Adding [DisposableEffectScope.onDispose] to [effect] is mandatory. + */ @Composable @NonRestartableComposable fun DisposableRefEffect( @@ -24,6 +43,12 @@ interface ElementScope : DOMScope { effect: DisposableEffectScope.(TElement) -> DisposableEffectResult ) + /** + * A side effect of composition that must run once an element enters composition + * and must be reversed or cleaned up if element or the DisposableRefEffect leaves the composition. + * [effect] lambda provides a reference to a native element represented by Composable. + * Adding [DisposableEffectScope.onDispose] to [effect] is mandatory. + */ @Composable @NonRestartableComposable fun DisposableRefEffect( @@ -32,10 +57,20 @@ interface ElementScope : DOMScope { DisposableRefEffect(null, effect) } + /** + * A side effect of composition that runs on every successful recomposition if [key] changes. + * Also see [SideEffect]. + * Same as other effects in [ElementScope], it provides a reference to a native element in [effect] lambda. + */ @Composable @NonRestartableComposable fun DomSideEffect(key: Any?, effect: DomEffectScope.(TElement) -> Unit) + /** + * A side effect of composition that runs on every successful recomposition. + * Also see [SideEffect]. + * Same as other effects in [ElementScope], it provides a reference to a native element in [effect] lambda. + */ @Composable @NonRestartableComposable fun DomSideEffect(effect: DomEffectScope.(TElement) -> Unit) @@ -102,4 +137,4 @@ private class DomDisposableEffectHolder( override fun onDispose(disposeEffect: (Element) -> Unit) { onDispose = disposeEffect } -} \ No newline at end of file +} diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/elements/Elements.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Elements.kt similarity index 55% rename from web/core/src/jsMain/kotlin/androidx/compose/web/elements/Elements.kt rename to web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Elements.kt index 2ba3370443..d532d46907 100644 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/elements/Elements.kt +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Elements.kt @@ -2,12 +2,17 @@ package org.jetbrains.compose.web.dom import androidx.compose.runtime.Composable import androidx.compose.runtime.ComposeNode -import androidx.compose.web.attributes.InputAttrsBuilder -import androidx.compose.web.attributes.TextAreaAttrsBuilder +import org.jetbrains.compose.web.attributes.builders.InputAttrsBuilder +import androidx.compose.web.attributes.SelectAttrsBuilder +import org.jetbrains.compose.web.attributes.builders.TextAreaAttrsBuilder import org.jetbrains.compose.web.DomApplier import org.jetbrains.compose.web.DomNodeWrapper import kotlinx.browser.document import org.jetbrains.compose.web.attributes.* +import org.jetbrains.compose.web.css.CSSRuleDeclarationList +import org.jetbrains.compose.web.css.StyleSheetBuilder +import org.jetbrains.compose.web.css.StyleSheetBuilderImpl +import org.w3c.dom.Element import org.w3c.dom.HTMLAnchorElement import org.w3c.dom.HTMLAreaElement import org.w3c.dom.HTMLAudioElement @@ -23,6 +28,7 @@ import org.w3c.dom.HTMLHeadingElement import org.w3c.dom.HTMLHRElement import org.w3c.dom.HTMLIFrameElement import org.w3c.dom.HTMLImageElement +import org.w3c.dom.HTMLInputElement import org.w3c.dom.HTMLLIElement import org.w3c.dom.HTMLLabelElement import org.w3c.dom.HTMLLegendElement @@ -41,27 +47,128 @@ import org.w3c.dom.HTMLProgressElement import org.w3c.dom.HTMLSelectElement import org.w3c.dom.HTMLSourceElement import org.w3c.dom.HTMLSpanElement +import org.w3c.dom.HTMLStyleElement import org.w3c.dom.HTMLTableCaptionElement import org.w3c.dom.HTMLTableCellElement import org.w3c.dom.HTMLTableColElement import org.w3c.dom.HTMLTableElement import org.w3c.dom.HTMLTableRowElement import org.w3c.dom.HTMLTableSectionElement +import org.w3c.dom.HTMLTextAreaElement import org.w3c.dom.HTMLTrackElement import org.w3c.dom.HTMLUListElement import org.w3c.dom.HTMLVideoElement import org.w3c.dom.Text +import org.w3c.dom.css.CSSStyleSheet typealias AttrBuilderContext = AttrsBuilder.() -> Unit typealias ContentBuilder = @Composable ElementScope.() -> Unit +private open class ElementBuilderImplementation(private val tagName: String) : ElementBuilder { + private val el: Element by lazy { document.createElement(tagName) } + override fun create(): TElement = el.cloneNode() as TElement +} + +private val Address: ElementBuilder = ElementBuilderImplementation("address") +private val Article: ElementBuilder = ElementBuilderImplementation("article") +private val Aside: ElementBuilder = ElementBuilderImplementation("aside") +private val Header: ElementBuilder = ElementBuilderImplementation("header") + +private val Area: ElementBuilder = ElementBuilderImplementation("area") +private val Audio: ElementBuilder = ElementBuilderImplementation("audio") +private val Map: ElementBuilder = ElementBuilderImplementation("map") +private val Track: ElementBuilder = ElementBuilderImplementation("track") +private val Video: ElementBuilder = ElementBuilderImplementation("video") + +private val Datalist: ElementBuilder = ElementBuilderImplementation("datalist") +private val Fieldset: ElementBuilder = ElementBuilderImplementation("fieldset") +private val Legend: ElementBuilder = ElementBuilderImplementation("legend") +private val Meter: ElementBuilder = ElementBuilderImplementation("meter") +private val Output: ElementBuilder = ElementBuilderImplementation("output") +private val Progress: ElementBuilder = ElementBuilderImplementation("progress") + +private val Embed: ElementBuilder = ElementBuilderImplementation("embed") +private val Iframe: ElementBuilder = ElementBuilderImplementation("iframe") +private val Object: ElementBuilder = ElementBuilderImplementation("object") +private val Param: ElementBuilder = ElementBuilderImplementation("param") +private val Picture: ElementBuilder = ElementBuilderImplementation("picture") +private val Source: ElementBuilder = ElementBuilderImplementation("source") + +private val Div: ElementBuilder = ElementBuilderImplementation("div") +private val A: ElementBuilder = ElementBuilderImplementation("a") +private val Input: ElementBuilder = ElementBuilderImplementation("input") +private val Button: ElementBuilder = ElementBuilderImplementation("button") + +private val H1: ElementBuilder = ElementBuilderImplementation("h1") +private val H2: ElementBuilder = ElementBuilderImplementation("h2") +private val H3: ElementBuilder = ElementBuilderImplementation("h3") +private val H4: ElementBuilder = ElementBuilderImplementation("h4") +private val H5: ElementBuilder = ElementBuilderImplementation("h5") +private val H6: ElementBuilder = ElementBuilderImplementation("h6") + +private val P: ElementBuilder = ElementBuilderImplementation("p") + +private val Em: ElementBuilder = ElementBuilderImplementation("em") +private val I: ElementBuilder = ElementBuilderImplementation("i") +private val B: ElementBuilder = ElementBuilderImplementation("b") +private val Small: ElementBuilder = ElementBuilderImplementation("small") + +private val Span: ElementBuilder = ElementBuilderImplementation("span") + +private val Br: ElementBuilder = ElementBuilderImplementation("br") + +private val Ul: ElementBuilder = ElementBuilderImplementation("ul") +private val Ol: ElementBuilder = ElementBuilderImplementation("ol") + +private val Li: ElementBuilder = ElementBuilderImplementation("li") + +private val Img: ElementBuilder = ElementBuilderImplementation("img") +private val Form: ElementBuilder = ElementBuilderImplementation("form") + +private val Select: ElementBuilder = ElementBuilderImplementation("select") +private val Option: ElementBuilder = ElementBuilderImplementation("option") +private val OptGroup: ElementBuilder = ElementBuilderImplementation("optgroup") + +private val Section: ElementBuilder = ElementBuilderImplementation("section") +private val TextArea: ElementBuilder = ElementBuilderImplementation("textarea") +private val Nav: ElementBuilder = ElementBuilderImplementation("nav") +private val Pre: ElementBuilder = ElementBuilderImplementation("pre") +private val Code: ElementBuilder = ElementBuilderImplementation("code") + +private val Main: ElementBuilder = ElementBuilderImplementation("main") +private val Footer: ElementBuilder = ElementBuilderImplementation("footer") +private val Hr: ElementBuilder = ElementBuilderImplementation("hr") +private val Label: ElementBuilder = ElementBuilderImplementation("label") +private val Table: ElementBuilder = ElementBuilderImplementation("table") +private val Caption: ElementBuilder = ElementBuilderImplementation("caption") +private val Col: ElementBuilder = ElementBuilderImplementation("col") +private val Colgroup: ElementBuilder = ElementBuilderImplementation("colgroup") +private val Tr: ElementBuilder = ElementBuilderImplementation("tr") +private val Thead: ElementBuilder = ElementBuilderImplementation("thead") +private val Th: ElementBuilder = ElementBuilderImplementation("th") +private val Td: ElementBuilder = ElementBuilderImplementation("td") +private val Tbody: ElementBuilder = ElementBuilderImplementation("tbody") +private val Tfoot: ElementBuilder = ElementBuilderImplementation("tfoot") + +val Style: ElementBuilder = ElementBuilderImplementation("style") + +fun interface ElementBuilder { + fun create(): TElement + + companion object { + fun createBuilder(tagName: String): ElementBuilder { + return object : ElementBuilderImplementation(tagName) {} + } + } +} + @Composable fun Address( attrs: AttrBuilderContext? = null, content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.Address, + elementBuilder = Address, applyAttrs = attrs, content = content ) @@ -73,7 +180,7 @@ fun Article( content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.Article, + elementBuilder = Article, applyAttrs = attrs, content = content ) @@ -85,7 +192,7 @@ fun Aside( content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.Aside, + elementBuilder = Aside, applyAttrs = attrs, content = content ) @@ -97,7 +204,7 @@ fun Header( content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.Header, + elementBuilder = Header, applyAttrs = attrs, content = content ) @@ -109,7 +216,7 @@ fun Area( content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.Area, + elementBuilder = Area, applyAttrs = attrs, content = content ) @@ -121,7 +228,7 @@ fun Audio( content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.Audio, + elementBuilder = Audio, applyAttrs = attrs, content = content ) @@ -133,7 +240,7 @@ fun HTMLMap( content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.Map, + elementBuilder = Map, applyAttrs = attrs, content = content ) @@ -145,7 +252,7 @@ fun Track( content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.Track, + elementBuilder = Track, applyAttrs = attrs, content = content ) @@ -157,7 +264,7 @@ fun Video( content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.Video, + elementBuilder = Video, applyAttrs = attrs, content = content ) @@ -169,7 +276,7 @@ fun Datalist( content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.Datalist, + elementBuilder = Datalist, applyAttrs = attrs, content = content ) @@ -181,7 +288,7 @@ fun Fieldset( content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.Fieldset, + elementBuilder = Fieldset, applyAttrs = attrs, content = content ) @@ -193,7 +300,7 @@ fun Legend( content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.Legend, + elementBuilder = Legend, applyAttrs = attrs, content = content ) @@ -205,7 +312,7 @@ fun Meter( content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.Meter, + elementBuilder = Meter, applyAttrs = attrs, content = content ) @@ -217,7 +324,7 @@ fun Output( content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.Output, + elementBuilder = Output, applyAttrs = attrs, content = content ) @@ -229,7 +336,7 @@ fun Progress( content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.Progress, + elementBuilder = Progress, applyAttrs = attrs, content = content ) @@ -241,7 +348,7 @@ fun Embed( content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.Embed, + elementBuilder = Embed, applyAttrs = attrs, content = content ) @@ -253,7 +360,7 @@ fun Iframe( content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.Iframe, + elementBuilder = Iframe, applyAttrs = attrs, content = content ) @@ -265,7 +372,7 @@ fun Object( content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.Object, + elementBuilder = Object, applyAttrs = attrs, content = content ) @@ -277,7 +384,7 @@ fun Param( content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.Param, + elementBuilder = Param, applyAttrs = attrs, content = content ) @@ -289,7 +396,7 @@ fun Picture( content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.Picture, + elementBuilder = Picture, applyAttrs = attrs, content = content ) @@ -301,7 +408,7 @@ fun Source( content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.Source, + elementBuilder = Source, applyAttrs = attrs, content = content ) @@ -323,7 +430,7 @@ fun Div( content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.Div, + elementBuilder = Div, applyAttrs = attrs, content = content ) @@ -336,7 +443,7 @@ fun A( content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.A, + elementBuilder = A, applyAttrs = { if (href != null) { this.href(href) @@ -353,107 +460,101 @@ fun A( fun Button( attrs: AttrBuilderContext? = null, content: ContentBuilder? = null -) = TagElement(elementBuilder = ElementBuilder.Button, applyAttrs = attrs, content = content) +) = TagElement(elementBuilder = Button, applyAttrs = attrs, content = content) @Composable fun H1( attrs: AttrBuilderContext? = null, content: ContentBuilder? = null -) = TagElement(elementBuilder = ElementBuilder.H1, applyAttrs = attrs, content = content) +) = TagElement(elementBuilder = H1, applyAttrs = attrs, content = content) @Composable fun H2( attrs: AttrBuilderContext? = null, content: ContentBuilder? = null -) = TagElement(elementBuilder = ElementBuilder.H2, applyAttrs = attrs, content = content) +) = TagElement(elementBuilder = H2, applyAttrs = attrs, content = content) @Composable fun H3( attrs: AttrBuilderContext? = null, content: ContentBuilder? = null -) = TagElement(elementBuilder = ElementBuilder.H3, applyAttrs = attrs, content = content) +) = TagElement(elementBuilder = H3, applyAttrs = attrs, content = content) @Composable fun H4( attrs: AttrBuilderContext? = null, content: ContentBuilder? = null -) = TagElement(elementBuilder = ElementBuilder.H4, applyAttrs = attrs, content = content) +) = TagElement(elementBuilder = H4, applyAttrs = attrs, content = content) @Composable fun H5( attrs: AttrBuilderContext? = null, content: ContentBuilder? = null -) = TagElement(elementBuilder = ElementBuilder.H5, applyAttrs = attrs, content = content) +) = TagElement(elementBuilder = H5, applyAttrs = attrs, content = content) @Composable fun H6( attrs: AttrBuilderContext? = null, content: ContentBuilder? = null -) = TagElement(elementBuilder = ElementBuilder.H6, applyAttrs = attrs, content = content) +) = TagElement(elementBuilder = H6, applyAttrs = attrs, content = content) @Composable fun P( attrs: AttrBuilderContext? = null, content: ContentBuilder? = null -) = TagElement(elementBuilder = ElementBuilder.P, applyAttrs = attrs, content = content) +) = TagElement(elementBuilder = P, applyAttrs = attrs, content = content) @Composable fun Em( attrs: AttrBuilderContext? = null, content: ContentBuilder? = null -) = TagElement(elementBuilder = ElementBuilder.Em, applyAttrs = attrs, content = content) +) = TagElement(elementBuilder = Em, applyAttrs = attrs, content = content) @Composable fun I( attrs: AttrBuilderContext? = null, content: ContentBuilder? = null -) = TagElement(elementBuilder = ElementBuilder.I, applyAttrs = attrs, content = content) +) = TagElement(elementBuilder = I, applyAttrs = attrs, content = content) @Composable fun B( attrs: AttrBuilderContext? = null, content: ContentBuilder? = null -) = TagElement(elementBuilder = ElementBuilder.B, applyAttrs = attrs, content = content) +) = TagElement(elementBuilder = B, applyAttrs = attrs, content = content) @Composable fun Small( attrs: AttrBuilderContext? = null, content: ContentBuilder? = null -) = TagElement(elementBuilder = ElementBuilder.Small, applyAttrs = attrs, content = content) +) = TagElement(elementBuilder = Small, applyAttrs = attrs, content = content) @Composable fun Span( attrs: AttrBuilderContext? = null, content: ContentBuilder? = null -) = TagElement(elementBuilder = ElementBuilder.Span, applyAttrs = attrs, content = content) +) = TagElement(elementBuilder = Span, applyAttrs = attrs, content = content) @Composable fun Br(attrs: AttrBuilderContext? = null) = - TagElement(elementBuilder = ElementBuilder.Br, applyAttrs = attrs, content = null) + TagElement(elementBuilder = Br, applyAttrs = attrs, content = null) @Composable fun Ul( attrs: AttrBuilderContext? = null, content: ContentBuilder? = null -) = TagElement(elementBuilder = ElementBuilder.Ul, applyAttrs = attrs, content = content) +) = TagElement(elementBuilder = Ul, applyAttrs = attrs, content = content) @Composable fun Ol( attrs: AttrBuilderContext? = null, content: ContentBuilder? = null -) = TagElement(elementBuilder = ElementBuilder.Ol, applyAttrs = attrs, content = content) - -@Composable -fun DOMScope.Li( - attrs: AttrBuilderContext? = null, - content: ContentBuilder? = null -) = TagElement(elementBuilder = ElementBuilder.Li, applyAttrs = attrs, content = content) +) = TagElement(elementBuilder = Ol, applyAttrs = attrs, content = content) @Composable -fun DOMScope.Li( +fun Li( attrs: AttrBuilderContext? = null, content: ContentBuilder? = null -) = TagElement(elementBuilder = ElementBuilder.Li, applyAttrs = attrs, content = content) +) = TagElement(elementBuilder = Li, applyAttrs = attrs, content = content) @Composable fun Img( @@ -461,7 +562,7 @@ fun Img( alt: String = "", attrs: AttrBuilderContext? = null ) = TagElement( - elementBuilder = ElementBuilder.Img, + elementBuilder = Img, applyAttrs = { src(src).alt(alt) if (attrs != null) { @@ -477,7 +578,7 @@ fun Form( attrs: AttrBuilderContext? = null, content: ContentBuilder? = null ) = TagElement( - elementBuilder = ElementBuilder.Form, + elementBuilder = Form, applyAttrs = { if (!action.isNullOrEmpty()) action(action) if (attrs != null) { @@ -489,11 +590,21 @@ fun Form( @Composable fun Select( - attrs: AttrBuilderContext? = null, + attrs: (SelectAttrsBuilder.() -> Unit)? = null, + multiple: Boolean = false, content: ContentBuilder? = null ) = TagElement( - elementBuilder = ElementBuilder.Select, - applyAttrs = attrs, + elementBuilder = Select, + applyAttrs = { + if (multiple) multiple() + if (attrs != null) { + val selectAttrsBuilder = with(SelectAttrsBuilder()) { + attrs() + this + } + copyFrom(selectAttrsBuilder) + } + }, content = content ) @@ -503,7 +614,7 @@ fun Option( attrs: AttrBuilderContext? = null, content: ContentBuilder? = null ) = TagElement( - elementBuilder = ElementBuilder.Option, + elementBuilder = Option, applyAttrs = { value(value) if (attrs != null) { @@ -519,7 +630,7 @@ fun OptGroup( attrs: AttrBuilderContext? = null, content: ContentBuilder? = null ) = TagElement( - elementBuilder = ElementBuilder.OptGroup, + elementBuilder = OptGroup, applyAttrs = { label(label) if (attrs != null) { @@ -534,7 +645,7 @@ fun Section( attrs: AttrBuilderContext? = null, content: ContentBuilder? = null ) = TagElement( - elementBuilder = ElementBuilder.Section, + elementBuilder = Section, applyAttrs = attrs, content = content ) @@ -544,7 +655,7 @@ fun TextArea( attrs: (TextAreaAttrsBuilder.() -> Unit)? = null, value: String ) = TagElement( - elementBuilder = ElementBuilder.TextArea, + elementBuilder = TextArea, applyAttrs = { val taab = TextAreaAttrsBuilder() if (attrs != null) { @@ -562,7 +673,7 @@ fun Nav( attrs: AttrBuilderContext? = null, content: ContentBuilder? = null ) = TagElement( - elementBuilder = ElementBuilder.Nav, + elementBuilder = Nav, applyAttrs = attrs, content = content ) @@ -573,7 +684,7 @@ fun Pre( content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.Pre, + elementBuilder = Pre, applyAttrs = attrs, content = content ) @@ -585,7 +696,7 @@ fun Code( content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.Code, + elementBuilder = Code, applyAttrs = attrs, content = content ) @@ -597,7 +708,7 @@ fun Main( content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.Main, + elementBuilder = Main, applyAttrs = attrs, content = content ) @@ -609,7 +720,7 @@ fun Footer( content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.Footer, + elementBuilder = Footer, applyAttrs = attrs, content = content ) @@ -620,7 +731,7 @@ fun Hr( attrs: AttrBuilderContext? = null ) { TagElement( - elementBuilder = ElementBuilder.Hr, + elementBuilder = Hr, applyAttrs = attrs, content = null ) @@ -633,7 +744,7 @@ fun Label( content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.Label, + elementBuilder = Label, applyAttrs = { if (forId != null) { forId(forId) @@ -652,7 +763,7 @@ fun Table( content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.Table, + elementBuilder = Table, applyAttrs = attrs, content = content ) @@ -664,7 +775,7 @@ fun Caption( content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.Caption, + elementBuilder = Caption, applyAttrs = attrs, content = content ) @@ -675,7 +786,7 @@ fun Col( attrs: AttrBuilderContext? = null ) { TagElement( - elementBuilder = ElementBuilder.Col, + elementBuilder = Col, applyAttrs = attrs, content = null ) @@ -687,7 +798,7 @@ fun Colgroup( content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.Colgroup, + elementBuilder = Colgroup, applyAttrs = attrs, content = content ) @@ -699,7 +810,7 @@ fun Tr( content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.Tr, + elementBuilder = Tr, applyAttrs = attrs, content = content ) @@ -711,7 +822,7 @@ fun Thead( content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.Thead, + elementBuilder = Thead, applyAttrs = attrs, content = content ) @@ -723,7 +834,7 @@ fun Th( content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.Th, + elementBuilder = Th, applyAttrs = attrs, content = content ) @@ -735,7 +846,7 @@ fun Td( content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.Td, + elementBuilder = Td, applyAttrs = attrs, content = content ) @@ -747,7 +858,7 @@ fun Tbody( content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.Tbody, + elementBuilder = Tbody, applyAttrs = attrs, content = content ) @@ -759,8 +870,83 @@ fun Tfoot( content: ContentBuilder? = null ) { TagElement( - elementBuilder = ElementBuilder.Tfoot, + elementBuilder = Tfoot, applyAttrs = attrs, content = content ) } + +/** + * Use this function to mount the