akurasov
3 years ago
586 changed files with 12995 additions and 8303 deletions
@ -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 |
||||
|
@ -1,3 +1,3 @@
|
||||
# __LATEST_COMPOSE_RELEASE_VERSION__ |
||||
compose.version=0.4.0 |
||||
compose.version=1.0.0-alpha1 |
||||
kotlin.code.style=official |
||||
|
@ -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 |
||||
|
@ -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 |
||||
|
@ -1 +1 @@
|
||||
Subproject commit 94cefabe7303d41aef797722ee3ab331a21689aa |
||||
Subproject commit aadb6bb9988bd5b232b2922fa5a248b823f0d5a5 |
@ -1 +1 @@
|
||||
Subproject commit cd6860e33655776f6533790a27cd37eb04b40e40 |
||||
Subproject commit 1b20aa551446123340cb42b4eb21d2f2797e608a |
@ -1 +1 @@
|
||||
Subproject commit f37dc6b42fe7838e9e37fbe8a9eb063a1550acd8 |
||||
Subproject commit 818a882ba70e8603d6a22b17d421c9049926da4c |
@ -0,0 +1,8 @@
|
||||
#!/bin/bash |
||||
|
||||
cd "$(dirname "$0")" |
||||
. ./prepare |
||||
|
||||
pushd .. |
||||
./gradlew buildNativeDemo $COMPOSE_DEFAULT_GRADLE_ARGS "$@" || exit 1 |
||||
popd |
@ -0,0 +1,8 @@
|
||||
#!/bin/bash |
||||
|
||||
cd "$(dirname "$0")" |
||||
. ./prepare |
||||
|
||||
pushd .. |
||||
./gradlew testRuntimeNative $COMPOSE_DEFAULT_GRADLE_ARGS "$@" || exit 1 |
||||
popd |
@ -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) |
@ -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,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() |
||||
} |
||||
|
@ -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() |
||||
} |
||||
|
@ -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 |
||||
) |
||||
} |
||||
} |
@ -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") |
||||
|
@ -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 |
||||
} |
||||
} |
@ -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) |
||||
} |
@ -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 |
||||
) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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() |
||||
} |
||||
} |
@ -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() |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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 |
||||
|
@ -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) {} |
||||
} |
||||
) |
||||
} |
||||
} |
@ -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 |
Before Width: | Height: | Size: 206 KiB After Width: | Height: | Size: 206 KiB |
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 152 KiB |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue