Browse Source

Merge remote-tracking branch 'origin/master'

pull/1051/head
akurasov 3 years ago
parent
commit
7b81c5f51f
  1. 52
      CHANGELOG.md
  2. 32
      FEATURES.md
  3. 33
      README.md
  4. 0
      artwork/compose-logo.xml
  5. 4
      cef/build.gradle.kts
  6. 2
      cef/gradle/wrapper/gradle-wrapper.properties
  7. 4
      ci/compose-uber-jar/build.gradle.kts
  8. 2
      ci/compose-uber-jar/gradle.properties
  9. 2
      ci/compose-uber-jar/gradle/wrapper/gradle-wrapper.properties
  10. 34
      components/SplitPane/demo/src/jvmMain/kotlin/org/jetbrains/compose/splitpane/demo/Main.kt
  11. 4
      components/SplitPane/library/src/commonMain/kotlin/org/jetbrains/compose/splitpane/SplitPaneState.kt
  12. 36
      components/SplitPane/library/src/desktopMain/kotlin/org/jetbrains/compose/splitpane/DesktopSplitter.kt
  13. 2
      components/build.gradle.kts
  14. 2
      components/gradle.properties
  15. 2
      components/gradle/wrapper/gradle-wrapper.properties
  16. 7
      compose/README.md
  17. 14
      compose/build.gradle.kts
  18. 2
      compose/frameworks/support
  19. 2
      compose/golden
  20. 2
      compose/prebuilts/androidx/internal
  21. 8
      compose/scripts/buildNativeDemo
  22. 8
      compose/scripts/testRuntimeNative
  23. 4
      examples/codeviewer/build.gradle.kts
  24. 5
      examples/codeviewer/common/src/commonMain/kotlin/org/jetbrains/codeviewer/ui/editor/Editor.kt
  25. 21
      examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Mouse.kt
  26. 4
      examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Theme.kt
  27. 28
      examples/codeviewer/desktop/src/jvmMain/kotlin/org/jetbrains/codeviewer/main.kt
  28. 7
      examples/falling-balls-web/build.gradle.kts
  29. 0
      examples/falling-balls-web/gradle/wrapper/gradle-wrapper.jar
  30. 2
      examples/falling-balls-web/gradle/wrapper/gradle-wrapper.properties
  31. 0
      examples/falling-balls-web/gradlew
  32. 0
      examples/falling-balls-web/gradlew.bat
  33. 1
      examples/falling-balls-web/settings.gradle.kts
  34. 0
      examples/falling-balls-web/src/commonMain/kotlin/fallingBalls/Game.kt
  35. 0
      examples/falling-balls-web/src/commonMain/kotlin/fallingBalls/Piece.kt
  36. 0
      examples/falling-balls-web/src/commonMain/kotlin/fallingBalls/PieceData.kt
  37. 0
      examples/falling-balls-web/src/commonMain/kotlin/fallingBalls/fallingBalls.kt
  38. 0
      examples/falling-balls-web/src/commonMain/kotlin/modifiers/position.kt
  39. 0
      examples/falling-balls-web/src/jsMain/kotlin/androidx/compose/web/with-web/App.kt
  40. 0
      examples/falling-balls-web/src/jsMain/kotlin/modifiers/position.kt
  41. 0
      examples/falling-balls-web/src/jsMain/resources/index.html
  42. 0
      examples/falling-balls-web/src/jsMain/resources/styles.css
  43. 0
      examples/falling-balls-web/src/jvmMain/kotlin/App.kt
  44. 0
      examples/falling-balls-web/src/jvmMain/kotlin/modifiers/position.kt
  45. 0
      examples/falling-balls/.gitignore
  46. 7
      examples/falling-balls/build.gradle.kts
  47. 0
      examples/falling-balls/gradle.properties
  48. 0
      examples/falling-balls/gradle/wrapper/gradle-wrapper.jar
  49. 0
      examples/falling-balls/gradle/wrapper/gradle-wrapper.properties
  50. 0
      examples/falling-balls/gradlew
  51. 0
      examples/falling-balls/gradlew.bat
  52. 0
      examples/falling-balls/settings.gradle.kts
  53. 4
      examples/falling-balls/src/main/kotlin/Game.kt
  54. 0
      examples/falling-balls/src/main/kotlin/Piece.kt
  55. 15
      examples/falling-balls/src/main/kotlin/main.kt
  56. 10
      examples/falling_balls/src/main/kotlin/main.kt
  57. 8
      examples/imageviewer/android/build.gradle.kts
  58. 4
      examples/imageviewer/android/src/main/java/example/imageviewer/MainActivity.kt
  59. 13
      examples/imageviewer/build.gradle.kts
  60. 10
      examples/imageviewer/common/build.gradle.kts
  61. 40
      examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/AndroidContentState.kt
  62. 77
      examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/utils/GraphicsMath.kt
  63. 8
      examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/AppUI.kt
  64. 114
      examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/FullscreenImage.kt
  65. 73
      examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/MainScreen.kt
  66. 4
      examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/model/ScreenType.kt
  67. 2
      examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Draggable.kt
  68. 43
      examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/LoadingScreen.kt
  69. 4
      examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Scalable.kt
  70. 6
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/R.kt
  71. 161
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/DesktopContentState.kt
  72. 44
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/style/Decoration.kt
  73. 158
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/Application.kt
  74. 108
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/GraphicsMath.kt
  75. 8
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/AppUI.kt
  76. 314
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/FullImageScreen.kt
  77. 225
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/FullscreenImage.kt
  78. 119
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/MainScreen.kt
  79. 35
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Tooltip.kt
  80. 44
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Zoomable.kt
  81. 2
      examples/imageviewer/desktop/build.gradle.kts
  82. 41
      examples/imageviewer/desktop/src/jvmMain/kotlin/example/imageviewer/Main.kt
  83. 2
      examples/imageviewer/gradle/wrapper/gradle-wrapper.properties
  84. 45
      examples/intelliJPlugin/src/main/kotlin/com/jetbrains/compose/ComposeSizeAdjustmentWrapper.kt
  85. 0
      examples/intellij-plugin/.gitignore
  86. 0
      examples/intellij-plugin/README.md
  87. 19
      examples/intellij-plugin/build.gradle.kts
  88. 0
      examples/intellij-plugin/gradle.properties
  89. 0
      examples/intellij-plugin/gradle/wrapper/gradle-wrapper.jar
  90. 2
      examples/intellij-plugin/gradle/wrapper/gradle-wrapper.properties
  91. 0
      examples/intellij-plugin/gradlew
  92. 0
      examples/intellij-plugin/gradlew.bat
  93. 0
      examples/intellij-plugin/screenshots/screenshot.png
  94. 0
      examples/intellij-plugin/screenshots/toolsshow.png
  95. 0
      examples/intellij-plugin/settings.gradle.kts
  96. 47
      examples/intellij-plugin/src/main/kotlin/com/jetbrains/compose/ComposeDemoAction.kt
  97. 0
      examples/intellij-plugin/src/main/kotlin/com/jetbrains/compose/theme/Color.kt
  98. 0
      examples/intellij-plugin/src/main/kotlin/com/jetbrains/compose/theme/Shape.kt
  99. 0
      examples/intellij-plugin/src/main/kotlin/com/jetbrains/compose/theme/Theme.kt
  100. 0
      examples/intellij-plugin/src/main/kotlin/com/jetbrains/compose/theme/Type.kt
  101. Some files were not shown because too many files have changed in this diff Show More

