diff --git a/examples/imageviewer/build.gradle.kts b/examples/imageviewer/build.gradle.kts index 455aeb2843..9ba01b06d2 100755 --- a/examples/imageviewer/build.gradle.kts +++ b/examples/imageviewer/build.gradle.kts @@ -1,27 +1,23 @@ buildscript { + val composeVersion = System.getenv("COMPOSE_TEMPLATE_COMPOSE_VERSION") ?: "0.5.0-build270" + repositories { - mavenLocal().mavenContent { - includeModule("org.jetbrains.compose", "compose-gradle-plugin") - } google() mavenCentral() maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") } dependencies { - // __LATEST_COMPOSE_RELEASE_VERSION__ - classpath("org.jetbrains.compose:compose-gradle-plugin:0.4.0") + classpath("org.jetbrains.compose:compose-gradle-plugin:$composeVersion") classpath("com.android.tools.build:gradle:4.0.1") - // __KOTLIN_COMPOSE_VERSION__ - classpath(kotlin("gradle-plugin", version = "1.5.10")) + classpath(kotlin("gradle-plugin", version = "1.5.21")) } } allprojects { repositories { - mavenLocal() google() mavenCentral() maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") } -} +} \ No newline at end of file diff --git a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/DesktopContentState.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/DesktopContentState.kt index 82cb159e7b..86009cff1b 100644 --- a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/DesktopContentState.kt +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/DesktopContentState.kt @@ -3,6 +3,7 @@ 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 example.imageviewer.ResString import example.imageviewer.core.FilterType import example.imageviewer.model.filtration.FiltersManager @@ -19,10 +20,12 @@ import javax.swing.SwingUtilities.invokeLater object ContentState : RememberObserver { + lateinit var windowState: WindowState private lateinit var repository: ImageRepository private lateinit var uriRepository: String - fun applyContent(uriRepository: String): ContentState { + fun applyContent(state: WindowState, uriRepository: String): ContentState { + windowState = state if (this::uriRepository.isInitialized && this.uriRepository == uriRepository) { return this } diff --git a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/style/Decoration.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/style/Decoration.kt index f455f3e1c1..7c06d90124 100755 --- a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/style/Decoration.kt +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/style/Decoration.kt @@ -1,58 +1,42 @@ package example.imageviewer.style import androidx.compose.runtime.Composable -import androidx.compose.ui.res.imageResource +import androidx.compose.ui.res.painterResource import java.awt.image.BufferedImage import javax.imageio.ImageIO @Composable -fun icEmpty() = imageResource("images/empty.png") +fun icEmpty() = painterResource("images/empty.png") @Composable -fun icBack() = imageResource("images/back.png") +fun icBack() = painterResource("images/back.png") @Composable -fun icRefresh() = imageResource("images/refresh.png") +fun icRefresh() = painterResource("images/refresh.png") @Composable -fun icDots() = imageResource("images/dots.png") +fun icDots() = painterResource("images/dots.png") @Composable -fun icFilterGrayscaleOn() = imageResource("images/grayscale_on.png") +fun icFilterGrayscaleOn() = painterResource("images/grayscale_on.png") @Composable -fun icFilterGrayscaleOff() = imageResource("images/grayscale_off.png") +fun icFilterGrayscaleOff() = painterResource("images/grayscale_off.png") @Composable -fun icFilterPixelOn() = imageResource("images/pixel_on.png") +fun icFilterPixelOn() = painterResource("images/pixel_on.png") @Composable -fun icFilterPixelOff() = imageResource("images/pixel_off.png") +fun icFilterPixelOff() = painterResource("images/pixel_off.png") @Composable -fun icFilterBlurOn() = imageResource("images/blur_on.png") +fun icFilterBlurOn() = painterResource("images/blur_on.png") @Composable -fun icFilterBlurOff() = imageResource("images/blur_off.png") +fun icFilterBlurOff() = painterResource("images/blur_off.png") @Composable -fun icFilterUnknown() = imageResource("images/filter_unknown.png") +fun icFilterUnknown() = painterResource("images/filter_unknown.png") -private var icon: BufferedImage? = null -fun icAppRounded(): BufferedImage { - if (icon != null) { - return icon!! - } - try { - val imageRes = "images/ic_imageviewer_round.png" - val img = Thread.currentThread().contextClassLoader.getResource(imageRes) - val bitmap: BufferedImage? = ImageIO.read(img) - if (bitmap != null) { - icon = bitmap - return bitmap - } - } catch (e: Exception) { - e.printStackTrace() - } - return BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB) -} +@Composable +fun icAppRounded() = painterResource("images/ic_imageviewer_round.png") diff --git a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/Application.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/Application.kt deleted file mode 100644 index dc736047e5..0000000000 --- a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/Application.kt +++ /dev/null @@ -1,158 +0,0 @@ -package example.imageviewer.utils - -import androidx.compose.desktop.AppManager -import androidx.compose.desktop.AppWindow -import androidx.compose.desktop.WindowEvents -import androidx.compose.runtime.* -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.window.v1.MenuBar -import kotlinx.coroutines.* -import kotlinx.coroutines.swing.Swing -import java.awt.image.BufferedImage - -fun Application( - content: @Composable ApplicationScope.() -> Unit -) { - GlobalScope.launch(Dispatchers.Swing + ImmediateFrameClock()) { - AppManager.setEvents(onWindowsEmpty = null) - - withRunningRecomposer { recomposer -> - val latch = CompletableDeferred() - val applier = ApplicationApplier { latch.complete(Unit) } - - val composition = Composition(applier, recomposer) - try { - val scope = ApplicationScope(recomposer) - - composition.setContent { scope.content() } - - latch.join() - } finally { - composition.dispose() - } - } - } -} - -class ApplicationScope internal constructor(private val recomposer: Recomposer) { - @Composable - fun ComposableWindow( - title: String = "JetpackDesktopWindow", - size: IntSize = IntSize(800, 600), - location: IntOffset = IntOffset.Zero, - centered: Boolean = true, - icon: BufferedImage? = null, - menuBar: MenuBar? = null, - undecorated: Boolean = false, - resizable: Boolean = true, - events: WindowEvents = WindowEvents(), - onDismissRequest: (() -> Unit)? = null, - content: @Composable () -> Unit = {} - ) { - var isOpened by remember { mutableStateOf(true) } - if (!isOpened) return - ComposeNode( - factory = { - val window = AppWindow( - title = title, - size = size, - location = location, - centered = centered, - icon = icon, - menuBar = menuBar, - undecorated = undecorated, - resizable = resizable, - events = events, - onDismissRequest = { - onDismissRequest?.invoke() - isOpened = false - } - ) - window.show(recomposer, content) - window - }, - update = { - set(title) { setTitle(it) } - set(size) { setSize(it.width, it.height) } - // set(location) { setLocation(it.x, it.y) } - set(icon) { setIcon(it) } - // set(menuBar) { if (it != null) setMenuBar(it) else removeMenuBar() } - // set(resizable) { setResizable(it) } - // set(events) { setEvents(it) } - // set(onDismissRequest) { setDismiss(it) } - } - ) - } -} - -private class ImmediateFrameClock : MonotonicFrameClock { - override suspend fun withFrameNanos( - onFrame: (frameTimeNanos: Long) -> R - ) = onFrame(System.nanoTime()) -} - -private class ApplicationApplier( - private val onWindowsEmpty: () -> Unit -) : Applier { - private val windows = mutableListOf() - - override var current: AppWindow? = null - - override fun insertBottomUp(index: Int, instance: AppWindow?) { - requireNotNull(instance) - check(current == null) { "Windows cannot be nested!" } - windows.add(index, instance) - } - - override fun remove(index: Int, count: Int) { - repeat(count) { - val window = windows.removeAt(index) - if (!window.isClosed) { - window.close() - } - } - } - - override fun move(from: Int, to: Int, count: Int) { - if (from > to) { - var current = to - repeat(count) { - val node = windows.removeAt(from) - windows.add(current, node) - current++ - } - } else { - repeat(count) { - val node = windows.removeAt(from) - windows.add(to - 1, node) - } - } - } - - override fun clear() { - windows.forEach { if (!it.isClosed) it.close() } - windows.clear() - } - - override fun onEndChanges() { - if (windows.isEmpty()) { - onWindowsEmpty() - } - } - - override fun down(node: AppWindow?) { - requireNotNull(node) - check(current == null) { "Windows cannot be nested!" } - current = node - } - - override fun up() { - check(current != null) { "Windows cannot be nested!" } - current = null - } - - override fun insertTopDown(index: Int, instance: AppWindow?) { - // ignored. Building tree bottom-up - } -} diff --git a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/GraphicsMath.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/GraphicsMath.kt index 119a2457f4..7b103af1fe 100755 --- a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/GraphicsMath.kt +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/GraphicsMath.kt @@ -1,7 +1,7 @@ package example.imageviewer.utils -import androidx.compose.desktop.AppManager -import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.window.WindowSize +import androidx.compose.ui.unit.dp import java.awt.Dimension import java.awt.Graphics2D import java.awt.Rectangle @@ -38,10 +38,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 +108,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 +118,11 @@ 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 getPreferredWindowSize(desiredWidth: Int, desiredHeight: Int): WindowSize { val screenSize: Dimension = Toolkit.getDefaultToolkit().screenSize val preferredWidth: Int = (screenSize.width * 0.8f).toInt() val preferredHeight: Int = (screenSize.height * 0.8f).toInt() val width: Int = if (desiredWidth < preferredWidth) desiredWidth else preferredWidth val height: Int = if (desiredHeight < preferredHeight) desiredHeight else preferredHeight - return IntSize(width, height) + return WindowSize(width.dp, height.dp) } diff --git a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/FullImageScreen.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/FullImageScreen.kt index 582294e010..d335e0f1be 100755 --- a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/FullImageScreen.kt +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/FullImageScreen.kt @@ -25,13 +25,19 @@ 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.asImageBitmap import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.painter.Painter 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.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 @@ -50,11 +56,9 @@ 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 @@ -192,7 +196,7 @@ fun FilterButton( } @Composable -fun getFilterImage(type: FilterType, content: ContentState): ImageBitmap { +fun getFilterImage(type: FilterType, content: ContentState): Painter { return when (type) { FilterType.GrayScale -> if (content.isFilterEnabled(type)) icFilterGrayscaleOn() else icFilterGrayscaleOff() @@ -201,6 +205,7 @@ fun getFilterImage(type: FilterType, content: ContentState): ImageBitmap { } } +@OptIn(ExperimentalComposeUiApi::class) @Composable fun setImage(content: ContentState) { val drag = remember { DragHandler() } @@ -214,13 +219,14 @@ fun setImage(content: ContentState) { Zoomable( onScale = scale, modifier = Modifier.fillMaxSize() - .shortcuts { - on(Key(KeyEvent.VK_LEFT)) { - content.swipePrevious() - } - on(Key(KeyEvent.VK_RIGHT)) { - content.swipeNext() + .onPreviewKeyEvent { + if (it.type == KeyEventType.KeyUp) { + when (it.key) { + Key.DirectionLeft -> content.swipePrevious() + Key.DirectionRight -> content.swipeNext() + } } + false } ) { val bitmap = imageByGesture(content, scale, drag) @@ -240,15 +246,20 @@ fun imageByGesture( scale: ScaleHandler, drag: DragHandler ): ImageBitmap { - val bitmap = cropBitmapByScale(content.getSelectedImage(), scale.factor.value, drag) + val bitmap = cropBitmapByScale(content.getSelectedImage(), content.windowState.size, scale.factor.value, drag) return org.jetbrains.skija.Image.makeFromEncoded(toByteArray(bitmap)).asImageBitmap() } -private fun cropBitmapByScale(bitmap: BufferedImage, scale: Float, drag: DragHandler): BufferedImage { - +private fun cropBitmapByScale( + bitmap: BufferedImage, + size: WindowSize, + scale: Float, + drag: DragHandler +): BufferedImage { val crop = cropBitmapByBounds( bitmap, - getDisplayBounds(bitmap), + getDisplayBounds(bitmap, size), + size, scale, drag ) @@ -261,6 +272,7 @@ private fun cropBitmapByScale(bitmap: BufferedImage, scale: Float, drag: DragHan private fun cropBitmapByBounds( bitmap: BufferedImage, bounds: Rectangle, + size: WindowSize, scaleFactor: Float, drag: DragHandler ): Rectangle { @@ -274,7 +286,7 @@ private fun cropBitmapByBounds( var boundW = (bounds.width / scale).roundToInt() var boundH = (bounds.height / scale).roundToInt() - scale *= displayWidth() / bounds.width.toDouble() + scale *= size.width.value / bounds.width.toDouble() val offsetX = drag.getAmount().x / scale val offsetY = drag.getAmount().y / scale diff --git a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/MainScreen.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/MainScreen.kt index cf3401c7ae..0ef28c0433 100755 --- a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/MainScreen.kt +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/MainScreen.kt @@ -35,8 +35,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import example.imageviewer.ResString @@ -165,9 +166,9 @@ fun setPreviewImageUI(content: ContentState) { Image( if (content.isMainImageEmpty()) icEmpty() - else org.jetbrains.skija.Image.makeFromEncoded( + else BitmapPainter(org.jetbrains.skija.Image.makeFromEncoded( toByteArray(content.getSelectedImage()) - ).asImageBitmap(), + ).asImageBitmap()), contentDescription = null, modifier = Modifier .fillMaxWidth().padding(start = 1.dp, top = 1.dp, end = 1.dp, bottom = 5.dp), diff --git a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Zoomable.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Zoomable.kt index f255b8b0c8..2a6f865e4c 100644 --- a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Zoomable.kt +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Zoomable.kt @@ -1,19 +1,24 @@ 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, @@ -24,19 +29,18 @@ fun Zoomable( Surface( color = Transparent, - modifier = modifier.shortcuts { - on(Key.I) { - onScale.onScale(1.2f) - } - on(Key.O) { - onScale.onScale(0.8f) - } - on(Key.R) { - onScale.resetFactor() + modifier = modifier.onPreviewKeyEvent { + if (it.type == KeyEventType.KeyUp) { + when (it.key) { + Key.I -> onScale.onScale(1.2f) + Key.O -> onScale.onScale(0.8f) + Key.R -> onScale.resetFactor() + } } + false } .focusRequester(focusRequester) - .focusModifier() + .focusable() .pointerInput(Unit) { detectTapGestures(onDoubleTap = { onScale.resetFactor() }) { focusRequester.requestFocus() diff --git a/examples/imageviewer/desktop/src/jvmMain/kotlin/example/imageviewer/Main.kt b/examples/imageviewer/desktop/src/jvmMain/kotlin/example/imageviewer/Main.kt index a955947865..509d84f310 100644 --- a/examples/imageviewer/desktop/src/jvmMain/kotlin/example/imageviewer/Main.kt +++ b/examples/imageviewer/desktop/src/jvmMain/kotlin/example/imageviewer/Main.kt @@ -1,47 +1,58 @@ package example.imageviewer -import androidx.compose.desktop.DesktopTheme import androidx.compose.material.MaterialTheme import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.WindowState +import androidx.compose.ui.window.WindowPosition +import androidx.compose.ui.window.application +import androidx.compose.ui.window.rememberWindowState import example.imageviewer.model.ContentState import example.imageviewer.style.icAppRounded -import example.imageviewer.utils.Application import example.imageviewer.utils.getPreferredWindowSize import example.imageviewer.view.BuildAppUI import example.imageviewer.view.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) - } + BuildAppUI(content) } } } else { - ComposableWindow( + Window( + onCloseRequest = ::exitApplication, title = "Image Viewer", - size = getPreferredWindowSize(800, 300), + state = WindowState( + position = WindowPosition.Aligned(Alignment.Center), + size = getPreferredWindowSize(800, 300) + ), undecorated = true, icon = icon, ) { MaterialTheme { - DesktopTheme { - SplashUI() - } + SplashUI() } } }