From 2f8d8471869f98d84dcc40e3e36eb850bd6419b4 Mon Sep 17 00:00:00 2001 From: Aleksandras Kostarevas Date: Fri, 27 Sep 2024 22:38:46 +0300 Subject: [PATCH] Add basic resizer for floating mode --- .../futo/inputmethod/latin/uix/ResizerRect.kt | 130 ++++++++++++++++++ .../futo/inputmethod/latin/uix/UixManager.kt | 70 ++++++++++ 2 files changed, 200 insertions(+) create mode 100644 java/src/org/futo/inputmethod/latin/uix/ResizerRect.kt diff --git a/java/src/org/futo/inputmethod/latin/uix/ResizerRect.kt b/java/src/org/futo/inputmethod/latin/uix/ResizerRect.kt new file mode 100644 index 000000000..b66f78078 --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/ResizerRect.kt @@ -0,0 +1,130 @@ +package org.futo.inputmethod.latin.uix + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +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.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.toSize + + +enum class CurrentDraggingTarget { + TopLeft, + TopRight, + BottomLeft, + BottomRight, + Center +} + +private fun CurrentDraggingTarget.computeOffset(size: Size): Offset = when(this) { + CurrentDraggingTarget.TopLeft -> Offset(0.0f, 0.0f) + CurrentDraggingTarget.TopRight -> Offset(size.width, 0.0f) + CurrentDraggingTarget.BottomLeft -> Offset(0.0f, size.height) + CurrentDraggingTarget.BottomRight -> Offset(size.width, size.height) + CurrentDraggingTarget.Center -> Offset(size.width * 0.5f, size.height * 0.5f) +} + +private fun CurrentDraggingTarget.computeOffset(size: IntSize): Offset = + computeOffset(size.toSize()) + +private fun CurrentDraggingTarget.dragDelta(offset: Offset): DragDelta = when(this) { + CurrentDraggingTarget.TopLeft -> DragDelta(left = offset.x, top = offset.y) + CurrentDraggingTarget.TopRight -> DragDelta(right = offset.x, top = offset.y) + CurrentDraggingTarget.BottomLeft -> DragDelta(left = offset.x, bottom = offset.y) + CurrentDraggingTarget.BottomRight -> DragDelta(right = offset.x, bottom = offset.y) + CurrentDraggingTarget.Center -> DragDelta( + left = offset.x, + right = offset.x, + top = offset.y, + bottom = offset.y + ) +} + +data class DragDelta( + val left: Float = 0.0f, + val top: Float = 0.0f, + val right: Float = 0.0f, + val bottom: Float = 0.0f +) + + +@Composable +fun BoxScope.ResizerRect(onDragged: (DragDelta) -> Boolean, showResetApply: Boolean, onApply: () -> Unit, onReset: () -> Unit) { + val shape = RectangleShape + + val draggingState = remember { mutableStateOf(null) } + val wasAccepted = remember { mutableStateOf(true) } + + Box(Modifier + .matchParentSize() + .background(MaterialTheme.colorScheme.background.copy(alpha = 0.5f), shape) + .border(3.dp, MaterialTheme.colorScheme.primary, shape) + .pointerInput(Unit) { + detectDragGestures( + onDragStart = { offset -> + draggingState.value = CurrentDraggingTarget.entries.minBy { + offset.minus(it.computeOffset(size)).getDistanceSquared() + } + }, + onDrag = { _, amount -> + draggingState.value?.let { + wasAccepted.value = onDragged(it.dragDelta(amount)) + } + }, + onDragEnd = { + draggingState.value = null + wasAccepted.value = true + } + ) + } + ) { + val primaryColor = MaterialTheme.colorScheme.primary + val primaryInverseColor = MaterialTheme.colorScheme.inversePrimary + val errorColor = MaterialTheme.colorScheme.error + val radius = with(LocalDensity.current) { 24.dp.toPx() } + + Canvas(Modifier.matchParentSize(), onDraw = { + CurrentDraggingTarget.entries.forEach { + drawCircle( + color = if (!wasAccepted.value) { + errorColor + } else if (draggingState.value == it) { + primaryInverseColor + } else { + primaryColor + }, + radius = radius, + center = it.computeOffset(size) + ) + } + }) + + if (showResetApply) { + Row(Modifier.align(Alignment.BottomCenter).padding(16.dp)) { + TextButton({ onReset() }) { Text("Reset") } + Spacer(Modifier.width(8.dp)) + TextButton({ onApply() }) { Text("Apply") } + } + } + } +} \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/UixManager.kt b/java/src/org/futo/inputmethod/latin/uix/UixManager.kt index 7fbe66d68..d51ecb3a2 100644 --- a/java/src/org/futo/inputmethod/latin/uix/UixManager.kt +++ b/java/src/org/futo/inputmethod/latin/uix/UixManager.kt @@ -41,6 +41,10 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredWidth import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Menu +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface @@ -60,6 +64,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape @@ -703,6 +708,7 @@ class UixManager(private val latinIME: LatinIME) { pointerInputKey: Any?, onDragged: (Offset) -> Unit, onDragEnd: () -> Unit, + onResizerOpen: () -> Unit, content: @Composable BoxScope.(actionBarGap: Dp) -> Unit ) { // Content @@ -720,6 +726,13 @@ class UixManager(private val latinIME: LatinIME) { onDrag = { _, dragAmount -> onDragged(dragAmount)}, onDragEnd = { onDragEnd() }) }) { + + IconButton(onClick = { + onResizerOpen() + }, Modifier.align(Alignment.CenterEnd)) { + Icon(Icons.Default.Menu, contentDescription = "resize") + } + Box( modifier = Modifier.fillMaxWidth(0.6f).height(4.dp) .align(Alignment.TopCenter).background( @@ -736,6 +749,54 @@ class UixManager(private val latinIME: LatinIME) { content: @Composable BoxScope.(actionBarGap: Dp) -> Unit ) = with(LocalDensity.current) { val offset = remember(size) { mutableStateOf(Offset(size.bottomOrigin.first.toFloat(), size.bottomOrigin.second.toFloat())) } + + val resizing = remember { mutableStateOf(false) } + + val onDragDelta: (DragDelta) -> Boolean = remember { { delta -> + // Matching the necessary coordinate space + var deltaX = delta.left + var deltaY = -delta.bottom + var deltaWidth = delta.right - delta.left + var deltaHeight = delta.bottom - delta.top + + var result = true + + // TODO: Limit the values so that we do not go off-screen + // If we have reached a minimum limit, return false + + // Basic limiting for minimum size + val currSettings = latinIME.sizingCalculator.getSavedSettings() + val currSize = Size( + currSettings.floatingWidthDp.dp.toPx(), + currSettings.floatingHeightDp.dp.toPx() + ) + + if(currSize.width + deltaWidth < 200.dp.toPx()) { + deltaWidth = deltaWidth.coerceAtLeast(200.dp.toPx() - currSize.width) + deltaX = 0.0f + result = false + } + + if(currSize.height + deltaHeight < 160.dp.toPx()) { + deltaHeight = deltaHeight.coerceAtLeast(160.dp.toPx() - currSize.height) + deltaY = 0.0f + result = false + } + + latinIME.sizingCalculator.editSavedSettings { settings -> + settings.copy( + floatingBottomOriginDp = Pair( + settings.floatingBottomOriginDp.first + deltaX.toDp().value, + settings.floatingBottomOriginDp.second + deltaY.toDp().value + ), + floatingWidthDp = settings.floatingWidthDp + deltaWidth.toDp().value, + floatingHeightDp = settings.floatingHeightDp + deltaHeight.toDp().value + ) + } + + result + } } + OffsetPositioner(offset.value) { KeyboardSurface( requiredWidthPx = size.width, @@ -778,9 +839,18 @@ class UixManager(private val latinIME: LatinIME) { ) } }, + onResizerOpen = { + resizing.value = true + }, content = content ) } + + if(resizing.value) { + ResizerRect(onDragDelta, showResetApply = true, onApply = { + resizing.value = false + }, onReset = { }) + } } } }