52
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) # M4 (Jun 2021)
* New experimental [Composable Window API](https://github.com/JetBrains/compose-jb/tree/master/tutorials/Window_API_new) * 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) * [Tooltips](https://github.com/JetBrains/compose-jb/tree/master/tutorials/Desktop_Components#tooltips)

32
FEATURES.md

@ -1,25 +1,40 @@
## Features ## Features
Features currently available in Compose for Desktop ### Supported platforms
* [Scrollbars support](tutorials/Scrollbars/README.md) * 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) * [Image loading support](tutorials/Image_And_Icons_Manipulations/README.md)
* [Keyboard handling](tutorials/Keyboard/README.md) * [Keyboard handling](tutorials/Keyboard/README.md)
* [Mouse clicks and move](tutorials/Mouse_Events/README.md) * [Mouse clicks and move](tutorials/Mouse_Events/README.md)
* [Packaging to native distributions](tutorials/Native_distributions_and_local_execution/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) * [Signing and notarization](tutorials/Signing_and_notarization_on_macOS/README.md)
* [Window properties handling](tutorials/Window_API/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. Follow individual tutorials to understand how to use particular feature.
### Limitations ### 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 * 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__) [comment]: <> (__SUPPORTED_GRADLE_VERSIONS__)
### Gradle plugin compatibility ### 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; * 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). * 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). * 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).

33
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 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) [![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) ![](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. 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 ## ## Repository organization ##
* [artwork](artwork) - design artifacts * [artwork](artwork) - design artifacts
* [benchmarks](benchmarks) - collection of benchmarks * [benchmarks](benchmarks) - collection of benchmarks
* [compose](compose) - composite build of [Compose-jb sources](https://github.com/JetBrains/androidx) * [compose](compose) - composite build of [Compose-jb sources](https://github.com/JetBrains/androidx)
* [ci](ci) - Continuous Integration helpers * [ci](ci) - Continuous Integration helpers
* [cef](cef) - CEF integration in Jetpack Compose * [cef](cef) - CEF integration in Jetpack Compose (somewhat outdated)
* [examples](examples) - examples of multiplatform Compose applications for Desktop and Android * [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 * [codeviewer](examples/codeviewer) - File Browser and Code Viewer application for Android and Desktop
* [imageviewer](examples/imageviewer) - Image 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 * [issues](examples/issues) - GitHub issue tracker with an adaptive UI and ktor-client
* [game](examples/falling_balls) - Simple game * [game](examples/falling-balls) - Simple game
* [game](examples/falling_balls_with_web) - Simple game for web target * [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 * [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 * [notepad](examples/notepad) - Notepad, using the new experimental Composable Window API
* [todoapp](examples/todoapp) - TODO items tracker with persistence and multiple screens * [todoapp](examples/todoapp) - TODO items tracker with persistence and multiple screens
* [widgetsgallery](examples/widgetsgallery) - Gallery of standard widgets * [widgets gallery](examples/widgetsgallery) - Gallery of standard widgets
* [IDEA plugin](examples/intelliJPlugin) - Plugin for IDEA using Compose for Desktop * [IDEA plugin](examples/intellij-plugin) - Plugin for IDEA using Compose for Desktop
* [gradle-plugins](gradle-plugins) - a plugin, simplifying usage of Compose with Gradle * [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) * [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) * [Getting started](tutorials/Getting_Started)
* [Image and icon manipulations](tutorials/Image_And_Icons_Manipulations) * [Image and icon manipulations](tutorials/Image_And_Icons_Manipulations)
* [Mouse events and hover](tutorials/Mouse_Events) * [Mouse events and hover](tutorials/Mouse_Events)
* [Scrolling and scrollbars](tutorials/Desktop_Components) * [Scrolling and scrollbars](tutorials/Desktop_Components)
* [Tooltips](tutorials/Desktop_Components#tooltips) * [Tooltips](tutorials/Desktop_Components#tooltips)
* [Top level windows management](tutorials/Window_API) * [Top level windows management](tutorials/Window_API_new)
* [Top level windows management (new Composable API, experimental)](tutorials/Window_API_new) * [Menu, tray, notifications](tutorials/Tray_Notifications_MenuBar_new)
* [Menu, tray, notifications](tutorials/Tray_Notifications_MenuBar)
* [Menu, tray, notifications (new Composable API, experimental)](tutorials/Tray_Notifications_MenuBar_new)
* [Keyboard support](tutorials/Keyboard) * [Keyboard support](tutorials/Keyboard)
* [Tab focus navigation](tutorials/Tab_Navigation)
* [Building native distribution](tutorials/Native_distributions_and_local_execution) * [Building native distribution](tutorials/Native_distributions_and_local_execution)
* [Signing and notarization](tutorials/Signing_and_notarization_on_macOS) * [Signing and notarization](tutorials/Signing_and_notarization_on_macOS)
* [Swing interoperability](tutorials/Swing_Integration) * [Swing interoperability](tutorials/Swing_Integration)
* [Navigation](tutorials/Navigation) * [Navigation](tutorials/Navigation)
* [components](components) - custom components of Compose for Desktop * [components](components) - custom components of Compose Multiplatform
* [Video Player](components/VideoPlayer) * [Video Player](components/VideoPlayer)
* [Split Pane](components/SplitPane) * [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. See https://github.com/JetBrains/compose-jb/tags for the latest build number.

0
tutorials/Image_And_Icons_Manipulations/compose-logo.xml → artwork/compose-logo.xml

4
cef/build.gradle.kts

@ -5,9 +5,9 @@ import kotlin.text.capitalize
plugins { plugins {
// __KOTLIN_COMPOSE_VERSION__ // __KOTLIN_COMPOSE_VERSION__
kotlin("jvm") version "1.4.20" kotlin("jvm") version "1.5.21"
// __LATEST_COMPOSE_RELEASE_VERSION__ // __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" id("de.undercouch.download") version "4.1.1"
application application
} }

2
cef/gradle/wrapper/gradle-wrapper.properties vendored

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

4
ci/compose-uber-jar/build.gradle.kts

@ -11,6 +11,7 @@ val properties = ComposeUberJarProperties()
repositories { repositories {
mavenCentral() mavenCentral()
maven(properties.composeRepoUrl) maven(properties.composeRepoUrl)
google()
} }
val composeVersion: String by lazy { val composeVersion: String by lazy {
@ -22,6 +23,7 @@ val composeVersion: String by lazy {
} }
dependencies { dependencies {
implementation("org.jetbrains.compose.desktop:desktop-jvm-macos-x64:$composeVersion")
implementation("org.jetbrains.compose.desktop:desktop-jvm-linux-x64:$composeVersion") implementation("org.jetbrains.compose.desktop:desktop-jvm-linux-x64:$composeVersion")
} }
@ -81,4 +83,4 @@ class ComposeUberJarProperties {
inline fun <reified T> Project.typedProperty(name: String): T? = inline fun <reified T> Project.typedProperty(name: String): T? =
project.findProperty(name) as? T project.findProperty(name) as? T
} }
} }

2
ci/compose-uber-jar/gradle.properties

@ -1,3 +1,3 @@
# __LATEST_COMPOSE_RELEASE_VERSION__ # __LATEST_COMPOSE_RELEASE_VERSION__
compose.version=0.4.0 compose.version=1.0.0-alpha1
kotlin.code.style=official kotlin.code.style=official

2
ci/compose-uber-jar/gradle/wrapper/gradle-wrapper.properties vendored

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

34
components/SplitPane/demo/src/jvmMain/kotlin/org/jetbrains/compose/splitpane/demo/Main.kt

@ -1,49 +1,33 @@
package org.jetbrains.compose.splitpane.demo package org.jetbrains.compose.splitpane.demo
import androidx.compose.desktop.DesktopTheme import androidx.compose.desktop.DesktopTheme
import androidx.compose.desktop.LocalAppWindow
import androidx.compose.desktop.Window
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.getValue import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor 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.unit.dp
import androidx.compose.ui.window.singleWindowApplication
import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi
import org.jetbrains.compose.splitpane.HorizontalSplitPane import org.jetbrains.compose.splitpane.HorizontalSplitPane
import org.jetbrains.compose.splitpane.VerticalSplitPane import org.jetbrains.compose.splitpane.VerticalSplitPane
import org.jetbrains.compose.splitpane.rememberSplitPaneState import org.jetbrains.compose.splitpane.rememberSplitPaneState
import java.awt.Cursor import java.awt.Cursor
private fun Modifier.cursorForHorizontalResize( @OptIn(ExperimentalComposeUiApi::class)
): Modifier = composed { private fun Modifier.cursorForHorizontalResize(): Modifier =
var isHover by remember { mutableStateOf(false) } pointerIcon(PointerIcon(Cursor(Cursor.E_RESIZE_CURSOR)))
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(ExperimentalSplitPaneApi::class) @OptIn(ExperimentalSplitPaneApi::class)
fun main() = Window( fun main() = singleWindowApplication(
"SplitPane demo" title = "SplitPane demo"
) { ) {
MaterialTheme { MaterialTheme {
DesktopTheme { DesktopTheme {

4
components/SplitPane/library/src/commonMain/kotlin/org/jetbrains/compose/splitpane/SplitPaneState.kt

@ -1,9 +1,5 @@
package org.jetbrains.compose.splitpane 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.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue

36
components/SplitPane/library/src/desktopMain/kotlin/org/jetbrains/compose/splitpane/DesktopSplitter.kt

@ -1,45 +1,23 @@
package org.jetbrains.compose.splitpane package org.jetbrains.compose.splitpane
import androidx.compose.desktop.LocalAppWindow
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.*
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.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.graphics.Color 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.consumeAllChanges
import androidx.compose.ui.input.pointer.pointerIcon
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.pointerMoveFilter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import java.awt.Cursor import java.awt.Cursor
private fun Modifier.cursorForHorizontalResize( @OptIn(ExperimentalComposeUiApi::class)
isHorizontal: Boolean private fun Modifier.cursorForHorizontalResize(isHorizontal: Boolean): Modifier =
): Modifier = composed { pointerIcon(PointerIcon(Cursor(if (isHorizontal) Cursor.E_RESIZE_CURSOR else Cursor.S_RESIZE_CURSOR)))
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 }
)
}
@Composable @Composable
private fun DesktopSplitPaneSeparator( private fun DesktopSplitPaneSeparator(

2
components/build.gradle.kts

@ -10,7 +10,7 @@ buildscript {
dependencies { dependencies {
classpath("org.jetbrains.compose:compose-gradle-plugin:$composeVersion") classpath("org.jetbrains.compose:compose-gradle-plugin:$composeVersion")
// __KOTLIN_COMPOSE_VERSION__ // __KOTLIN_COMPOSE_VERSION__
classpath(kotlin("gradle-plugin", version = "1.5.10")) classpath(kotlin("gradle-plugin", version = "1.5.21"))
} }
} }

2
components/gradle.properties

@ -4,4 +4,4 @@ android.enableJetifier=true
kotlin.code.style=official kotlin.code.style=official
# __LATEST_COMPOSE_RELEASE_VERSION__ # __LATEST_COMPOSE_RELEASE_VERSION__
compose.version=0.4.0 compose.version=1.0.0-alpha1

2
components/gradle/wrapper/gradle-wrapper.properties vendored

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

7
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: ## 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: 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) - CMake 3.10.2.4988404 (in folder $androidSdk/cmake, not in $androidSdk/cmake/$version)
## Requirements to develop in IDE ## 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) - 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` - Specified Gradle JDK 11 in `... -> Build Tools -> Gradle`
- Environment variables: - Environment variables:
@ -35,6 +35,7 @@ export COMPOSE_CUSTOM_GROUP=org.jetbrains.compose
androidx.compose.multiplatformEnabled=true androidx.compose.multiplatformEnabled=true
androidx.compose.jsCompilerTestsEnabled=true androidx.compose.jsCompilerTestsEnabled=true
``` ```
(note that https://android.googlesource.com/platform/frameworks/support build doesn't work with androidx.compose.jsCompilerTestsEnabled)
## Scripts ## Scripts
Publish artifacts to the local directory `out/androidx/build/support_repo/org/jetbrains/compose`: 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: Run tests for Web:
``` ```
./scripts/testWeb ./scripts/testWeb
``` ```

14
compose/build.gradle.kts

@ -37,6 +37,7 @@ tasks.register("publishComposeJb") {
":compose:ui:ui-test-junit4", ":compose:ui:ui-test-junit4",
":compose:ui:ui-text", ":compose:ui:ui-text",
":compose:ui:ui-tooling", ":compose:ui:ui-tooling",
":compose:ui:ui-tooling-preview",
":compose:ui:ui-unit", ":compose:ui:ui-unit",
":compose:ui:ui-util", ":compose:ui:ui-util",
).forEach { ).forEach {
@ -70,6 +71,9 @@ tasks.register("testComposeJbDesktop") {
dependsOnComposeTask(":compose:desktop:desktop:jvmTest") dependsOnComposeTask(":compose:desktop:desktop:jvmTest")
dependsOnComposeTask(":compose:animation:animation:desktopTest") dependsOnComposeTask(":compose:animation:animation:desktopTest")
dependsOnComposeTask(":compose:animation:animation-core: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:desktopTest")
dependsOnComposeTask(":compose:foundation:foundation-layout:desktopTest") dependsOnComposeTask(":compose:foundation:foundation-layout:desktopTest")
dependsOnComposeTask(":compose:material:material:desktopTest") dependsOnComposeTask(":compose:material:material:desktopTest")
@ -81,4 +85,12 @@ tasks.register("testComposeJbDesktop") {
tasks.register("testComposeJbWeb") { tasks.register("testComposeJbWeb") {
dependsOnComposeTask(":compose:runtime:runtime:jsTest") dependsOnComposeTask(":compose:runtime:runtime:jsTest")
dependsOnComposeTask(":compose:runtime:runtime:test") dependsOnComposeTask(":compose:runtime:runtime:test")
} }
tasks.register("buildNativeDemo") {
dependsOnComposeTask(":compose:native:demo:assemble")
}
tasks.register("testRuntimeNative") {
dependsOnComposeTask(":compose:runtime:runtime:macosX64Test")
}

2
compose/frameworks/support

@ -1 +1 @@
Subproject commit 94cefabe7303d41aef797722ee3ab331a21689aa Subproject commit aadb6bb9988bd5b232b2922fa5a248b823f0d5a5

2
compose/golden

@ -1 +1 @@
Subproject commit cd6860e33655776f6533790a27cd37eb04b40e40 Subproject commit 1b20aa551446123340cb42b4eb21d2f2797e608a

2
compose/prebuilts/androidx/internal

@ -1 +1 @@
Subproject commit f37dc6b42fe7838e9e37fbe8a9eb063a1550acd8 Subproject commit 818a882ba70e8603d6a22b17d421c9049926da4c

8
compose/scripts/buildNativeDemo

@ -0,0 +1,8 @@
#!/bin/bash
cd "$(dirname "$0")"
. ./prepare
pushd ..
./gradlew buildNativeDemo $COMPOSE_DEFAULT_GRADLE_ARGS "$@" || exit 1
popd

8
compose/scripts/testRuntimeNative

@ -0,0 +1,8 @@
#!/bin/bash
cd "$(dirname "$0")"
. ./prepare
pushd ..
./gradlew testRuntimeNative $COMPOSE_DEFAULT_GRADLE_ARGS "$@" || exit 1
popd

4
examples/codeviewer/build.gradle.kts

@ -8,10 +8,10 @@ buildscript {
dependencies { dependencies {
// __LATEST_COMPOSE_RELEASE_VERSION__ // __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") classpath("com.android.tools.build:gradle:4.0.1")
// __KOTLIN_COMPOSE_VERSION__ // __KOTLIN_COMPOSE_VERSION__
classpath(kotlin("gradle-plugin", version = "1.5.10")) classpath(kotlin("gradle-plugin", version = "1.5.21"))
} }
} }

5
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 { fun content(index: Int): Editor.Content {
val text = textLines.get(index) val text = textLines.get(index)
.trim('\n') // fix for native crash in Skia. val state = mutableStateOf(text)
// 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)
return Editor.Content(state, isCode) return Editor.Content(state, isCode)
} }

21
examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Mouse.kt

@ -1,13 +1,15 @@
package org.jetbrains.codeviewer.platform package org.jetbrains.codeviewer.platform
import androidx.compose.desktop.LocalAppWindow
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.composed import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset 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 androidx.compose.ui.input.pointer.pointerMoveFilter
import java.awt.Cursor import java.awt.Cursor
@ -17,17 +19,20 @@ actual fun Modifier.pointerMoveFilter(
onMove: (Offset) -> Boolean onMove: (Offset) -> Boolean
): Modifier = this.pointerMoveFilter(onEnter = onEnter, onExit = onExit, onMove = onMove) ): Modifier = this.pointerMoveFilter(onEnter = onEnter, onExit = onExit, onMove = onMove)
@OptIn(ExperimentalComposeUiApi::class)
actual fun Modifier.cursorForHorizontalResize(): Modifier = composed { actual fun Modifier.cursorForHorizontalResize(): Modifier = composed {
var isHover by remember { mutableStateOf(false) } 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( pointerMoveFilter(
onEnter = { isHover = true; true }, onEnter = { isHover = true; true },
onExit = { isHover = false; true } onExit = { isHover = false; true }
).pointerIcon(
PointerIcon(
if (isHover) {
Cursor(Cursor.E_RESIZE_CURSOR)
} else {
Cursor.getDefaultCursor()
}
)
) )
} }

4
examples/codeviewer/common/src/desktopMain/kotlin/org/jetbrains/codeviewer/platform/Theme.kt

@ -1,7 +1,7 @@
package org.jetbrains.codeviewer.platform package org.jetbrains.codeviewer.platform
import androidx.compose.desktop.DesktopTheme import androidx.compose.desktop.DesktopMaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@Composable @Composable
actual fun PlatformTheme(content: @Composable () -> Unit) = DesktopTheme(content = content) actual fun PlatformTheme(content: @Composable () -> Unit) = DesktopMaterialTheme(content = content)

28
examples/codeviewer/desktop/src/jvmMain/kotlin/org/jetbrains/codeviewer/main.kt

@ -1,23 +1,19 @@
package org.jetbrains.codeviewer package org.jetbrains.codeviewer
import androidx.compose.desktop.Window import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.unit.IntSize 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 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", title = "Code Viewer",
size = IntSize(1280, 768), state = WindowState(width = 1280.dp, height = 768.dp),
icon = loadImageResource("ic_launcher.png"), icon = BitmapPainter(useResource("ic_launcher.png", ::loadImageBitmap)),
) { ) {
MainView() 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)
}

7
examples/falling_balls_with_web/build.gradle.kts → examples/falling-balls-web/build.gradle.kts

@ -2,8 +2,8 @@ import org.jetbrains.compose.compose
import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.compose.desktop.application.dsl.TargetFormat
plugins { plugins {
kotlin("multiplatform") version "1.5.10" kotlin("multiplatform") version "1.5.21"
id("org.jetbrains.compose") version "0.5.0-build228" id("org.jetbrains.compose") version "1.0.0-alpha1-rc5"
} }
version = "1.0-SNAPSHOT" version = "1.0-SNAPSHOT"
@ -11,6 +11,7 @@ version = "1.0-SNAPSHOT"
repositories { repositories {
mavenCentral() mavenCentral()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
google()
} }
kotlin { kotlin {
@ -53,7 +54,7 @@ compose.desktop {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "ImageViewer" packageName = "ImageViewer"
packageVersion = "1.0.0" packageVersion = "1.0.0"
modules("jdk.crypto.ec") modules("jdk.crypto.ec")
val iconsRoot = project.file("../common/src/desktopMain/resources/images") val iconsRoot = project.file("../common/src/desktopMain/resources/images")

0
examples/falling_balls/gradle/wrapper/gradle-wrapper.jar → examples/falling-balls-web/gradle/wrapper/gradle-wrapper.jar vendored

2
examples/intelliJPlugin/gradle/wrapper/gradle-wrapper.properties → examples/falling-balls-web/gradle/wrapper/gradle-wrapper.properties vendored

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

0
examples/falling_balls/gradlew → examples/falling-balls-web/gradlew vendored

0
examples/falling_balls_with_web/gradlew.bat → examples/falling-balls-web/gradlew.bat vendored

1
examples/falling_balls_with_web/settings.gradle.kts → examples/falling-balls-web/settings.gradle.kts

@ -3,6 +3,7 @@ pluginManagement {
gradlePluginPortal() gradlePluginPortal()
mavenCentral() 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") }
google()
} }
} }

0
examples/falling_balls_with_web/src/commonMain/kotlin/fallingBalls/Game.kt → examples/falling-balls-web/src/commonMain/kotlin/fallingBalls/Game.kt

0
examples/falling_balls_with_web/src/commonMain/kotlin/fallingBalls/Piece.kt → examples/falling-balls-web/src/commonMain/kotlin/fallingBalls/Piece.kt

0
examples/falling_balls_with_web/src/commonMain/kotlin/fallingBalls/PieceData.kt → examples/falling-balls-web/src/commonMain/kotlin/fallingBalls/PieceData.kt

0
examples/falling_balls_with_web/src/commonMain/kotlin/fallingBalls/fallingBalls.kt → examples/falling-balls-web/src/commonMain/kotlin/fallingBalls/fallingBalls.kt

0
examples/falling_balls_with_web/src/commonMain/kotlin/modifiers/position.kt → examples/falling-balls-web/src/commonMain/kotlin/modifiers/position.kt

0
examples/falling_balls_with_web/src/jsMain/kotlin/androidx/compose/web/with-web/App.kt → examples/falling-balls-web/src/jsMain/kotlin/androidx/compose/web/with-web/App.kt

0
examples/falling_balls_with_web/src/jsMain/kotlin/modifiers/position.kt → examples/falling-balls-web/src/jsMain/kotlin/modifiers/position.kt

0
examples/falling_balls_with_web/src/jsMain/resources/index.html → examples/falling-balls-web/src/jsMain/resources/index.html

0
examples/falling_balls_with_web/src/jsMain/resources/styles.css → examples/falling-balls-web/src/jsMain/resources/styles.css

0
examples/falling_balls_with_web/src/jvmMain/kotlin/App.kt → examples/falling-balls-web/src/jvmMain/kotlin/App.kt

0
examples/falling_balls_with_web/src/jvmMain/kotlin/modifiers/position.kt → examples/falling-balls-web/src/jvmMain/kotlin/modifiers/position.kt

0
examples/falling_balls/.gitignore → examples/falling-balls/.gitignore vendored

7
examples/falling_balls/build.gradle.kts → examples/falling-balls/build.gradle.kts

@ -4,9 +4,9 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
// __KOTLIN_COMPOSE_VERSION__ // __KOTLIN_COMPOSE_VERSION__
kotlin("jvm") version "1.5.10" kotlin("jvm") version "1.5.21"
// __LATEST_COMPOSE_RELEASE_VERSION__ // __LATEST_COMPOSE_RELEASE_VERSION__
id("org.jetbrains.compose") version "0.4.0" id("org.jetbrains.compose") version "1.0.0-alpha1"
} }
group = "me.user" group = "me.user"
@ -14,6 +14,7 @@ version = "1.0"
repositories { repositories {
mavenCentral() mavenCentral()
google()
maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") } maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") }
} }
@ -23,6 +24,8 @@ dependencies {
tasks.withType<KotlinCompile>() { tasks.withType<KotlinCompile>() {
kotlinOptions.jvmTarget = "11" kotlinOptions.jvmTarget = "11"
kotlinOptions.allWarningsAsErrors = true
kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
} }
compose.desktop { compose.desktop {

0
examples/falling_balls/gradle.properties → examples/falling-balls/gradle.properties

0
examples/falling_balls_with_web/gradle/wrapper/gradle-wrapper.jar → examples/falling-balls/gradle/wrapper/gradle-wrapper.jar vendored

0
examples/falling_balls/gradle/wrapper/gradle-wrapper.properties → examples/falling-balls/gradle/wrapper/gradle-wrapper.properties vendored

0
examples/falling_balls_with_web/gradlew → examples/falling-balls/gradlew vendored

0
examples/falling_balls/gradlew.bat → examples/falling-balls/gradlew.bat vendored

0
examples/falling_balls/settings.gradle.kts → examples/falling-balls/settings.gradle.kts

4
examples/falling_balls/src/main/kotlin/Game.kt → examples/falling-balls/src/main/kotlin/Game.kt

@ -1,5 +1,6 @@
package org.jetbrains.compose.demo.falling package org.jetbrains.compose.demo.falling
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.Button import androidx.compose.material.Button
import androidx.compose.material.Slider import androidx.compose.material.Slider
@ -27,7 +28,7 @@ class Game {
var elapsed by mutableStateOf(0L) var elapsed by mutableStateOf(0L)
var score by mutableStateOf(0) var score by mutableStateOf(0)
var clicked by mutableStateOf(0) private var clicked by mutableStateOf(0)
var started by mutableStateOf(false) var started by mutableStateOf(false)
var paused by mutableStateOf(false) var paused by mutableStateOf(false)
@ -72,6 +73,7 @@ class Game {
} }
@Composable @Composable
@Preview
fun FallingBallsGame() { fun FallingBallsGame() {
val game = remember { Game() } val game = remember { Game() }
val density = LocalDensity.current val density = LocalDensity.current

0
examples/falling_balls/src/main/kotlin/Piece.kt → examples/falling-balls/src/main/kotlin/Piece.kt

15
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()
}

10
examples/falling_balls/src/main/kotlin/main.kt

@ -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()
}

8
examples/imageviewer/android/build.gradle.kts

@ -5,11 +5,11 @@ plugins {
} }
android { android {
compileSdkVersion(30) compileSdk = 30
defaultConfig { defaultConfig {
minSdkVersion(21) minSdk = 21
targetSdkVersion(30) targetSdk = 30
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "1.0"
} }
@ -22,5 +22,5 @@ android {
dependencies { dependencies {
implementation(project(":common")) implementation(project(":common"))
implementation("androidx.activity:activity-compose:1.3.0-alpha02") implementation("androidx.activity:activity-compose:1.3.0")
} }

4
examples/imageviewer/android/src/main/java/example/imageviewer/MainActivity.kt

@ -3,7 +3,7 @@ package example.imageviewer
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import example.imageviewer.view.BuildAppUI import example.imageviewer.view.AppUI
import example.imageviewer.model.ContentState import example.imageviewer.model.ContentState
import example.imageviewer.model.ImageRepository import example.imageviewer.model.ImageRepository
@ -17,7 +17,7 @@ class MainActivity : AppCompatActivity() {
) )
setContent { setContent {
BuildAppUI(content) AppUI(content)
} }
} }
} }

13
examples/imageviewer/build.gradle.kts

@ -1,8 +1,5 @@
buildscript { buildscript {
repositories { repositories {
mavenLocal().mavenContent {
includeModule("org.jetbrains.compose", "compose-gradle-plugin")
}
google() google()
mavenCentral() mavenCentral()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
@ -10,18 +7,16 @@ buildscript {
dependencies { dependencies {
// __LATEST_COMPOSE_RELEASE_VERSION__ // __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") classpath("com.android.tools.build:gradle:4.1.0")
// __KOTLIN_COMPOSE_VERSION__ classpath(kotlin("gradle-plugin", version = "1.5.21"))
classpath(kotlin("gradle-plugin", version = "1.5.10"))
} }
} }
allprojects { allprojects {
repositories { repositories {
mavenLocal()
google() google()
mavenCentral() mavenCentral()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
} }
} }

10
examples/imageviewer/common/build.gradle.kts

@ -20,7 +20,7 @@ kotlin {
} }
named("androidMain") { named("androidMain") {
dependencies { dependencies {
api("androidx.appcompat:appcompat:1.3.0-beta01") api("androidx.appcompat:appcompat:1.3.1")
api("androidx.core:core-ktx:1.3.1") api("androidx.core:core-ktx:1.3.1")
implementation("io.ktor:ktor-client-cio:1.4.1") implementation("io.ktor:ktor-client-cio:1.4.1")
} }
@ -35,13 +35,11 @@ kotlin {
} }
android { android {
compileSdkVersion(30) compileSdk = 30
defaultConfig { defaultConfig {
minSdkVersion(21) minSdk = 21
targetSdkVersion(30) targetSdk = 30
versionCode = 1
versionName = "1.0"
} }
compileOptions { compileOptions {

40
examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/ContentState.kt → examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/AndroidContentState.kt

@ -31,7 +31,7 @@ object ContentState {
this.uriRepository = uriRepository this.uriRepository = uriRepository
repository = ImageRepository(uriRepository) repository = ImageRepository(uriRepository)
appliedFilters = FiltersManager(context) appliedFilters = FiltersManager(context)
isAppUIReady.value = false isContentReady.value = false
initData() initData()
@ -50,9 +50,14 @@ object ContentState {
return context.resources.configuration.orientation 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 { fun isContentReady(): Boolean {
return isAppUIReady.value return isContentReady.value
} }
fun getString(id: Int): String { fun getString(id: Int): String {
@ -142,7 +147,7 @@ object ContentState {
// application content initialization // application content initialization
private fun initData() { private fun initData() {
if (isAppUIReady.value) if (isContentReady.value)
return return
val directory = context.cacheDir.absolutePath val directory = context.cacheDir.absolutePath
@ -158,7 +163,7 @@ object ContentState {
getString(R.string.repo_invalid), getString(R.string.repo_invalid),
context context
) )
isAppUIReady.value = true onContentReady()
} }
return@execute return@execute
} }
@ -171,7 +176,7 @@ object ContentState {
getString(R.string.repo_empty), getString(R.string.repo_empty),
context context
) )
isAppUIReady.value = true onContentReady()
} }
} else { } else {
val picture = loadFullImage(imageList[0]) val picture = loadFullImage(imageList[0])
@ -186,7 +191,7 @@ object ContentState {
mainImage.value = MainImageWrapper.getImage() mainImage.value = MainImageWrapper.getImage()
currentImageIndex.value = MainImageWrapper.getId() currentImageIndex.value = MainImageWrapper.getId()
} }
isAppUIReady.value = true onContentReady()
} }
} }
} else { } else {
@ -195,7 +200,7 @@ object ContentState {
getString(R.string.no_internet), getString(R.string.no_internet),
context context
) )
isAppUIReady.value = true onContentReady()
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
@ -210,7 +215,7 @@ object ContentState {
} }
fun fullscreen(picture: Picture) { fun fullscreen(picture: Picture) {
isAppUIReady.value = false isContentReady.value = false
AppState.screenState(ScreenType.FullscreenImage) AppState.screenState(ScreenType.FullscreenImage)
setMainImage(picture) setMainImage(picture)
} }
@ -218,9 +223,10 @@ object ContentState {
fun setMainImage(picture: Picture) { fun setMainImage(picture: Picture) {
if (MainImageWrapper.getId() == picture.id) { if (MainImageWrapper.getId() == picture.id) {
if (!isContentReady()) if (!isContentReady())
isAppUIReady.value = true onContentReady()
return return
} }
isContentReady.value = false
executor.execute { executor.execute {
if (isInternetAvailable()) { if (isInternetAvailable()) {
@ -230,7 +236,7 @@ object ContentState {
handler.post { handler.post {
wrapPictureIntoMainImage(fullSizePicture) wrapPictureIntoMainImage(fullSizePicture)
isAppUIReady.value = true onContentReady()
} }
} else { } else {
handler.post { handler.post {
@ -244,6 +250,11 @@ object ContentState {
} }
} }
private fun onContentReady() {
isContentReady.value = true
isAppReady.value = true
}
private fun wrapPictureIntoMainImage(picture: Picture) { private fun wrapPictureIntoMainImage(picture: Picture) {
MainImageWrapper.wrapPicture(picture) MainImageWrapper.wrapPicture(picture)
MainImageWrapper.saveOrigin() MainImageWrapper.saveOrigin()
@ -282,8 +293,9 @@ object ContentState {
if (isInternetAvailable()) { if (isInternetAvailable()) {
handler.post { handler.post {
clearCache(context) clearCache(context)
MainImageWrapper.clear()
miniatures.clear() miniatures.clear()
isAppUIReady.value = false isContentReady.value = false
initData() initData()
} }
} else { } else {
@ -334,6 +346,10 @@ private object MainImageWrapper {
return (picture.value.name == "") return (picture.value.name == "")
} }
fun clear() {
picture.value = Picture(image = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))
}
fun getName(): String { fun getName(): String {
return picture.value.name return picture.value.name
} }

77
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.RenderScript
import android.renderscript.ScriptIntrinsicBlur import android.renderscript.ScriptIntrinsicBlur
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import kotlin.math.pow
import kotlin.math.roundToInt
import example.imageviewer.view.DragHandler
fun scaleBitmapAspectRatio( fun scaleBitmapAspectRatio(
bitmap: Bitmap, bitmap: Bitmap,
@ -116,3 +119,77 @@ fun displayWidth(): Int {
fun displayHeight(): Int { fun displayHeight(): Int {
return Resources.getSystem().displayMetrics.heightPixels 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()
)
}

8
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 import example.imageviewer.style.Gray
@Composable @Composable
fun BuildAppUI(content: ContentState) { fun AppUI(content: ContentState) {
Surface( Surface(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
color = Gray color = Gray
) { ) {
when (AppState.screenState()) { when (AppState.screenState()) {
ScreenType.Main -> { ScreenType.MainScreen -> {
setMainScreen(content) MainScreen(content)
} }
ScreenType.FullscreenImage -> { ScreenType.FullscreenImage -> {
setImageFullScreen(content) FullscreenImage(content)
} }
} }
} }

114
examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/FullImageScreen.kt → 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.icFilterPixelOff
import example.imageviewer.style.icFilterPixelOn import example.imageviewer.style.icFilterPixelOn
import example.imageviewer.utils.adjustImageScale import example.imageviewer.utils.adjustImageScale
import example.imageviewer.utils.cropBitmapByScale
import example.imageviewer.utils.displayWidth import example.imageviewer.utils.displayWidth
import example.imageviewer.utils.getDisplayBounds import example.imageviewer.utils.getDisplayBounds
import kotlin.math.abs import kotlin.math.abs
@ -54,37 +55,20 @@ import kotlin.math.pow
import kotlin.math.roundToInt import kotlin.math.roundToInt
@Composable @Composable
fun setImageFullScreen( fun FullscreenImage(
content: ContentState content: ContentState
) { ) {
if (content.isContentReady()) { Column {
Column { ToolBar(content.getSelectedImageName(), content)
setToolBar(content.getSelectedImageName(), content) Image(content)
setImage(content)
}
} else {
setLoadingScreen()
} }
} if (!content.isContentReady()) {
LoadingScreen()
@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
)
}
}
} }
} }
@Composable @Composable
fun setToolBar( fun ToolBar(
text: String, text: String,
content: ContentState content: ContentState
) { ) {
@ -100,7 +84,7 @@ fun setToolBar(
onClick = { onClick = {
if (content.isContentReady()) { if (content.isContentReady()) {
content.restoreMainImage() content.restoreMainImage()
AppState.screenState(ScreenType.Main) AppState.screenState(ScreenType.MainScreen)
} }
}) { }) {
Image( Image(
@ -160,7 +144,6 @@ fun FilterButton(
@Composable @Composable
fun getFilterImage(type: FilterType, content: ContentState): Painter { fun getFilterImage(type: FilterType, content: ContentState): Painter {
return when (type) { return when (type) {
FilterType.GrayScale -> if (content.isFilterEnabled(type)) icFilterGrayscaleOn() else icFilterGrayscaleOff() FilterType.GrayScale -> if (content.isFilterEnabled(type)) icFilterGrayscaleOn() else icFilterGrayscaleOff()
FilterType.Pixel -> if (content.isFilterEnabled(type)) icFilterPixelOn() else icFilterPixelOff() FilterType.Pixel -> if (content.isFilterEnabled(type)) icFilterPixelOn() else icFilterPixelOff()
@ -169,8 +152,7 @@ fun getFilterImage(type: FilterType, content: ContentState): Painter {
} }
@Composable @Composable
fun setImage(content: ContentState) { fun Image(content: ContentState) {
val drag = remember { DragHandler() } val drag = remember { DragHandler() }
val scale = remember { ScaleHandler() } val scale = remember { ScaleHandler() }
@ -213,79 +195,3 @@ fun imageByGesture(
return bitmap 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()
)
}

73
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.icEmpty
import example.imageviewer.style.icRefresh import example.imageviewer.style.icRefresh
@Composable @Composable
fun setMainScreen(content: ContentState) { fun MainScreen(content: ContentState) {
Column {
if (content.isContentReady()) { TopContent(content)
Column { ScrollableArea(content)
setTopContent(content)
setScrollableArea(content)
}
} else {
setLoadingScreen(content)
} }
} if (!content.isContentReady()) {
LoadingScreen(content.getString(R.string.loading))
@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
)
} }
} }
@Composable @Composable
fun setTopContent(content: ContentState) { fun TopContent(content: ContentState) {
setTitleBar(text = content.getString(R.string.app_name), content = content) TitleBar(text = content.getString(R.string.app_name), content = content)
if (content.getOrientation() == Configuration.ORIENTATION_PORTRAIT) { if (content.getOrientation() == Configuration.ORIENTATION_PORTRAIT) {
setPreviewImageUI(content) PreviewImage(content)
setSpacer(h = 10) Spacer(modifier = Modifier.height(10.dp))
setDivider() Divider()
} }
setSpacer(h = 5) Spacer(modifier = Modifier.height(5.dp))
} }
@Composable @Composable
fun setTitleBar(text: String, content: ContentState) { fun TitleBar(text: String, content: ContentState) {
TopAppBar( TopAppBar(
backgroundColor = DarkGreen, backgroundColor = DarkGreen,
title = { title = {
@ -132,8 +104,7 @@ fun setTitleBar(text: String, content: ContentState) {
} }
@Composable @Composable
fun setPreviewImageUI(content: ContentState) { fun PreviewImage(content: ContentState) {
Clickable(onClick = { Clickable(onClick = {
AppState.screenState(ScreenType.FullscreenImage) AppState.screenState(ScreenType.FullscreenImage)
}) { }) {
@ -159,11 +130,10 @@ fun setPreviewImageUI(content: ContentState) {
} }
@Composable @Composable
fun setMiniatureUI( fun Miniature(
picture: Picture, picture: Picture,
content: ContentState content: ContentState
) { ) {
Card( Card(
backgroundColor = MiniatureColor, backgroundColor = MiniatureColor,
modifier = Modifier.padding(start = 10.dp, end = 10.dp).height(70.dp) modifier = Modifier.padding(start = 10.dp, end = 10.dp).height(70.dp)
@ -224,12 +194,12 @@ fun setMiniatureUI(
} }
@Composable @Composable
fun setScrollableArea(content: ContentState) { fun ScrollableArea(content: ContentState) {
var index = 1 var index = 1
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
Column(Modifier.verticalScroll(scrollState)) { Column(Modifier.verticalScroll(scrollState)) {
for (picture in content.getMiniatures()) { for (picture in content.getMiniatures()) {
setMiniatureUI( Miniature(
picture = picture, picture = picture,
content = content content = content
) )
@ -240,16 +210,9 @@ fun setScrollableArea(content: ContentState) {
} }
@Composable @Composable
fun setDivider() { fun Divider() {
Divider( Divider(
color = LightGray, color = LightGray,
modifier = Modifier.padding(start = 10.dp, end = 10.dp) modifier = Modifier.padding(start = 10.dp, end = 10.dp)
) )
} }
@Composable
fun setSpacer(h: Int) {
Spacer(modifier = Modifier.height(h.dp))
}

4
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 import androidx.compose.runtime.mutableStateOf
enum class ScreenType { enum class ScreenType {
Main, FullscreenImage MainScreen, FullscreenImage
} }
object AppState { object AppState {
private var screen: MutableState<ScreenType> private var screen: MutableState<ScreenType>
init { init {
screen = mutableStateOf(ScreenType.Main) screen = mutableStateOf(ScreenType.MainScreen)
} }
fun screenState() : ScreenType { fun screenState() : ScreenType {

2
examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Draggable.kt

@ -15,6 +15,7 @@ import example.imageviewer.style.Transparent
fun Draggable( fun Draggable(
dragHandler: DragHandler, dragHandler: DragHandler,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onUpdate: (() -> Unit)? = null,
children: @Composable() () -> Unit children: @Composable() () -> Unit
) { ) {
Surface( Surface(
@ -26,6 +27,7 @@ fun Draggable(
onDragCancel = { dragHandler.cancel() }, onDragCancel = { dragHandler.cancel() },
) { change, dragAmount -> ) { change, dragAmount ->
dragHandler.drag(dragAmount) dragHandler.drag(dragAmount)
onUpdate?.invoke()
change.consumePositionChange() change.consumePositionChange()
} }
} }

43
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
)
}
}

4
examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Scalable.kt

@ -18,7 +18,7 @@ fun Scalable(
Surface( Surface(
color = Transparent, color = Transparent,
modifier = modifier.pointerInput(Unit) { modifier = modifier.pointerInput(Unit) {
detectTapGestures(onDoubleTap = { onScale.resetFactor() }) detectTapGestures(onDoubleTap = { onScale.reset() })
detectTransformGestures { _, _, zoom, _ -> detectTransformGestures { _, _, zoom, _ ->
onScale.onScale(zoom) onScale.onScale(zoom)
} }
@ -31,7 +31,7 @@ fun Scalable(
class ScaleHandler(private val maxFactor: Float = 5f, private val minFactor: Float = 1f) { class ScaleHandler(private val maxFactor: Float = 5f, private val minFactor: Float = 1f) {
val factor = mutableStateOf(1f) val factor = mutableStateOf(1f)
fun resetFactor() { fun reset() {
if (factor.value > minFactor) if (factor.value > minFactor)
factor.value = minFactor factor.value = minFactor
} }

6
examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/R.kt

@ -14,6 +14,8 @@ object ResString {
val picture: String val picture: String
val size: String val size: String
val pixels: String val pixels: String
val back: String
val refresh: String
init { init {
if (System.getProperty("user.language").equals("ru")) { if (System.getProperty("user.language").equals("ru")) {
@ -29,6 +31,8 @@ object ResString {
picture = "Изображение:" picture = "Изображение:"
size = "Размеры:" size = "Размеры:"
pixels = "пикселей." pixels = "пикселей."
back = "Назад"
refresh = "Обновить"
} else { } else {
appName = "ImageViewer" appName = "ImageViewer"
loading = "Loading images..." loading = "Loading images..."
@ -42,6 +46,8 @@ object ResString {
picture = "Picture:" picture = "Picture:"
size = "Size:" size = "Size:"
pixels = "pixels." pixels = "pixels."
back = "Back"
refresh = "Refresh"
} }
} }
} }

161
examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/DesktopContentState.kt

@ -1,8 +1,10 @@
package example.imageviewer.model package example.imageviewer.model
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.RememberObserver
import androidx.compose.runtime.mutableStateOf 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.ResString
import example.imageviewer.core.FilterType import example.imageviewer.core.FilterType
import example.imageviewer.model.filtration.FiltersManager import example.imageviewer.model.filtration.FiltersManager
@ -10,19 +12,31 @@ import example.imageviewer.utils.cacheImagePath
import example.imageviewer.utils.clearCache import example.imageviewer.utils.clearCache
import example.imageviewer.utils.isInternetAvailable import example.imageviewer.utils.isInternetAvailable
import example.imageviewer.view.showPopUpMessage 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.awt.image.BufferedImage
import java.io.File import java.io.File
import java.util.concurrent.ExecutorService import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors import java.util.concurrent.Executors
import javax.swing.SwingUtilities.invokeLater import org.jetbrains.skija.Image.makeFromEncoded
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
object ContentState : RememberObserver { 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 repository: ImageRepository
private lateinit var uriRepository: String 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) { if (this::uriRepository.isInitialized && this.uriRepository == uriRepository) {
return this return this
} }
@ -35,8 +49,6 @@ object ContentState : RememberObserver {
return this return this
} }
private val executor: ExecutorService by lazy { Executors.newFixedThreadPool(2) }
private val isAppReady = mutableStateOf(false) private val isAppReady = mutableStateOf(false)
fun isAppReady(): Boolean { fun isAppReady(): Boolean {
return isAppReady.value return isAppReady.value
@ -48,7 +60,6 @@ object ContentState : RememberObserver {
} }
// drawable content // drawable content
private val mainImageWrapper = MainImageWrapper
private val mainImage = mutableStateOf(BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB)) private val mainImage = mutableStateOf(BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB))
private val currentImageIndex = mutableStateOf(0) private val currentImageIndex = mutableStateOf(0)
private val miniatures = Miniatures() private val miniatures = Miniatures()
@ -57,12 +68,12 @@ object ContentState : RememberObserver {
return miniatures.getMiniatures() return miniatures.getMiniatures()
} }
fun getSelectedImage(): BufferedImage { fun getSelectedImage(): ImageBitmap {
return mainImage.value return MainImageWrapper.mainImageAsImageBitmap.value
} }
fun getSelectedImageName(): String { fun getSelectedImageName(): String {
return mainImageWrapper.getName() return MainImageWrapper.getName()
} }
// filters managing // filters managing
@ -79,7 +90,6 @@ object ContentState : RememberObserver {
} }
fun toggleFilter(filter: FilterType) { fun toggleFilter(filter: FilterType) {
if (containsFilter(filter)) { if (containsFilter(filter)) {
removeFilter(filter) removeFilter(filter)
} else { } else {
@ -88,23 +98,24 @@ object ContentState : RememberObserver {
toggleFilterState(filter) toggleFilterState(filter)
var bitmap = mainImageWrapper.origin var bitmap = MainImageWrapper.origin
if (bitmap != null) { if (bitmap != null) {
bitmap = appliedFilters.applyFilters(bitmap) bitmap = appliedFilters.applyFilters(bitmap)
mainImageWrapper.setImage(bitmap) MainImageWrapper.setImage(bitmap)
mainImage.value = bitmap mainImage.value = bitmap
updateMainImage()
} }
} }
private fun addFilter(filter: FilterType) { private fun addFilter(filter: FilterType) {
appliedFilters.add(filter) appliedFilters.add(filter)
mainImageWrapper.addFilter(filter) MainImageWrapper.addFilter(filter)
} }
private fun removeFilter(filter: FilterType) { private fun removeFilter(filter: FilterType) {
appliedFilters.remove(filter) appliedFilters.remove(filter)
mainImageWrapper.removeFilter(filter) MainImageWrapper.removeFilter(filter)
} }
private fun containsFilter(type: FilterType): Boolean { private fun containsFilter(type: FilterType): Boolean {
@ -121,7 +132,7 @@ object ContentState : RememberObserver {
private fun restoreFilters(): BufferedImage { private fun restoreFilters(): BufferedImage {
filterUIState.clear() filterUIState.clear()
appliedFilters.clear() appliedFilters.clear()
return mainImageWrapper.restore() return MainImageWrapper.restore()
} }
fun restoreMainImage() { fun restoreMainImage() {
@ -138,52 +149,41 @@ object ContentState : RememberObserver {
directory.mkdir() directory.mkdir()
} }
executor.execute { scope.launch(Dispatchers.IO) {
try { try {
if (isInternetAvailable()) { if (isInternetAvailable()) {
val imageList = repository.get() val imageList = repository.get()
if (imageList.isEmpty()) { if (imageList.isEmpty()) {
invokeLater { showPopUpMessage(
showPopUpMessage( ResString.repoInvalid
ResString.repoInvalid )
) onContentReady()
onContentReady() } else {
} val pictureList = loadImages(cacheImagePath, imageList)
return@execute
}
val pictureList = loadImages(cacheImagePath, imageList)
if (pictureList.isEmpty()) { if (pictureList.isEmpty()) {
invokeLater {
showPopUpMessage( showPopUpMessage(
ResString.repoEmpty ResString.repoEmpty
) )
onContentReady() onContentReady()
} } else {
} else { val picture = loadFullImage(imageList[0])
val picture = loadFullImage(imageList[0])
invokeLater {
miniatures.setMiniatures(pictureList) miniatures.setMiniatures(pictureList)
if (isMainImageEmpty()) { if (isMainImageEmpty()) {
wrapPictureIntoMainImage(picture) wrapPictureIntoMainImage(picture)
} else { } else {
appliedFilters.add(mainImageWrapper.getFilters()) appliedFilters.add(MainImageWrapper.getFilters())
currentImageIndex.value = mainImageWrapper.getId() currentImageIndex.value = MainImageWrapper.getId()
} }
onContentReady() onContentReady()
} }
} }
} else { } else {
invokeLater { showPopUpMessage(
showPopUpMessage( ResString.noInternet
ResString.noInternet )
) onContentReady()
onContentReady()
}
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
@ -193,7 +193,7 @@ object ContentState : RememberObserver {
// preview/fullscreen image managing // preview/fullscreen image managing
fun isMainImageEmpty(): Boolean { fun isMainImageEmpty(): Boolean {
return mainImageWrapper.isEmpty() return MainImageWrapper.isEmpty()
} }
fun fullscreen(picture: Picture) { fun fullscreen(picture: Picture) {
@ -203,31 +203,27 @@ object ContentState : RememberObserver {
} }
fun setMainImage(picture: Picture) { fun setMainImage(picture: Picture) {
if (mainImageWrapper.getId() == picture.id) { if (MainImageWrapper.getId() == picture.id) {
if (!isContentReady()) { if (!isContentReady()) {
onContentReady() onContentReady()
} }
return return
} }
isContentReady.value = false
executor.execute { scope.launch(Dispatchers.IO) {
scale.reset()
if (isInternetAvailable()) { if (isInternetAvailable()) {
invokeLater {
val fullSizePicture = loadFullImage(picture.source) val fullSizePicture = loadFullImage(picture.source)
fullSizePicture.id = picture.id fullSizePicture.id = picture.id
wrapPictureIntoMainImage(fullSizePicture) wrapPictureIntoMainImage(fullSizePicture)
onContentReady()
}
} else { } else {
invokeLater {
showPopUpMessage( showPopUpMessage(
"${ResString.noInternet}\n${ResString.loadImageUnavailable}" "${ResString.noInternet}\n${ResString.loadImageUnavailable}"
) )
wrapPictureIntoMainImage(picture) wrapPictureIntoMainImage(picture)
onContentReady()
}
} }
onContentReady()
} }
} }
@ -237,10 +233,24 @@ object ContentState : RememberObserver {
} }
private fun wrapPictureIntoMainImage(picture: Picture) { private fun wrapPictureIntoMainImage(picture: Picture) {
mainImageWrapper.wrapPicture(picture) MainImageWrapper.wrapPicture(picture)
mainImageWrapper.saveOrigin() MainImageWrapper.saveOrigin()
mainImage.value = picture.image mainImage.value = picture.image
currentImageIndex.value = picture.id currentImageIndex.value = picture.id
updateMainImage()
}
fun updateMainImage() {
MainImageWrapper.mainImageAsImageBitmap.value = makeFromEncoded(
toByteArray(
cropBitmapByScale(
mainImage.value,
windowState.size,
scale.factor.value,
drag
)
)
).asImageBitmap()
} }
fun swipeNext() { fun swipeNext() {
@ -264,29 +274,20 @@ object ContentState : RememberObserver {
} }
fun refresh() { fun refresh() {
executor.execute { scope.launch(Dispatchers.IO) {
if (isInternetAvailable()) { if (isInternetAvailable()) {
invokeLater { clearCache()
clearCache() MainImageWrapper.clear()
miniatures.clear() miniatures.clear()
isContentReady.value = false isContentReady.value = false
initData() initData()
}
} else { } else {
invokeLater { showPopUpMessage(
showPopUpMessage( "${ResString.noInternet}\n${ResString.refreshUnavailable}"
"${ResString.noInternet}\n${ResString.refreshUnavailable}" )
)
}
} }
} }
} }
override fun onRemembered() { }
override fun onAbandoned() { }
override fun onForgotten() {
executor.shutdown()
}
} }
private object MainImageWrapper { private object MainImageWrapper {
@ -299,15 +300,15 @@ private object MainImageWrapper {
} }
fun restore(): BufferedImage { fun restore(): BufferedImage {
if (origin != null) { if (origin != null) {
picture.value.image = copy(origin!!) picture.value.image = copy(origin!!)
filtersSet.clear() filtersSet.clear()
} }
return copy(picture.value.image) return copy(picture.value.image)
} }
var mainImageAsImageBitmap = mutableStateOf(ImageBitmap(1, 1))
// picture adapter // picture adapter
private var picture = mutableStateOf( private var picture = mutableStateOf(
Picture(image = BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB)) Picture(image = BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB))
@ -325,6 +326,10 @@ private object MainImageWrapper {
return (picture.value.name == "") return (picture.value.name == "")
} }
fun clear() {
picture.value = Picture(image = BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB))
}
fun getName(): String { fun getName(): String {
return picture.value.name return picture.value.name
} }

44
examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/style/Decoration.kt

@ -1,58 +1,42 @@
package example.imageviewer.style package example.imageviewer.style
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.res.imageResource import androidx.compose.ui.res.painterResource
import java.awt.image.BufferedImage import java.awt.image.BufferedImage
import javax.imageio.ImageIO import javax.imageio.ImageIO
@Composable @Composable
fun icEmpty() = imageResource("images/empty.png") fun icEmpty() = painterResource("images/empty.png")
@Composable @Composable
fun icBack() = imageResource("images/back.png") fun icBack() = painterResource("images/back.png")
@Composable @Composable
fun icRefresh() = imageResource("images/refresh.png") fun icRefresh() = painterResource("images/refresh.png")
@Composable @Composable
fun icDots() = imageResource("images/dots.png") fun icDots() = painterResource("images/dots.png")
@Composable @Composable
fun icFilterGrayscaleOn() = imageResource("images/grayscale_on.png") fun icFilterGrayscaleOn() = painterResource("images/grayscale_on.png")
@Composable @Composable
fun icFilterGrayscaleOff() = imageResource("images/grayscale_off.png") fun icFilterGrayscaleOff() = painterResource("images/grayscale_off.png")
@Composable @Composable
fun icFilterPixelOn() = imageResource("images/pixel_on.png") fun icFilterPixelOn() = painterResource("images/pixel_on.png")
@Composable @Composable
fun icFilterPixelOff() = imageResource("images/pixel_off.png") fun icFilterPixelOff() = painterResource("images/pixel_off.png")
@Composable @Composable
fun icFilterBlurOn() = imageResource("images/blur_on.png") fun icFilterBlurOn() = painterResource("images/blur_on.png")
@Composable @Composable
fun icFilterBlurOff() = imageResource("images/blur_off.png") fun icFilterBlurOff() = painterResource("images/blur_off.png")
@Composable @Composable
fun icFilterUnknown() = imageResource("images/filter_unknown.png") fun icFilterUnknown() = painterResource("images/filter_unknown.png")
private var icon: BufferedImage? = null @Composable
fun icAppRounded(): BufferedImage { fun icAppRounded() = painterResource("images/ic_imageviewer_round.png")
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)
}

158
examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/Application.kt

@ -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<Unit>()
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<AppWindow, ApplicationApplier>(
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 <R> withFrameNanos(
onFrame: (frameTimeNanos: Long) -> R
) = onFrame(System.nanoTime())
}
private class ApplicationApplier(
private val onWindowsEmpty: () -> Unit
) : Applier<AppWindow?> {
private val windows = mutableListOf<AppWindow>()
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
}
}

108
examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/GraphicsMath.kt

@ -1,7 +1,7 @@
package example.imageviewer.utils package example.imageviewer.utils
import androidx.compose.desktop.AppManager import androidx.compose.ui.window.WindowSize
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp
import java.awt.Dimension import java.awt.Dimension
import java.awt.Graphics2D import java.awt.Graphics2D
import java.awt.Rectangle import java.awt.Rectangle
@ -14,6 +14,9 @@ import javax.imageio.ImageIO
import java.awt.image.BufferedImageOp import java.awt.image.BufferedImageOp
import java.awt.image.ConvolveOp import java.awt.image.ConvolveOp
import java.awt.image.Kernel import java.awt.image.Kernel
import kotlin.math.pow
import kotlin.math.roundToInt
import example.imageviewer.view.DragHandler
fun scaleBitmapAspectRatio( fun scaleBitmapAspectRatio(
bitmap: BufferedImage, bitmap: BufferedImage,
@ -38,10 +41,10 @@ fun scaleBitmapAspectRatio(
return result return result
} }
fun getDisplayBounds(bitmap: BufferedImage): Rectangle { fun getDisplayBounds(bitmap: BufferedImage, windowSize: WindowSize): Rectangle {
val boundW: Float = displayWidth().toFloat() val boundW: Float = windowSize.width.value.toFloat()
val boundH: Float = displayHeight().toFloat() val boundH: Float = windowSize.height.value.toFloat()
val ratioX: Float = bitmap.width / boundW val ratioX: Float = bitmap.width / boundW
val ratioY: Float = bitmap.height / boundH 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 { fun toByteArray(bitmap: BufferedImage) : ByteArray {
val baos = ByteArrayOutputStream() val baos = ByteArrayOutputStream()
ImageIO.write(bitmap, "png", baos) 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) 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 screenSize: Dimension = Toolkit.getDefaultToolkit().screenSize
val preferredWidth: Int = (screenSize.width * 0.8f).toInt() val preferredWidth: Int = (screenSize.width * 0.8f).toInt()
val preferredHeight: Int = (screenSize.height * 0.8f).toInt() val preferredHeight: Int = (screenSize.height * 0.8f).toInt()
val width: Int = if (desiredWidth < preferredWidth) desiredWidth else preferredWidth val width: Int = if (desiredWidth < preferredWidth) desiredWidth else preferredWidth
val height: Int = if (desiredHeight < preferredHeight) desiredHeight else preferredHeight val height: Int = if (desiredHeight < preferredHeight) desiredHeight else preferredHeight
return IntSize(width, height) return WindowSize(width.dp, height.dp)
} }

8
examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/AppUI.kt

@ -15,18 +15,18 @@ private val message: MutableState<String> = mutableStateOf("")
private val state: MutableState<Boolean> = mutableStateOf(false) private val state: MutableState<Boolean> = mutableStateOf(false)
@Composable @Composable
fun BuildAppUI(content: ContentState) { fun AppUI(content: ContentState) {
Surface( Surface(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
color = Gray color = Gray
) { ) {
when (AppState.screenState()) { when (AppState.screenState()) {
ScreenType.Main -> { ScreenType.MainScreen -> {
setMainScreen(content) MainScreen(content)
} }
ScreenType.FullscreenImage -> { ScreenType.FullscreenImage -> {
setImageFullScreen(content) FullscreenImage(content)
} }
} }
} }

314
examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/FullImageScreen.kt

@ -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)
}

225
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
)
}
}
}
}

119
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.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.asImageBitmap 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.layout.ContentScale
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import example.imageviewer.ResString import example.imageviewer.ResString
@ -59,51 +60,27 @@ import example.imageviewer.style.icRefresh
import example.imageviewer.utils.toByteArray import example.imageviewer.utils.toByteArray
@Composable @Composable
fun setMainScreen(content: ContentState) { fun MainScreen(content: ContentState) {
if (content.isContentReady()) { Column {
Column { TopContent(content)
setTopContent(content) ScrollableArea(content)
setScrollableArea(content)
}
} else {
setLoadingScreen(content)
} }
} if (!content.isContentReady()) {
LoadingScreen(ResString.loading)
@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
)
} }
} }
@Composable @Composable
fun setTopContent(content: ContentState) { fun TopContent(content: ContentState) {
setTitleBar(text = ResString.appName, content = content) TitleBar(text = ResString.appName, content = content)
setPreviewImageUI(content) PreviewImage(content)
setSpacer(h = 10) Spacer(modifier = Modifier.height(10.dp))
setDivider() Divider()
setSpacer(h = 5) Spacer(modifier = Modifier.height(5.dp))
} }
@Composable @Composable
fun setTitleBar(text: String, content: ContentState) { fun TitleBar(text: String, content: ContentState) {
val refreshButtonHover = remember { mutableStateOf(false) } val refreshButtonHover = remember { mutableStateOf(false) }
TopAppBar( TopAppBar(
backgroundColor = DarkGreen, backgroundColor = DarkGreen,
@ -119,29 +96,31 @@ fun setTitleBar(text: String, content: ContentState) {
modifier = Modifier.padding(end = 20.dp).align(Alignment.CenterVertically), modifier = Modifier.padding(end = 20.dp).align(Alignment.CenterVertically),
shape = CircleShape shape = CircleShape
) { ) {
Clickable( Tooltip(ResString.refresh) {
modifier = Modifier.hover( Clickable(
onEnter = { modifier = Modifier.hover(
refreshButtonHover.value = true onEnter = {
false refreshButtonHover.value = true
}, false
onExit = { },
refreshButtonHover.value = false onExit = {
false refreshButtonHover.value = false
} false
) }
.background(color = if (refreshButtonHover.value) TranslucentBlack else Transparent), )
onClick = { .background(color = if (refreshButtonHover.value) TranslucentBlack else Transparent),
if (content.isContentReady()) { onClick = {
content.refresh() 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 @Composable
fun setPreviewImageUI(content: ContentState) { fun PreviewImage(content: ContentState) {
Clickable( Clickable(
modifier = Modifier.background(color = DarkGray), modifier = Modifier.background(color = DarkGray),
onClick = { onClick = {
@ -165,9 +144,8 @@ fun setPreviewImageUI(content: ContentState) {
Image( Image(
if (content.isMainImageEmpty()) if (content.isMainImageEmpty())
icEmpty() icEmpty()
else org.jetbrains.skija.Image.makeFromEncoded( else
toByteArray(content.getSelectedImage()) BitmapPainter(content.getSelectedImage()),
).asImageBitmap(),
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
.fillMaxWidth().padding(start = 1.dp, top = 1.dp, end = 1.dp, bottom = 5.dp), .fillMaxWidth().padding(start = 1.dp, top = 1.dp, end = 1.dp, bottom = 5.dp),
@ -178,7 +156,7 @@ fun setPreviewImageUI(content: ContentState) {
} }
@Composable @Composable
fun setMiniatureUI( fun Miniature(
picture: Picture, picture: Picture,
content: ContentState content: ContentState
) { ) {
@ -265,7 +243,7 @@ fun setMiniatureUI(
} }
@Composable @Composable
fun setScrollableArea(content: ContentState) { fun ScrollableArea(content: ContentState) {
Box( Box(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
.padding(end = 8.dp) .padding(end = 8.dp)
@ -275,7 +253,7 @@ fun setScrollableArea(content: ContentState) {
var index = 1 var index = 1
Column { Column {
for (picture in content.getMiniatures()) { for (picture in content.getMiniatures()) {
setMiniatureUI( Miniature(
picture = picture, picture = picture,
content = content content = content
) )
@ -293,16 +271,9 @@ fun setScrollableArea(content: ContentState) {
} }
@Composable @Composable
fun setDivider() { fun Divider() {
Divider( Divider(
color = LightGray, color = LightGray,
modifier = Modifier.padding(start = 10.dp, end = 10.dp) modifier = Modifier.padding(start = 10.dp, end = 10.dp)
) )
} }
@Composable
fun setSpacer(h: Int) {
Spacer(modifier = Modifier.height(h.dp))
}

35
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()
}
}

44
examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Zoomable.kt

@ -1,44 +1,58 @@
package example.imageviewer.view package example.imageviewer.view
import androidx.compose.foundation.focusable
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusModifier
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.input.key.Key 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.input.pointer.pointerInput
import androidx.compose.ui.ExperimentalComposeUiApi
import example.imageviewer.style.Transparent import example.imageviewer.style.Transparent
import androidx.compose.runtime.DisposableEffect
@OptIn(ExperimentalComposeUiApi::class)
@Composable @Composable
fun Zoomable( fun Zoomable(
onScale: ScaleHandler, scaleHandler: ScaleHandler,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onUpdate: (() -> Unit)? = null,
children: @Composable() () -> Unit children: @Composable() () -> Unit
) { ) {
val focusRequester = FocusRequester() val focusRequester = FocusRequester()
Surface( Surface(
color = Transparent, color = Transparent,
modifier = modifier.shortcuts { modifier = modifier.onPreviewKeyEvent {
on(Key.I) { if (it.type == KeyEventType.KeyUp) {
onScale.onScale(1.2f) when (it.key) {
} Key.I -> {
on(Key.O) { scaleHandler.onScale(1.2f)
onScale.onScale(0.8f) onUpdate?.invoke()
} }
on(Key.R) { Key.O -> {
onScale.resetFactor() scaleHandler.onScale(0.8f)
onUpdate?.invoke()
}
Key.R -> {
scaleHandler.reset()
onUpdate?.invoke()
}
}
} }
false
} }
.focusRequester(focusRequester) .focusRequester(focusRequester)
.focusModifier() .focusable()
.pointerInput(Unit) { .pointerInput(Unit) {
detectTapGestures(onDoubleTap = { onScale.resetFactor() }) { detectTapGestures(onDoubleTap = { scaleHandler.reset() }) {
focusRequester.requestFocus() focusRequester.requestFocus()
} }
} }

2
examples/imageviewer/desktop/build.gradle.kts

@ -28,8 +28,6 @@ compose.desktop {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "ImageViewer" packageName = "ImageViewer"
packageVersion = "1.0.0" packageVersion = "1.0.0"
modules("jdk.crypto.ec")
val iconsRoot = project.file("../common/src/desktopMain/resources/images") val iconsRoot = project.file("../common/src/desktopMain/resources/images")
macOS { macOS {

41
examples/imageviewer/desktop/src/jvmMain/kotlin/example/imageviewer/Main.kt

@ -1,47 +1,58 @@
package example.imageviewer package example.imageviewer
import androidx.compose.desktop.DesktopTheme
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.remember 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.model.ContentState
import example.imageviewer.style.icAppRounded import example.imageviewer.style.icAppRounded
import example.imageviewer.utils.Application
import example.imageviewer.utils.getPreferredWindowSize import example.imageviewer.utils.getPreferredWindowSize
import example.imageviewer.view.BuildAppUI import example.imageviewer.view.AppUI
import example.imageviewer.view.SplashUI import example.imageviewer.view.SplashUI
fun main() = Application { fun main() = application {
val state = rememberWindowState()
val content = remember { val content = remember {
ContentState.applyContent( ContentState.applyContent(
state,
"https://raw.githubusercontent.com/JetBrains/compose-jb/master/artwork/imageviewerrepo/fetching.list" "https://raw.githubusercontent.com/JetBrains/compose-jb/master/artwork/imageviewerrepo/fetching.list"
) )
} }
val icon = remember(::icAppRounded) val icon = icAppRounded()
if (content.isAppReady()) { if (content.isAppReady()) {
ComposableWindow( Window(
onCloseRequest = ::exitApplication,
title = "Image Viewer", title = "Image Viewer",
size = getPreferredWindowSize(800, 1000), state = WindowState(
position = WindowPosition.Aligned(Alignment.Center),
size = getPreferredWindowSize(800, 1000)
),
icon = icon icon = icon
) { ) {
MaterialTheme { MaterialTheme {
DesktopTheme { AppUI(content)
BuildAppUI(content)
}
} }
} }
} else { } else {
ComposableWindow( Window(
onCloseRequest = ::exitApplication,
title = "Image Viewer", title = "Image Viewer",
size = getPreferredWindowSize(800, 300), state = WindowState(
position = WindowPosition.Aligned(Alignment.Center),
size = getPreferredWindowSize(800, 300)
),
undecorated = true, undecorated = true,
icon = icon, icon = icon,
) { ) {
MaterialTheme { MaterialTheme {
DesktopTheme { SplashUI()
SplashUI()
}
} }
} }
} }

2
examples/imageviewer/gradle/wrapper/gradle-wrapper.properties vendored

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

45
examples/intelliJPlugin/src/main/kotlin/com/jetbrains/compose/ComposeSizeAdjustmentWrapper.kt

@ -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) {}
}
)
}
}

0
examples/intelliJPlugin/.gitignore → examples/intellij-plugin/.gitignore vendored

0
examples/intelliJPlugin/README.md → examples/intellij-plugin/README.md

19
examples/intelliJPlugin/build.gradle.kts → examples/intellij-plugin/build.gradle.kts

@ -1,11 +1,12 @@
import org.jetbrains.compose.compose import org.jetbrains.compose.compose
plugins { plugins {
id("org.jetbrains.intellij") version "0.6.5" id("org.jetbrains.intellij") version "1.1.4"
java java
kotlin("jvm") version "1.5.10" kotlin("jvm") version "1.5.21"
// __LATEST_COMPOSE_RELEASE_VERSION__ // __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" group = "org.example"
@ -13,24 +14,18 @@ version = "1.0-SNAPSHOT"
repositories { repositories {
mavenCentral() mavenCentral()
google()
maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") } maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") }
} }
dependencies { dependencies {
implementation(kotlin("stdlib"))
implementation("org.jetbrains.compose.material:material:")
implementation(compose.desktop.currentOs) implementation(compose.desktop.currentOs)
testCompile("junit", "junit", "4.12") testImplementation("junit", "junit", "4.12")
} }
// See https://github.com/JetBrains/gradle-intellij-plugin/ // See https://github.com/JetBrains/gradle-intellij-plugin/
intellij { intellij {
version = "2021.1" version.set("2021.2")
}
tasks.getByName<org.jetbrains.intellij.tasks.PatchPluginXmlTask>("patchPluginXml") {
changeNotes("""
Add change notes here.<br>
<em>most HTML tags may be used</em>""")
} }
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> { tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {

0
examples/intelliJPlugin/gradle.properties → examples/intellij-plugin/gradle.properties

0
examples/intelliJPlugin/gradle/wrapper/gradle-wrapper.jar → examples/intellij-plugin/gradle/wrapper/gradle-wrapper.jar vendored

2
examples/web-getting-started/gradle/wrapper/gradle-wrapper.properties → examples/intellij-plugin/gradle/wrapper/gradle-wrapper.properties vendored

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

0
examples/intelliJPlugin/gradlew → examples/intellij-plugin/gradlew vendored

0
examples/intelliJPlugin/gradlew.bat → examples/intellij-plugin/gradlew.bat vendored

0
examples/intelliJPlugin/screenshots/screenshot.png → examples/intellij-plugin/screenshots/screenshot.png

Before

Width:  |  Height:  |  Size: 206 KiB

After

Width:  |  Height:  |  Size: 206 KiB

0
examples/intelliJPlugin/screenshots/toolsshow.png → examples/intellij-plugin/screenshots/toolsshow.png

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 152 KiB

0
examples/intelliJPlugin/settings.gradle.kts → examples/intellij-plugin/settings.gradle.kts

47
examples/intelliJPlugin/src/main/kotlin/com/jetbrains/compose/ComposeDemoAction.kt → examples/intellij-plugin/src/main/kotlin/com/jetbrains/compose/ComposeDemoAction.kt

@ -1,6 +1,5 @@
package com.jetbrains.compose package com.jetbrains.compose
import androidx.compose.desktop.ComposePanel
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row 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.foundation.layout.fillMaxSize
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.ui.Modifier 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.actionSystem.AnActionEvent
import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper import com.intellij.openapi.ui.DialogWrapper
import com.intellij.ui.layout.panel
import com.jetbrains.compose.theme.WidgetTheme import com.jetbrains.compose.theme.WidgetTheme
import com.jetbrains.compose.widgets.Buttons import com.jetbrains.compose.widgets.Buttons
import com.jetbrains.compose.widgets.LazyScrollable import com.jetbrains.compose.widgets.LazyScrollable
@ -22,6 +23,7 @@ import com.jetbrains.compose.widgets.TextInputs
import com.jetbrains.compose.widgets.Toggles import com.jetbrains.compose.widgets.Toggles
import java.awt.Dimension import java.awt.Dimension
import javax.swing.JComponent import javax.swing.JComponent
import javax.swing.SwingUtilities
/** /**
@ -39,31 +41,24 @@ class ComposeDemoAction : DumbAwareAction() {
} }
override fun createCenterPanel(): JComponent { override fun createCenterPanel(): JComponent {
val dialog = this
return ComposePanel().apply { return ComposePanel().apply {
preferredSize = Dimension(800, 600) setBounds(0, 0, 800, 600)
setContent { setContent {
ComposeSizeAdjustmentWrapper( WidgetTheme(darkTheme = true) {
window = dialog, Surface(modifier = Modifier.fillMaxSize()) {
panel = this, Row {
preferredSize = IntSize(800, 600) Column(
) { modifier = Modifier.fillMaxHeight().weight(1f)
WidgetTheme(darkTheme = true) { ) {
Surface(modifier = Modifier.fillMaxSize()) { Buttons()
Row { Loaders()
Column( TextInputs()
modifier = Modifier.fillMaxHeight().weight(1f) Toggles()
) { }
Buttons() Box(
Loaders() modifier = Modifier.fillMaxHeight().weight(1f)
TextInputs() ) {
Toggles() LazyScrollable()
}
Box(
modifier = Modifier.fillMaxHeight().weight(1f)
) {
LazyScrollable()
}
} }
} }
} }

0
examples/intelliJPlugin/src/main/kotlin/com/jetbrains/compose/theme/Color.kt → examples/intellij-plugin/src/main/kotlin/com/jetbrains/compose/theme/Color.kt

0
examples/intelliJPlugin/src/main/kotlin/com/jetbrains/compose/theme/Shape.kt → examples/intellij-plugin/src/main/kotlin/com/jetbrains/compose/theme/Shape.kt

0
examples/intelliJPlugin/src/main/kotlin/com/jetbrains/compose/theme/Theme.kt → examples/intellij-plugin/src/main/kotlin/com/jetbrains/compose/theme/Theme.kt

0
examples/intelliJPlugin/src/main/kotlin/com/jetbrains/compose/theme/Type.kt → examples/intellij-plugin/src/main/kotlin/com/jetbrains/compose/theme/Type.kt

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save