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. 31
      README.md
  4. 0
      artwork/compose-logo.xml
  5. 4
      cef/build.gradle.kts
  6. 2
      cef/gradle/wrapper/gradle-wrapper.properties
  7. 2
      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. 5
      compose/README.md
  17. 12
      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. 19
      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. 26
      examples/codeviewer/desktop/src/jvmMain/kotlin/org/jetbrains/codeviewer/main.kt
  28. 5
      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. 11
      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. 112
      examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/FullscreenImage.kt
  65. 71
      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. 123
      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. 75
      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. 40
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Zoomable.kt
  81. 2
      examples/imageviewer/desktop/build.gradle.kts
  82. 39
      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. 17
      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)
* 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)

32
FEATURES.md

@ -1,24 +1,39 @@
## Features
Features currently available in Compose for Desktop
* [Scrollbars support](tutorials/Scrollbars/README.md)
### Supported platforms
* macOS (x86-64, arm64)
* Windows (x86-64)
* Linux (x86-64, arm64)
* Web browsers
### Features currently available in Compose for Desktop
* [Intro](tutorials/Getting_Started)
* [Desktop Components](tutorials/Desktop_Components/README.md)
* [Image loading support](tutorials/Image_And_Icons_Manipulations/README.md)
* [Keyboard handling](tutorials/Keyboard/README.md)
* [Mouse clicks and move](tutorials/Mouse_Events/README.md)
* [Packaging to native distributions](tutorials/Native_distributions_and_local_execution/README.md)
* [Tray, menu bar and notifications](tutorials/Tray_Notifications_MenuBar/README.md)
* [Window properties handling](tutorials/Window_API/README.md)
* [Signing and notarization](tutorials/Signing_and_notarization_on_macOS/README.md)
* [Swing interoperability](tutorials/Swing_Integration/README.md)
* [Keyboard navigation](tutorials/Tab_Navigation/README.md)
* [Tray, menu bar and notifications](tutorials/Tray_Notifications_MenuBar_new/README.md)
* [Window properties handling](tutorials/Window_API_new/README.md)
### Features currently available in Compose for Web
* [Intro](tutorials/Web/Building_UI/README.md)
* [Event handling](tutorials/Web/Events_Handling/README.md)
* [CSS](tutorials/Web/Style_Dsl/README.md)
Follow individual tutorials to understand how to use particular feature.
### Limitations
Following limitations apply to Milestone 3 (M3) release.
Following limitations apply to Alpha release.
* Only 64-bit Windows is supported
* Only 64-bit x86 Windows is supported
* Only JDK 11 or later is supported due to the memory management scheme used in Skia bindings
* Some Linux distributions require additional packages to be installed, see [this issue](https://github.com/JetBrains/compose-jb/issues/273) for more information
* Only JDK 15 or later is supported for packaging native distributions due to jpackage limitations
[comment]: <> (__SUPPORTED_GRADLE_VERSIONS__)
@ -27,3 +42,4 @@ Following limitations apply to Milestone 3 (M3) release.
* M1 works only with Gradle 6.4 and 6.5;
* M2 works only with Gradle 6.4 or later (6.7 is the latest tested version).
* M3 works only with Gradle 6.4 or later (6.8 is the latest tested version).
* Alpha works with Gralde 6.7 or later (7.1 is the latest tested version).

31
README.md

@ -2,50 +2,51 @@
[![Latest release](https://img.shields.io/github/v/release/JetBrains/compose-jb?color=brightgreen&label=latest%20release)](https://github.com/JetBrains/compose-jb/releases/latest)
[![Latest build](https://img.shields.io/github/v/release/JetBrains/compose-jb?color=orange&include_prereleases&label=latest%20build)](https://github.com/JetBrains/compose-jb/releases)
# Compose for Desktop and Web, by JetBrains
# Compose Multiplatform, by JetBrains
![](artwork/readme/apps.png)
Compose Kotlin UI framework port for desktop platforms (macOS, Linux, Windows), components outside of the core Compose repository
Compose Kotlin UI framework port for desktop platforms (macOS, Linux, Windows) and Web, components outside of the core Compose repository
at https://android.googlesource.com/platform/frameworks/support.
Preview functionality (check your application UI without building/running it) for desktop platforms is available via IDEA plugin (https://plugins.jetbrains.com/plugin/16541-compose-multiplatform-ide-support).
## Repository organization ##
* [artwork](artwork) - design artifacts
* [benchmarks](benchmarks) - collection of benchmarks
* [compose](compose) - composite build of [Compose-jb sources](https://github.com/JetBrains/androidx)
* [ci](ci) - Continuous Integration helpers
* [cef](cef) - CEF integration in Jetpack Compose
* [examples](examples) - examples of multiplatform Compose applications for Desktop and Android
* [cef](cef) - CEF integration in Jetpack Compose (somewhat outdated)
* [examples](examples) - examples of multiplatform Compose applications for Desktop, Android and Web
* [codeviewer](examples/codeviewer) - File Browser and Code Viewer application for Android and Desktop
* [imageviewer](examples/imageviewer) - Image Viewer application for Android and Desktop
* [issues](examples/issues) - GitHub issue tracker with an adaptive UI and ktor-client
* [game](examples/falling_balls) - Simple game
* [game](examples/falling_balls_with_web) - Simple game for web target
* [game](examples/falling-balls) - Simple game
* [game](examples/falling-balls-with-web) - Simple game for web target
* [compose-bird](examples/web-compose-bird) - A flappy bird clone using Compose for Web
* [notepad](examples/notepad) - Notepad, using the new experimental Composable Window API
* [todoapp](examples/todoapp) - TODO items tracker with persistence and multiple screens
* [widgets gallery](examples/widgetsgallery) - Gallery of standard widgets
* [IDEA plugin](examples/intelliJPlugin) - Plugin for IDEA using Compose for Desktop
* [gradle-plugins](gradle-plugins) - a plugin, simplifying usage of Compose with Gradle
* [IDEA plugin](examples/intellij-plugin) - Plugin for IDEA using Compose for Desktop
* [gradle-plugins](gradle-plugins) - a plugin, simplifying usage of Compose Multiplatform with Gradle
* [templates](templates) - new application templates (see `desktop-template/build_and_run_from_cli_example.sh` for using without Gradle)
* [tutorials](tutorials) - tutorials on using Compose for Desktop
* [tutorials](tutorials) - tutorials on using Compose Multiplatform
* [Getting started](tutorials/Getting_Started)
* [Image and icon manipulations](tutorials/Image_And_Icons_Manipulations)
* [Mouse events and hover](tutorials/Mouse_Events)
* [Scrolling and scrollbars](tutorials/Desktop_Components)
* [Tooltips](tutorials/Desktop_Components#tooltips)
* [Top level windows management](tutorials/Window_API)
* [Top level windows management (new Composable API, experimental)](tutorials/Window_API_new)
* [Menu, tray, notifications](tutorials/Tray_Notifications_MenuBar)
* [Menu, tray, notifications (new Composable API, experimental)](tutorials/Tray_Notifications_MenuBar_new)
* [Top level windows management](tutorials/Window_API_new)
* [Menu, tray, notifications](tutorials/Tray_Notifications_MenuBar_new)
* [Keyboard support](tutorials/Keyboard)
* [Tab focus navigation](tutorials/Tab_Navigation)
* [Building native distribution](tutorials/Native_distributions_and_local_execution)
* [Signing and notarization](tutorials/Signing_and_notarization_on_macOS)
* [Swing interoperability](tutorials/Swing_Integration)
* [Navigation](tutorials/Navigation)
* [components](components) - custom components of Compose for Desktop
* [components](components) - custom components of Compose Multiplatform
* [Video Player](components/VideoPlayer)
* [Split Pane](components/SplitPane)
## Getting latest version of Multiplatform Compose ##
## Getting latest version of Compose Multiplatform ##
See https://github.com/JetBrains/compose-jb/tags for the latest build number.

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 {
// __KOTLIN_COMPOSE_VERSION__
kotlin("jvm") version "1.4.20"
kotlin("jvm") version "1.5.21"
// __LATEST_COMPOSE_RELEASE_VERSION__
id("org.jetbrains.compose") version "0.3.0-build133"
id("org.jetbrains.compose") version "1.0.0-alpha1"
id("de.undercouch.download") version "4.1.1"
application
}

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

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

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

@ -11,6 +11,7 @@ val properties = ComposeUberJarProperties()
repositories {
mavenCentral()
maven(properties.composeRepoUrl)
google()
}
val composeVersion: String by lazy {
@ -22,6 +23,7 @@ val composeVersion: String by lazy {
}
dependencies {
implementation("org.jetbrains.compose.desktop:desktop-jvm-macos-x64:$composeVersion")
implementation("org.jetbrains.compose.desktop:desktop-jvm-linux-x64:$composeVersion")
}

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

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

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

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

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

@ -1,49 +1,33 @@
package org.jetbrains.compose.splitpane.demo
import androidx.compose.desktop.DesktopTheme
import androidx.compose.desktop.LocalAppWindow
import androidx.compose.desktop.Window
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.width
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.input.pointer.pointerMoveFilter
import androidx.compose.ui.input.pointer.PointerIcon
import androidx.compose.ui.input.pointer.pointerIcon
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.singleWindowApplication
import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi
import org.jetbrains.compose.splitpane.HorizontalSplitPane
import org.jetbrains.compose.splitpane.VerticalSplitPane
import org.jetbrains.compose.splitpane.rememberSplitPaneState
import java.awt.Cursor
private fun Modifier.cursorForHorizontalResize(
): Modifier = composed {
var isHover by remember { mutableStateOf(false) }
if (isHover) {
LocalAppWindow.current.window.cursor = Cursor(Cursor.E_RESIZE_CURSOR)
} else {
LocalAppWindow.current.window.cursor = Cursor.getDefaultCursor()
}
pointerMoveFilter(
onEnter = { isHover = true; true },
onExit = { isHover = false; true }
)
}
@OptIn(ExperimentalComposeUiApi::class)
private fun Modifier.cursorForHorizontalResize(): Modifier =
pointerIcon(PointerIcon(Cursor(Cursor.E_RESIZE_CURSOR)))
@OptIn(ExperimentalSplitPaneApi::class)
fun main() = Window(
"SplitPane demo"
fun main() = singleWindowApplication(
title = "SplitPane demo"
) {
MaterialTheme {
DesktopTheme {

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

@ -1,9 +1,5 @@
package org.jetbrains.compose.splitpane
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsDraggedAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue

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

@ -1,45 +1,23 @@
package org.jetbrains.compose.splitpane
import androidx.compose.desktop.LocalAppWindow
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.PointerIcon
import androidx.compose.ui.input.pointer.consumeAllChanges
import androidx.compose.ui.input.pointer.pointerIcon
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.pointerMoveFilter
import androidx.compose.ui.unit.dp
import java.awt.Cursor
private fun Modifier.cursorForHorizontalResize(
isHorizontal: Boolean
): Modifier = composed {
var isHover by remember { mutableStateOf(false) }
if (isHover) {
LocalAppWindow.current.window.cursor = Cursor(
if (isHorizontal) Cursor.E_RESIZE_CURSOR else Cursor.S_RESIZE_CURSOR
)
} else {
LocalAppWindow.current.window.cursor = Cursor.getDefaultCursor()
}
pointerMoveFilter(
onEnter = { isHover = true; true },
onExit = { isHover = false; true }
)
}
@OptIn(ExperimentalComposeUiApi::class)
private fun Modifier.cursorForHorizontalResize(isHorizontal: Boolean): Modifier =
pointerIcon(PointerIcon(Cursor(if (isHorizontal) Cursor.E_RESIZE_CURSOR else Cursor.S_RESIZE_CURSOR)))
@Composable
private fun DesktopSplitPaneSeparator(

2
components/build.gradle.kts

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

2
components/gradle.properties

@ -4,4 +4,4 @@ android.enableJetifier=true
kotlin.code.style=official
# __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
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.6-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

5
compose/README.md

@ -4,7 +4,7 @@ Composite build of [Compose-jb sources](https://github.com/JetBrains/androidx)
## Download submodules after downloading the main project:
```
git submodule update --init
git submodule update --init --recursive
```
Set this property to always update submodules on git checkout/pull/reset:
```
@ -20,7 +20,7 @@ git config --global submodule.recurse true
- CMake 3.10.2.4988404 (in folder $androidSdk/cmake, not in $androidSdk/cmake/$version)
## Requirements to develop in IDE
- Android Studio Arctic Fox | 2020.3.1 Canary 15
- Android Studio Arctic Fox
- Custom Gradle 7.1 specified in `Settings -> Build, Execution, Deployment -> Build Tools -> Gradle` (because Android Studio will pick the wrong Gradle in the subproject instead of the Gradle in the root project)
- Specified Gradle JDK 11 in `... -> Build Tools -> Gradle`
- Environment variables:
@ -35,6 +35,7 @@ export COMPOSE_CUSTOM_GROUP=org.jetbrains.compose
androidx.compose.multiplatformEnabled=true
androidx.compose.jsCompilerTestsEnabled=true
```
(note that https://android.googlesource.com/platform/frameworks/support build doesn't work with androidx.compose.jsCompilerTestsEnabled)
## Scripts
Publish artifacts to the local directory `out/androidx/build/support_repo/org/jetbrains/compose`:

12
compose/build.gradle.kts

@ -37,6 +37,7 @@ tasks.register("publishComposeJb") {
":compose:ui:ui-test-junit4",
":compose:ui:ui-text",
":compose:ui:ui-tooling",
":compose:ui:ui-tooling-preview",
":compose:ui:ui-unit",
":compose:ui:ui-util",
).forEach {
@ -70,6 +71,9 @@ tasks.register("testComposeJbDesktop") {
dependsOnComposeTask(":compose:desktop:desktop:jvmTest")
dependsOnComposeTask(":compose:animation:animation:desktopTest")
dependsOnComposeTask(":compose:animation:animation-core:desktopTest")
dependsOnComposeTask(":compose:ui:ui:desktopTest")
dependsOnComposeTask(":compose:ui:ui-graphics:desktopTest")
dependsOnComposeTask(":compose:ui:ui-text:desktopTest")
dependsOnComposeTask(":compose:foundation:foundation:desktopTest")
dependsOnComposeTask(":compose:foundation:foundation-layout:desktopTest")
dependsOnComposeTask(":compose:material:material:desktopTest")
@ -82,3 +86,11 @@ tasks.register("testComposeJbWeb") {
dependsOnComposeTask(":compose:runtime:runtime:jsTest")
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 {
// __LATEST_COMPOSE_RELEASE_VERSION__
classpath("org.jetbrains.compose:compose-gradle-plugin:0.4.0")
classpath("org.jetbrains.compose:compose-gradle-plugin:1.0.0-alpha1")
classpath("com.android.tools.build:gradle:4.0.1")
// __KOTLIN_COMPOSE_VERSION__
classpath(kotlin("gradle-plugin", version = "1.5.10"))
classpath(kotlin("gradle-plugin", version = "1.5.21"))
}
}

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 {
val text = textLines.get(index)
.trim('\n') // fix for native crash in Skia.
// Workaround for another Skia problem with empty line layout.
// TODO: maybe use another symbols, i.e. \u2800 or \u00a0.
val state = mutableStateOf(if (text.isEmpty()) " " else text)
val state = mutableStateOf(text)
return Editor.Content(state, isCode)
}

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

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

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

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

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

@ -1,23 +1,19 @@
package org.jetbrains.codeviewer
import androidx.compose.desktop.Window
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.res.loadImageBitmap
import androidx.compose.ui.res.useResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.WindowState
import androidx.compose.ui.window.singleWindowApplication
import org.jetbrains.codeviewer.ui.MainView
import java.awt.image.BufferedImage
import javax.imageio.ImageIO
fun main() = Window(
@OptIn(ExperimentalComposeUiApi::class)
fun main() = singleWindowApplication(
title = "Code Viewer",
size = IntSize(1280, 768),
icon = loadImageResource("ic_launcher.png"),
state = WindowState(width = 1280.dp, height = 768.dp),
icon = BitmapPainter(useResource("ic_launcher.png", ::loadImageBitmap)),
) {
MainView()
}
@Suppress("SameParameterValue")
private fun loadImageResource(path: String): BufferedImage {
val resource = Thread.currentThread().contextClassLoader.getResource(path)
requireNotNull(resource) { "Resource $path not found" }
return resource.openStream().use(ImageIO::read)
}

5
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
plugins {
kotlin("multiplatform") version "1.5.10"
id("org.jetbrains.compose") version "0.5.0-build228"
kotlin("multiplatform") version "1.5.21"
id("org.jetbrains.compose") version "1.0.0-alpha1-rc5"
}
version = "1.0-SNAPSHOT"
@ -11,6 +11,7 @@ version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
google()
}
kotlin {

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
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

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()
mavenCentral()
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 {
// __KOTLIN_COMPOSE_VERSION__
kotlin("jvm") version "1.5.10"
kotlin("jvm") version "1.5.21"
// __LATEST_COMPOSE_RELEASE_VERSION__
id("org.jetbrains.compose") version "0.4.0"
id("org.jetbrains.compose") version "1.0.0-alpha1"
}
group = "me.user"
@ -14,6 +14,7 @@ version = "1.0"
repositories {
mavenCentral()
google()
maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") }
}
@ -23,6 +24,8 @@ dependencies {
tasks.withType<KotlinCompile>() {
kotlinOptions.jvmTarget = "11"
kotlinOptions.allWarningsAsErrors = true
kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
}
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
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.Slider
@ -27,7 +28,7 @@ class Game {
var elapsed by mutableStateOf(0L)
var score by mutableStateOf(0)
var clicked by mutableStateOf(0)
private var clicked by mutableStateOf(0)
var started by mutableStateOf(false)
var paused by mutableStateOf(false)
@ -72,6 +73,7 @@ class Game {
}
@Composable
@Preview
fun FallingBallsGame() {
val game = remember { Game() }
val density = LocalDensity.current

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

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

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

11
examples/imageviewer/build.gradle.kts

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

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

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

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

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.ScriptIntrinsicBlur
import androidx.compose.ui.layout.ContentScale
import kotlin.math.pow
import kotlin.math.roundToInt
import example.imageviewer.view.DragHandler
fun scaleBitmapAspectRatio(
bitmap: Bitmap,
@ -116,3 +119,77 @@ fun displayWidth(): Int {
fun displayHeight(): Int {
return Resources.getSystem().displayMetrics.heightPixels
}
fun cropBitmapByScale(bitmap: Bitmap, scale: Float, drag: DragHandler): Bitmap {
val crop = cropBitmapByBounds(
bitmap,
getDisplayBounds(bitmap),
scale,
drag
)
return Bitmap.createBitmap(
bitmap,
crop.left,
crop.top,
crop.right - crop.left,
crop.bottom - crop.top
)
}
fun cropBitmapByBounds(
bitmap: Bitmap,
bounds: Rect,
scaleFactor: Float,
drag: DragHandler
): Rect {
if (scaleFactor <= 1f)
return Rect(0, 0, bitmap.width, bitmap.height)
var scale = scaleFactor.toDouble().pow(1.4)
var boundW = (bounds.width() / scale).roundToInt()
var boundH = (bounds.height() / scale).roundToInt()
scale *= displayWidth() / bounds.width().toDouble()
val offsetX = drag.getAmount().x / scale
val offsetY = drag.getAmount().y / scale
if (boundW > bitmap.width) {
boundW = bitmap.width
}
if (boundH > bitmap.height) {
boundH = bitmap.height
}
val invisibleW = bitmap.width - boundW
var leftOffset = (invisibleW / 2.0 - offsetX).roundToInt().toFloat()
if (leftOffset > invisibleW) {
leftOffset = invisibleW.toFloat()
drag.getAmount().x = -((invisibleW / 2.0) * scale).roundToInt().toFloat()
}
if (leftOffset < 0) {
drag.getAmount().x = ((invisibleW / 2.0) * scale).roundToInt().toFloat()
leftOffset = 0f
}
val invisibleH = bitmap.height - boundH
var topOffset = (invisibleH / 2 - offsetY).roundToInt().toFloat()
if (topOffset > invisibleH) {
topOffset = invisibleH.toFloat()
drag.getAmount().y = -((invisibleH / 2.0) * scale).roundToInt().toFloat()
}
if (topOffset < 0) {
drag.getAmount().y = ((invisibleH / 2.0) * scale).roundToInt().toFloat()
topOffset = 0f
}
return Rect(
leftOffset.toInt(),
topOffset.toInt(),
(leftOffset + boundW).toInt(),
(topOffset + boundH).toInt()
)
}

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

112
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.icFilterPixelOn
import example.imageviewer.utils.adjustImageScale
import example.imageviewer.utils.cropBitmapByScale
import example.imageviewer.utils.displayWidth
import example.imageviewer.utils.getDisplayBounds
import kotlin.math.abs
@ -54,37 +55,20 @@ import kotlin.math.pow
import kotlin.math.roundToInt
@Composable
fun setImageFullScreen(
fun FullscreenImage(
content: ContentState
) {
if (content.isContentReady()) {
Column {
setToolBar(content.getSelectedImageName(), content)
setImage(content)
}
} else {
setLoadingScreen()
}
}
@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
)
}
ToolBar(content.getSelectedImageName(), content)
Image(content)
}
if (!content.isContentReady()) {
LoadingScreen()
}
}
@Composable
fun setToolBar(
fun ToolBar(
text: String,
content: ContentState
) {
@ -100,7 +84,7 @@ fun setToolBar(
onClick = {
if (content.isContentReady()) {
content.restoreMainImage()
AppState.screenState(ScreenType.Main)
AppState.screenState(ScreenType.MainScreen)
}
}) {
Image(
@ -160,7 +144,6 @@ fun FilterButton(
@Composable
fun getFilterImage(type: FilterType, content: ContentState): Painter {
return when (type) {
FilterType.GrayScale -> if (content.isFilterEnabled(type)) icFilterGrayscaleOn() else icFilterGrayscaleOff()
FilterType.Pixel -> if (content.isFilterEnabled(type)) icFilterPixelOn() else icFilterPixelOff()
@ -169,8 +152,7 @@ fun getFilterImage(type: FilterType, content: ContentState): Painter {
}
@Composable
fun setImage(content: ContentState) {
fun Image(content: ContentState) {
val drag = remember { DragHandler() }
val scale = remember { ScaleHandler() }
@ -213,79 +195,3 @@ fun imageByGesture(
return bitmap
}
private fun cropBitmapByScale(bitmap: Bitmap, scale: Float, drag: DragHandler): Bitmap {
val crop = cropBitmapByBounds(
bitmap,
getDisplayBounds(bitmap),
scale,
drag
)
return Bitmap.createBitmap(
bitmap,
crop.left,
crop.top,
crop.right - crop.left,
crop.bottom - crop.top
)
}
private fun cropBitmapByBounds(
bitmap: Bitmap,
bounds: Rect,
scaleFactor: Float,
drag: DragHandler
): Rect {
if (scaleFactor <= 1f)
return Rect(0, 0, bitmap.width, bitmap.height)
var scale = scaleFactor.toDouble().pow(1.4)
var boundW = (bounds.width() / scale).roundToInt()
var boundH = (bounds.height() / scale).roundToInt()
scale *= displayWidth() / bounds.width().toDouble()
val offsetX = drag.getAmount().x / scale
val offsetY = drag.getAmount().y / scale
if (boundW > bitmap.width) {
boundW = bitmap.width
}
if (boundH > bitmap.height) {
boundH = bitmap.height
}
val invisibleW = bitmap.width - boundW
var leftOffset = (invisibleW / 2.0 - offsetX).roundToInt().toFloat()
if (leftOffset > invisibleW) {
leftOffset = invisibleW.toFloat()
drag.getAmount().x = -((invisibleW / 2.0) * scale).roundToInt().toFloat()
}
if (leftOffset < 0) {
drag.getAmount().x = ((invisibleW / 2.0) * scale).roundToInt().toFloat()
leftOffset = 0f
}
val invisibleH = bitmap.height - boundH
var topOffset = (invisibleH / 2 - offsetY).roundToInt().toFloat()
if (topOffset > invisibleH) {
topOffset = invisibleH.toFloat()
drag.getAmount().y = -((invisibleH / 2.0) * scale).roundToInt().toFloat()
}
if (topOffset < 0) {
drag.getAmount().y = ((invisibleH / 2.0) * scale).roundToInt().toFloat()
topOffset = 0f
}
return Rect(
leftOffset.toInt(),
topOffset.toInt(),
(leftOffset + boundW).toInt(),
(topOffset + boundH).toInt()
)
}

71
examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/MainScreen.kt

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

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
enum class ScreenType {
Main, FullscreenImage
MainScreen, FullscreenImage
}
object AppState {
private var screen: MutableState<ScreenType>
init {
screen = mutableStateOf(ScreenType.Main)
screen = mutableStateOf(ScreenType.MainScreen)
}
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(
dragHandler: DragHandler,
modifier: Modifier = Modifier,
onUpdate: (() -> Unit)? = null,
children: @Composable() () -> Unit
) {
Surface(
@ -26,6 +27,7 @@ fun Draggable(
onDragCancel = { dragHandler.cancel() },
) { change, dragAmount ->
dragHandler.drag(dragAmount)
onUpdate?.invoke()
change.consumePositionChange()
}
}

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

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

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

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

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

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

@ -1,58 +1,42 @@
package example.imageviewer.style
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.imageResource
import androidx.compose.ui.res.painterResource
import java.awt.image.BufferedImage
import javax.imageio.ImageIO
@Composable
fun icEmpty() = imageResource("images/empty.png")
fun icEmpty() = painterResource("images/empty.png")
@Composable
fun icBack() = imageResource("images/back.png")
fun icBack() = painterResource("images/back.png")
@Composable
fun icRefresh() = imageResource("images/refresh.png")
fun icRefresh() = painterResource("images/refresh.png")
@Composable
fun icDots() = imageResource("images/dots.png")
fun icDots() = painterResource("images/dots.png")
@Composable
fun icFilterGrayscaleOn() = imageResource("images/grayscale_on.png")
fun icFilterGrayscaleOn() = painterResource("images/grayscale_on.png")
@Composable
fun icFilterGrayscaleOff() = imageResource("images/grayscale_off.png")
fun icFilterGrayscaleOff() = painterResource("images/grayscale_off.png")
@Composable
fun icFilterPixelOn() = imageResource("images/pixel_on.png")
fun icFilterPixelOn() = painterResource("images/pixel_on.png")
@Composable
fun icFilterPixelOff() = imageResource("images/pixel_off.png")
fun icFilterPixelOff() = painterResource("images/pixel_off.png")
@Composable
fun icFilterBlurOn() = imageResource("images/blur_on.png")
fun icFilterBlurOn() = painterResource("images/blur_on.png")
@Composable
fun icFilterBlurOff() = imageResource("images/blur_off.png")
fun icFilterBlurOff() = painterResource("images/blur_off.png")
@Composable
fun icFilterUnknown() = imageResource("images/filter_unknown.png")
fun icFilterUnknown() = painterResource("images/filter_unknown.png")
private var icon: BufferedImage? = null
fun icAppRounded(): BufferedImage {
if (icon != null) {
return icon!!
}
try {
val imageRes = "images/ic_imageviewer_round.png"
val img = Thread.currentThread().contextClassLoader.getResource(imageRes)
val bitmap: BufferedImage? = ImageIO.read(img)
if (bitmap != null) {
icon = bitmap
return bitmap
}
} catch (e: Exception) {
e.printStackTrace()
}
return BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB)
}
@Composable
fun icAppRounded() = painterResource("images/ic_imageviewer_round.png")

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
import androidx.compose.desktop.AppManager
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.window.WindowSize
import androidx.compose.ui.unit.dp
import java.awt.Dimension
import java.awt.Graphics2D
import java.awt.Rectangle
@ -14,6 +14,9 @@ import javax.imageio.ImageIO
import java.awt.image.BufferedImageOp
import java.awt.image.ConvolveOp
import java.awt.image.Kernel
import kotlin.math.pow
import kotlin.math.roundToInt
import example.imageviewer.view.DragHandler
fun scaleBitmapAspectRatio(
bitmap: BufferedImage,
@ -38,10 +41,10 @@ fun scaleBitmapAspectRatio(
return result
}
fun getDisplayBounds(bitmap: BufferedImage): Rectangle {
fun getDisplayBounds(bitmap: BufferedImage, windowSize: WindowSize): Rectangle {
val boundW: Float = displayWidth().toFloat()
val boundH: Float = displayHeight().toFloat()
val boundW: Float = windowSize.width.value.toFloat()
val boundH: Float = windowSize.height.value.toFloat()
val ratioX: Float = bitmap.width / boundW
val ratioY: Float = bitmap.height / boundH
@ -108,22 +111,6 @@ fun applyBlurFilter(bitmap: BufferedImage): BufferedImage {
)
}
fun displayWidth(): Int {
val window = AppManager.focusedWindow
if (window != null) {
return window.width
}
return 0
}
fun displayHeight(): Int {
val window = AppManager.focusedWindow
if (window != null) {
return window.height
}
return 0
}
fun toByteArray(bitmap: BufferedImage) : ByteArray {
val baos = ByteArrayOutputStream()
ImageIO.write(bitmap, "png", baos)
@ -134,11 +121,86 @@ fun cropImage(bitmap: BufferedImage, crop: Rectangle) : BufferedImage {
return bitmap.getSubimage(crop.x, crop.y, crop.width, crop.height)
}
fun getPreferredWindowSize(desiredWidth: Int, desiredHeight: Int): IntSize {
fun cropBitmapByScale(
bitmap: BufferedImage,
size: WindowSize,
scale: Float,
drag: DragHandler
): BufferedImage {
val crop = cropBitmapByBounds(
bitmap,
getDisplayBounds(bitmap, size),
size,
scale,
drag
)
return cropImage(
bitmap,
Rectangle(crop.x, crop.y, crop.width - crop.x, crop.height - crop.y)
)
}
fun cropBitmapByBounds(
bitmap: BufferedImage,
bounds: Rectangle,
size: WindowSize,
scaleFactor: Float,
drag: DragHandler
): Rectangle {
if (scaleFactor <= 1f) {
return Rectangle(0, 0, bitmap.width, bitmap.height)
}
var scale = scaleFactor.toDouble().pow(1.4)
var boundW = (bounds.width / scale).roundToInt()
var boundH = (bounds.height / scale).roundToInt()
scale *= size.width.value / bounds.width.toDouble()
val offsetX = drag.getAmount().x / scale
val offsetY = drag.getAmount().y / scale
if (boundW > bitmap.width) {
boundW = bitmap.width
}
if (boundH > bitmap.height) {
boundH = bitmap.height
}
val invisibleW = bitmap.width - boundW
var leftOffset = (invisibleW / 2.0 - offsetX).roundToInt()
if (leftOffset > invisibleW) {
leftOffset = invisibleW
drag.getAmount().x = -((invisibleW / 2.0) * scale).roundToInt().toFloat()
}
if (leftOffset < 0) {
drag.getAmount().x = ((invisibleW / 2.0) * scale).roundToInt().toFloat()
leftOffset = 0
}
val invisibleH = bitmap.height - boundH
var topOffset = (invisibleH / 2 - offsetY).roundToInt()
if (topOffset > invisibleH) {
topOffset = invisibleH
drag.getAmount().y = -((invisibleH / 2.0) * scale).roundToInt().toFloat()
}
if (topOffset < 0) {
drag.getAmount().y = ((invisibleH / 2.0) * scale).roundToInt().toFloat()
topOffset = 0
}
return Rectangle(leftOffset, topOffset, leftOffset + boundW, topOffset + boundH)
}
fun getPreferredWindowSize(desiredWidth: Int, desiredHeight: Int): WindowSize {
val screenSize: Dimension = Toolkit.getDefaultToolkit().screenSize
val preferredWidth: Int = (screenSize.width * 0.8f).toInt()
val preferredHeight: Int = (screenSize.height * 0.8f).toInt()
val width: Int = if (desiredWidth < preferredWidth) desiredWidth else preferredWidth
val height: Int = if (desiredHeight < preferredHeight) desiredHeight else preferredHeight
return IntSize(width, height)
return WindowSize(width.dp, height.dp)
}

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

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

75
examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/MainScreen.kt

@ -35,8 +35,9 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import example.imageviewer.ResString
@ -59,51 +60,27 @@ import example.imageviewer.style.icRefresh
import example.imageviewer.utils.toByteArray
@Composable
fun setMainScreen(content: ContentState) {
if (content.isContentReady()) {
fun MainScreen(content: ContentState) {
Column {
setTopContent(content)
setScrollableArea(content)
}
} else {
setLoadingScreen(content)
}
}
@Composable
private fun setLoadingScreen(content: ContentState) {
Box {
Column {
setTopContent(content)
}
Box(modifier = Modifier.align(Alignment.Center)) {
Surface(color = DarkGray, elevation = 4.dp, shape = CircleShape) {
CircularProgressIndicator(
modifier = Modifier.size(50.dp).padding(3.dp, 3.dp, 4.dp, 4.dp),
color = DarkGreen
)
TopContent(content)
ScrollableArea(content)
}
}
Text(
text = ResString.loading,
modifier = Modifier.align(Alignment.Center).offset(0.dp, 70.dp),
style = MaterialTheme.typography.body1,
color = Foreground
)
if (!content.isContentReady()) {
LoadingScreen(ResString.loading)
}
}
@Composable
fun setTopContent(content: ContentState) {
setTitleBar(text = ResString.appName, content = content)
setPreviewImageUI(content)
setSpacer(h = 10)
setDivider()
setSpacer(h = 5)
fun TopContent(content: ContentState) {
TitleBar(text = ResString.appName, content = content)
PreviewImage(content)
Spacer(modifier = Modifier.height(10.dp))
Divider()
Spacer(modifier = Modifier.height(5.dp))
}
@Composable
fun setTitleBar(text: String, content: ContentState) {
fun TitleBar(text: String, content: ContentState) {
val refreshButtonHover = remember { mutableStateOf(false) }
TopAppBar(
backgroundColor = DarkGreen,
@ -119,6 +96,7 @@ fun setTitleBar(text: String, content: ContentState) {
modifier = Modifier.padding(end = 20.dp).align(Alignment.CenterVertically),
shape = CircleShape
) {
Tooltip(ResString.refresh) {
Clickable(
modifier = Modifier.hover(
onEnter = {
@ -145,11 +123,12 @@ fun setTitleBar(text: String, content: ContentState) {
}
}
}
}
})
}
@Composable
fun setPreviewImageUI(content: ContentState) {
fun PreviewImage(content: ContentState) {
Clickable(
modifier = Modifier.background(color = DarkGray),
onClick = {
@ -165,9 +144,8 @@ fun setPreviewImageUI(content: ContentState) {
Image(
if (content.isMainImageEmpty())
icEmpty()
else org.jetbrains.skija.Image.makeFromEncoded(
toByteArray(content.getSelectedImage())
).asImageBitmap(),
else
BitmapPainter(content.getSelectedImage()),
contentDescription = null,
modifier = Modifier
.fillMaxWidth().padding(start = 1.dp, top = 1.dp, end = 1.dp, bottom = 5.dp),
@ -178,7 +156,7 @@ fun setPreviewImageUI(content: ContentState) {
}
@Composable
fun setMiniatureUI(
fun Miniature(
picture: Picture,
content: ContentState
) {
@ -265,7 +243,7 @@ fun setMiniatureUI(
}
@Composable
fun setScrollableArea(content: ContentState) {
fun ScrollableArea(content: ContentState) {
Box(
modifier = Modifier.fillMaxSize()
.padding(end = 8.dp)
@ -275,7 +253,7 @@ fun setScrollableArea(content: ContentState) {
var index = 1
Column {
for (picture in content.getMiniatures()) {
setMiniatureUI(
Miniature(
picture = picture,
content = content
)
@ -293,16 +271,9 @@ fun setScrollableArea(content: ContentState) {
}
@Composable
fun setDivider() {
fun Divider() {
Divider(
color = LightGray,
modifier = Modifier.padding(start = 10.dp, end = 10.dp)
)
}
@Composable
fun setSpacer(h: Int) {
Spacer(modifier = Modifier.height(h.dp))
}

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

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

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

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

@ -29,8 +29,6 @@ compose.desktop {
packageName = "ImageViewer"
packageVersion = "1.0.0"
modules("jdk.crypto.ec")
val iconsRoot = project.file("../common/src/desktopMain/resources/images")
macOS {
iconFile.set(iconsRoot.resolve("icon-mac.icns"))

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

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

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

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

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
plugins {
id("org.jetbrains.intellij") version "0.6.5"
id("org.jetbrains.intellij") version "1.1.4"
java
kotlin("jvm") version "1.5.10"
kotlin("jvm") version "1.5.21"
// __LATEST_COMPOSE_RELEASE_VERSION__
id("org.jetbrains.compose") version "0.4.0"
id("org.jetbrains.compose") version "1.0.0-alpha1"
id("idea")
}
group = "org.example"
@ -13,24 +14,18 @@ version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
google()
maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") }
}
dependencies {
implementation(kotlin("stdlib"))
implementation("org.jetbrains.compose.material:material:")
implementation(compose.desktop.currentOs)
testCompile("junit", "junit", "4.12")
testImplementation("junit", "junit", "4.12")
}
// See https://github.com/JetBrains/gradle-intellij-plugin/
intellij {
version = "2021.1"
}
tasks.getByName<org.jetbrains.intellij.tasks.PatchPluginXmlTask>("patchPluginXml") {
changeNotes("""
Add change notes here.<br>
<em>most HTML tags may be used</em>""")
version.set("2021.2")
}
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
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

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

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

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