package com.vgmlr.varnish
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import coil.request.ImageRequest
import coil.size.Precision
import coil.size.Scale
import java.io.File
import kotlin.math.abs
import kotlin.math.min
import kotlin.math.roundToInt
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
private const val CROP_PX = 2048
private const val MIN_PX = 48f
private const val G_L = 1
private const val G_T = 2
private const val G_R = 4
private const val G_B = 8
private const val G_MOVE = 16
private val TOUCH = 28.dp
private val HANDLE = 12.dp
private val SHADE = Color.Black.copy(alpha = 0.55f)
private val GUIDE = Color.White.copy(alpha = 0.35f)
@Composable
fun CropScreen(vm: VarnishViewModel, screen: Screen.Crop) {
val context = LocalContext.current
val file = remember(screen.path) { File(screen.path) }
val dims by produceState<Pair<Int, Int>?>(null, screen.path) {
value = withContext(Dispatchers.IO) { VarnishCodec.bounds(file) }
}
val request = remember(screen.path) {
ImageRequest.Builder(context)
.data(file)
.size(CROP_PX)
.precision(Precision.INEXACT)
.scale(Scale.FIT)
.build()
}
var box by remember { mutableStateOf(IntSize.Zero) }
var l by rememberSaveable { mutableFloatStateOf(0f) }
var t by rememberSaveable { mutableFloatStateOf(0f) }
var r by rememberSaveable { mutableFloatStateOf(1f) }
var b by rememberSaveable { mutableFloatStateOf(1f) }
var busy by remember { mutableStateOf(false) }
val fit: Rect? = remember(dims, box) {
val d = dims ?: return@remember null
if (box.width == 0 || box.height == 0) return@remember null
val s = min(box.width / d.first.toFloat(), box.height / d.second.toFloat())
val w = d.first * s
val h = d.second * s
Rect(Offset((box.width - w) / 2f, (box.height - h) / 2f), Size(w, h))
}
val top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + TOP_BAR_H + GAP
val bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + EDGE
Box(Modifier.fillMaxSize().background(PageBg)) {
Box(
Modifier
.fillMaxSize()
.padding(top = top, bottom = bottom, start = EDGE, end = EDGE)
.onSizeChanged { box = it }
) {
AsyncImage(
model = request,
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier.fillMaxSize()
)
Canvas(
Modifier
.fillMaxSize()
.pointerInput(fit) {
val f = fit ?: return@pointerInput
val touch = TOUCH.toPx()
var g = 0
detectDragGestures(
onDragStart = { p ->
val c = Rect(
f.left + l * f.width, f.top + t * f.height,
f.left + r * f.width, f.top + b * f.height
)
g = grip(p, c, touch)
},
onDragEnd = { g = 0 },
onDragCancel = { g = 0 }
) { change, drag ->
if (g == 0) return@detectDragGestures
change.consume()
val dx = drag.x / f.width
val dy = drag.y / f.height
val mw = MIN_PX / f.width
val mh = MIN_PX / f.height
if (g == G_MOVE) {
val w = r - l
val h = b - t
val nl = (l + dx).coerceIn(0f, 1f - w)
val nt = (t + dy).coerceIn(0f, 1f - h)
l = nl
r = nl + w
t = nt
b = nt + h
} else {
if (g and G_L != 0) l = (l + dx).coerceIn(0f, r - mw)
if (g and G_R != 0) r = (r + dx).coerceIn(l + mw, 1f)
if (g and G_T != 0) t = (t + dy).coerceIn(0f, b - mh)
if (g and G_B != 0) b = (b + dy).coerceIn(t + mh, 1f)
}
}
}
) {
val f = fit ?: return@Canvas
val c = Rect(
f.left + l * f.width, f.top + t * f.height,
f.left + r * f.width, f.top + b * f.height
)
drawRect(SHADE, Offset(f.left, f.top), Size(f.width, c.top - f.top))
drawRect(SHADE, Offset(f.left, c.bottom), Size(f.width, f.bottom - c.bottom))
drawRect(SHADE, Offset(f.left, c.top), Size(c.left - f.left, c.height))
drawRect(SHADE, Offset(c.right, c.top), Size(f.right - c.right, c.height))
val sw = 1.dp.toPx()
for (k in 1..2) {
val x = c.left + c.width * k / 3f
val y = c.top + c.height * k / 3f
drawLine(GUIDE, Offset(x, c.top), Offset(x, c.bottom), sw)
drawLine(GUIDE, Offset(c.left, y), Offset(c.right, y), sw)
}
drawRect(Color.White, c.topLeft, c.size, style = Stroke(2.dp.toPx()))
val hs = HANDLE.toPx()
listOf(c.topLeft, c.topRight, c.bottomLeft, c.bottomRight).forEach { p ->
drawRect(Color.White, Offset(p.x - hs / 2f, p.y - hs / 2f), Size(hs, hs))
}
}
}
TopBar(
onBack = { vm.back() },
trailing = {
VButton(Icons.Default.Check, "Save", enabled = !busy && dims != null) {
val d = dims ?: return@VButton
val px = android.graphics.Rect(
(l * d.first).roundToInt(),
(t * d.second).roundToInt(),
(r * d.first).roundToInt(),
(b * d.second).roundToInt()
)
busy = true
vm.crop(screen.path, px) { busy = false }
}
}
) {
TitlePlate(file.name, Modifier.weight(1f))
}
}
}
private fun grip(p: Offset, c: Rect, touch: Float): Int {
val inX = p.x in (c.left - touch)..(c.right + touch)
val inY = p.y in (c.top - touch)..(c.bottom + touch)
val dl = abs(p.x - c.left)
val dr = abs(p.x - c.right)
val dt = abs(p.y - c.top)
val db = abs(p.y - c.bottom)
var g = 0
if (inY && min(dl, dr) <= touch) g = g or if (dl <= dr) G_L else G_R
if (inX && min(dt, db) <= touch) g = g or if (dt <= db) G_T else G_B
if (g == 0 && c.contains(p)) g = G_MOVE
return g
}