package com.vgmlr.wedge
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.Undo
import androidx.compose.material.icons.automirrored.outlined.Backspace
import androidx.compose.material.icons.automirrored.outlined.Sort
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Create
import androidx.compose.material.icons.filled.StrikethroughS
import androidx.compose.material.icons.outlined.*
import androidx.compose.material.icons.outlined.Park
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.foundation.border
import androidx.compose.ui.res.vectorResource
internal val BTN_SIZE = 54.dp
internal val BTN_RADIUS = 6.dp
internal val CHIP_RADIUS = 6.dp
internal val BTN_ICON = 24.dp
internal val BTN_PAD = 12.dp
internal const val BTN_ALPHA = 1.0f
private val STRIKE_DY = 2.dp // strike button increase
internal val BAR_EDGE = 12.dp
private val STACK_BTN_BG = Color(0xFF486860)
private val SETTING_BTN_BG = Color(0xFF272A30)
private val SYNC_INK = Color(0xFFFFFFFF)
internal val CRYPT_BG = Color(0xFF880000)
private const val CRYPT_HOLD_MS = 500L // long press encrypt
internal val LocalRuleAlpha =
staticCompositionLocalOf { WedgeConfig.GUIDE_OPACITY_DEFAULT / 100f }
private val STACK_BTN = 48.dp
private val STACK_GAP = 8.dp
private val STACK_ICON = 28.dp
private val STACK_PAD = 8.dp
private val STACK_SHAPE_ICON = 24.dp
private val STACK_GRAIN_ICON = 30.dp
private val STACK_DRAW_ICON = 30.dp
private val STACK_STRIKE_ICON = 34.dp
private val STACK_STRIKE_PAD = 6.dp
private val BAR_LABEL = 17.sp
private val BAR_LABEL_PAD = 14.dp
internal val TOP_BAR_H = STACK_BTN + STACK_GAP * 2
private const val STACK_ALPHA_IME = 0.7f
private const val GROUP_IDLE_MS = 5_000L
private const val ANIM_IN = 220
private const val FADE_IN = 180
private const val ANIM_OUT = 220
private const val FADE_OUT = 140
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun WedgeButton(
icon: ImageVector,
desc: String?,
modifier: Modifier = Modifier,
size: Dp = BTN_SIZE,
iconSize: Dp = BTN_ICON,
iconPad: Dp = BTN_PAD,
iconOffsetY: Dp = 0.dp,
enabled: Boolean = true,
alpha: Float = BTN_ALPHA,
focusable: Boolean = true,
bg: Color = Color.Unspecified,
tint: Color = Color.Unspecified,
onLongClick: (() -> Unit)? = null,
longPressMs: Long = CRYPT_HOLD_MS,
onClick: () -> Unit
) {
val fill = if (bg == Color.Unspecified) colorResource(id = R.color.primary_color) else bg
val ink = if (tint == Color.Unspecified) colorResource(id = R.color.title_color) else tint
val haptic = LocalHapticFeedback.current
val base = LocalViewConfiguration.current
val config = remember(base, longPressMs) {
object : ViewConfiguration by base {
override val longPressTimeoutMillis = longPressMs
}
}
val press = if (onLongClick == null) {
Modifier.clickable(enabled = enabled, onClick = onClick)
} else {
Modifier.combinedClickable(
enabled = enabled,
onLongClick = {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
onLongClick()
},
onClick = onClick
)
}
CompositionLocalProvider(
LocalViewConfiguration provides if (onLongClick == null) base else config
) {
Box(
modifier = modifier
.size(size)
.graphicsLayer(alpha = alpha)
.clip(RoundedCornerShape(BTN_RADIUS))
.background(fill)
.then(if (focusable) Modifier else Modifier.focusProperties { canFocus = false })
.then(press)
.padding(iconPad),
contentAlignment = Alignment.Center
) {
Icon(
icon,
contentDescription = desc,
modifier = Modifier
.size(iconSize)
.offset(y = iconOffsetY),
tint = ink.copy(alpha = if (enabled) 1f else 0.3f)
)
}
}
}
enum class StackGroup { NONE, MENU, ARTICLE, CREATE }
@Composable
private fun StackButton(
icon: ImageVector,
desc: String,
enabled: Boolean = true,
focusable: Boolean = false,
iconSize: Dp = STACK_ICON,
iconPad: Dp = STACK_PAD,
iconOffsetY: Dp = 0.dp,
bg: Color = STACK_BTN_BG,
tint: Color = Color.Unspecified,
onLongClick: (() -> Unit)? = null,
longPressMs: Long = CRYPT_HOLD_MS,
onClick: () -> Unit
) = WedgeButton(
icon, desc,
size = STACK_BTN,
iconSize = iconSize,
iconPad = iconPad,
iconOffsetY = iconOffsetY,
enabled = enabled,
focusable = focusable,
bg = bg,
tint = tint,
onLongClick = onLongClick,
longPressMs = longPressMs,
onClick = onClick
)
@Composable
private fun SyncFlashIcon() {
Box(
modifier = Modifier
.size(STACK_BTN)
.padding(STACK_PAD),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Outlined.Loop,
contentDescription = "Synced",
modifier = Modifier.size(STACK_ICON),
tint = SYNC_INK
)
}
}
@Composable
private fun StackSlot(
visible: Boolean,
from: Alignment.Vertical = Alignment.Bottom,
content: @Composable () -> Unit
) {
AnimatedVisibility(
visible = visible,
enter = expandVertically(tween(ANIM_IN), from) + fadeIn(tween(FADE_IN)),
exit = shrinkVertically(tween(ANIM_OUT), from) + fadeOut(tween(FADE_OUT))
) {
Box(Modifier.padding(top = STACK_GAP)) { content() }
}
}
@Composable
private fun StackColumn(
trigger: @Composable () -> Unit,
slots: @Composable ColumnScope.() -> Unit
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
slots()
Box(Modifier.padding(top = STACK_GAP)) { trigger() }
}
}
@Composable
private fun OverlayFrame(
modifier: Modifier = Modifier,
content: @Composable BoxScope.() -> Unit
) {
Box(
modifier = modifier
.height(STACK_BTN)
.clip(RoundedCornerShape(BTN_RADIUS))
.background(colorResource(id = R.color.calc_bg_color))
.border(EDGE, RoundedCornerShape(BTN_RADIUS)),
content = content
)
}
@Composable
private fun BarPlate(
label: String,
modifier: Modifier = Modifier,
align: Alignment = Alignment.CenterEnd
) {
Box(
modifier
.height(STACK_BTN)
.clip(RoundedCornerShape(BTN_RADIUS))
.background(SETTING_BTN_BG)
.border(EDGE, RoundedCornerShape(BTN_RADIUS))
.padding(horizontal = BAR_LABEL_PAD),
contentAlignment = align
) {
Text(
label,
style = TextStyle(
colorResource(id = R.color.calc_color),
fontSize = BAR_LABEL,
letterSpacing = 1.4.sp
),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
@Composable
private fun BarStack(
onTouch: () -> Unit,
trigger: @Composable () -> Unit,
slots: @Composable ColumnScope.() -> Unit
) {
Column(
modifier = Modifier.pointerInput(Unit) {
awaitEachGesture {
awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
onTouch()
}
},
horizontalAlignment = Alignment.CenterHorizontally
) {
trigger()
slots()
}
}
@Composable
fun EditorTopBar(
group: StackGroup,
onGroupChange: (StackGroup) -> Unit,
field: @Composable RowScope.() -> Unit,
isEncrypted: Boolean,
hasSelection: Boolean,
onSync: () -> Unit,
onSecurityAction: () -> Unit,
onExport: () -> Unit,
onSettings: () -> Unit,
onShowVersion: () -> Unit,
onQuickSend: () -> Unit,
onRemoveDay: () -> Unit,
onSort: () -> Unit,
onToggleCalc: () -> Unit,
onCalendar: () -> Unit,
modifier: Modifier = Modifier
) {
var touchStamp by remember { mutableIntStateOf(0) }
LaunchedEffect(group, touchStamp) {
if (group != StackGroup.MENU && group != StackGroup.ARTICLE) return@LaunchedEffect
delay(GROUP_IDLE_MS)
onGroupChange(StackGroup.NONE)
}
val toggle: (StackGroup) -> Unit = { g ->
onGroupChange(if (group == g) StackGroup.NONE else g)
}
val onMenu = group == StackGroup.MENU
val onArticle = group == StackGroup.ARTICLE
val bump: () -> Unit = { touchStamp++ }
val fire: (() -> Unit) -> () -> Unit = { f -> { onGroupChange(StackGroup.NONE); f() } }
val menu: @Composable () -> Unit = {
BarStack(
onTouch = bump,
trigger = {
StackButton(Icons.Default.Create, "Menu") { toggle(StackGroup.MENU) }
}
) {
StackSlot(onMenu, Alignment.Top) {
StackButton(Icons.Outlined.Wifi, "Sync", onClick = fire(onSync))
}
StackSlot(onMenu, Alignment.Top) {
StackButton(
if (isEncrypted) Icons.Outlined.LockOpen else Icons.Outlined.Lock,
if (isEncrypted) "Decrypt" else "Encrypt",
onClick = fire(onSecurityAction)
)
}
StackSlot(onMenu, Alignment.Top) {
StackButton(Icons.Outlined.Save, "Export", onClick = fire(onExport))
}
StackSlot(onMenu, Alignment.Top) {
StackButton(Icons.Outlined.Settings, "Settings", onClick = fire(onSettings))
}
StackSlot(onMenu, Alignment.Top) {
StackButton(Icons.Outlined.Park, "Version", onClick = fire(onShowVersion))
}
StackSlot(onMenu, Alignment.Top) {
StackButton(Icons.Outlined.Block, "Exit", enabled = false) {}
}
}
}
val article: @Composable () -> Unit = {
BarStack(
onTouch = bump,
trigger = {
StackButton(
Icons.Outlined.ShapeLine, "Article",
iconSize = STACK_SHAPE_ICON
) { toggle(StackGroup.ARTICLE) }
}
) {
StackSlot(onArticle, Alignment.Top) {
StackButton(Icons.Outlined.AutoDelete, "Prune Day", onClick = fire(onRemoveDay))
}
StackSlot(onArticle, Alignment.Top) {
StackButton(
Icons.AutoMirrored.Outlined.Sort, "Sort",
enabled = hasSelection, onClick = fire(onSort)
)
}
StackSlot(onArticle, Alignment.Top) {
StackButton(
ImageVector.vectorResource(R.drawable.ic_grain_menu), "Calculator",
iconSize = STACK_GRAIN_ICON,
focusable = true, onClick = fire(onToggleCalc)
)
}
StackSlot(onArticle, Alignment.Top) {
StackButton(
Icons.Outlined.CalendarMonth, "Calendar",
focusable = true, onClick = fire(onCalendar)
)
}
StackSlot(onArticle, Alignment.Top) {
StackButton(Icons.Outlined.Loop, "Send", onClick = fire(onQuickSend))
}
}
}
Row(
modifier
.fillMaxWidth()
.statusBarsPadding()
.padding(horizontal = BAR_EDGE, vertical = STACK_GAP),
horizontalArrangement = Arrangement.spacedBy(STACK_GAP),
verticalAlignment = Alignment.Top
) {
field()
article()
menu()
}
}
@Composable
fun SettingsTopBar(
title: String,
onBack: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier
.fillMaxWidth()
.statusBarsPadding()
.padding(horizontal = BAR_EDGE, vertical = STACK_GAP),
horizontalArrangement = Arrangement.spacedBy(STACK_GAP),
verticalAlignment = Alignment.Top
) {
StackButton(Icons.AutoMirrored.Filled.ArrowBack, "Back", onClick = onBack)
BarPlate(title, Modifier.weight(1f), Alignment.CenterStart)
}
}
@Composable
fun EditorButtonStack(
group: StackGroup,
onGroupChange: (StackGroup) -> Unit,
padRight: Int,
padBottom: Int,
editorFocused: Boolean,
hasSelection: Boolean,
syncFlash: Boolean,
isEncrypted: Boolean,
onStrike: () -> Unit,
onDeleteLine: () -> Unit,
onMoveLineUp: () -> Unit,
onMoveLineDown: () -> Unit,
onCopy: () -> Unit,
onPaste: () -> Unit,
onQuickCrypt: () -> Unit,
undoEnabled: Boolean,
onUndo: () -> Unit,
modifier: Modifier = Modifier
) {
var touchStamp by remember { mutableIntStateOf(0) }
LaunchedEffect(group, touchStamp) {
if (group != StackGroup.CREATE) return@LaunchedEffect
delay(GROUP_IDLE_MS)
onGroupChange(StackGroup.NONE)
}
val toggle: (StackGroup) -> Unit = { g ->
onGroupChange(if (group == g) StackGroup.NONE else g)
}
val onCreate = group == StackGroup.CREATE
val fire: (() -> Unit) -> () -> Unit = { f -> { onGroupChange(StackGroup.NONE); f() } }
val imeOpen = WindowInsets.ime.getBottom(LocalDensity.current) > 0
Column(
modifier
.graphicsLayer(alpha = if (imeOpen) STACK_ALPHA_IME else 1f)
.padding(end = padRight.dp, bottom = padBottom.dp)
.pointerInput(Unit) {
awaitEachGesture {
awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
touchStamp++
}
},
horizontalAlignment = Alignment.End
) {
StackSlot(syncFlash) { SyncFlashIcon() }
StackColumn(
trigger = {
StackButton(
if (isEncrypted) Icons.Outlined.LockOpen else Icons.Outlined.AutoAwesomeMosaic,
if (isEncrypted) "Decrypt" else "Create",
iconSize = if (isEncrypted) STACK_ICON else STACK_DRAW_ICON,
onLongClick = if (isEncrypted) null else onQuickCrypt,
longPressMs = CRYPT_HOLD_MS
) { if (isEncrypted) onQuickCrypt() else toggle(StackGroup.CREATE) }
}
) {
StackSlot(onCreate) {
StackButton(
Icons.AutoMirrored.Filled.Undo, "Undo",
enabled = undoEnabled, onClick = fire(onUndo)
)
}
StackSlot(onCreate) {
StackButton(
if (hasSelection) Icons.Outlined.ContentCopy else Icons.Outlined.ContentPaste,
if (hasSelection) "Copy" else "Paste",
enabled = editorFocused,
onClick = if (hasSelection) onCopy else onPaste
)
}
StackSlot(onCreate) {
StackButton(
Icons.Outlined.ArrowUpward, "Move Line Up",
enabled = editorFocused,
onClick = onMoveLineUp
)
}
StackSlot(onCreate) {
StackButton(
Icons.Outlined.ArrowDownward, "Move Line Down",
enabled = editorFocused,
onClick = onMoveLineDown
)
}
StackSlot(onCreate) {
StackButton(
Icons.AutoMirrored.Outlined.Backspace, "Delete Line",
enabled = editorFocused,
onClick = onDeleteLine
)
}
StackSlot(onCreate) {
StackButton(
Icons.Default.StrikethroughS, "Strikethrough",
enabled = editorFocused,
iconSize = STACK_STRIKE_ICON,
iconPad = STACK_STRIKE_PAD,
iconOffsetY = STRIKE_DY,
onClick = onStrike
)
}
}
}
}
@Composable
fun CalculatorOverlay(
calcValue: String,
onCalcValueChange: (String) -> Unit,
onDismiss: () -> Unit,
focusRequester: FocusRequester,
modifier: Modifier = Modifier
) {
OverlayFrame(modifier) {
IconButton(
onClick = { if (calcValue.isEmpty()) onDismiss() else onCalcValueChange("") },
modifier = Modifier.align(Alignment.CenterStart)
) { Icon(Icons.Default.Clear, null, tint = colorResource(id = R.color.calc_color).copy(alpha = 0.4f)) }
BasicTextField(
value = calcValue,
onValueChange = { onCalcValueChange(if (it.endsWith("=")) WedgeCalculator.doCalc(it.dropLast(1)) else it) },
modifier = Modifier
.fillMaxWidth()
.padding(start = 48.dp, end = 22.dp)
.align(Alignment.Center)
.focusRequester(focusRequester),
textStyle = TextStyle(colorResource(id = R.color.calc_color), fontSize = 19.sp, textAlign = TextAlign.End, fontFamily = FontFamily.Monospace),
cursorBrush = SolidColor(colorResource(id = R.color.calc_color)),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { onCalcValueChange(WedgeCalculator.doCalc(calcValue)) }),
singleLine = true,
decorationBox = { inner ->
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) {
if (calcValue.isEmpty()) Text("0", style = TextStyle(colorResource(id = R.color.calc_color).copy(alpha = 0.6f), fontSize = 19.sp, textAlign = TextAlign.End, fontFamily = FontFamily.Monospace))
inner()
}
}
)
}
}
@Composable
fun SearchOverlay(
query: String,
onQueryChange: (String) -> Unit,
onDismiss: () -> Unit,
onNext: () -> Unit,
hasMatch: Boolean,
focusRequester: FocusRequester,
modifier: Modifier = Modifier
) {
OverlayFrame(modifier) {
Icon(
Icons.Outlined.Search,
contentDescription = null,
tint = colorResource(id = R.color.calc_color).copy(alpha = 0.6f),
modifier = Modifier
.align(Alignment.CenterStart)
.padding(start = 14.dp)
.size(26.dp)
)
BasicTextField(
value = query,
onValueChange = onQueryChange,
modifier = Modifier
.fillMaxWidth()
.padding(start = 48.dp, end = 84.dp)
.align(Alignment.Center)
.focusRequester(focusRequester),
textStyle = TextStyle(
colorResource(id = R.color.calc_color),
fontSize = 17.sp,
textAlign = TextAlign.Start,
fontFamily = FontFamily.Monospace
),
cursorBrush = SolidColor(colorResource(id = R.color.calc_color)),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = KeyboardActions(onSearch = { onNext() }),
singleLine = true,
decorationBox = { inner ->
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterStart) {
if (query.isEmpty()) Text(
"Search",
style = TextStyle(
colorResource(id = R.color.calc_color).copy(alpha = 0.6f),
fontSize = 17.sp,
fontFamily = FontFamily.Monospace
)
)
inner()
}
}
)
Row(
modifier = Modifier
.align(Alignment.CenterEnd)
.padding(end = 3.dp),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(
onClick = onNext,
enabled = hasMatch,
modifier = Modifier.focusProperties { canFocus = false }
) {
Icon(
Icons.Outlined.ArrowDownward, null,
tint = colorResource(id = R.color.calc_color)
.copy(alpha = if (hasMatch) 1f else 0.25f)
)
}
IconButton(
onClick = { if (query.isEmpty()) onDismiss() else onQueryChange("") }
) {
Icon(
Icons.Default.Clear, null,
tint = colorResource(id = R.color.calc_color).copy(alpha = 0.4f)
)
}
}
}
}
@Composable
fun EncryptionOverlay(
passPhrase: String,
onPassPhraseChange: (String) -> Unit,
onAction: () -> Unit,
onDismiss: () -> Unit,
focusRequester: FocusRequester,
modifier: Modifier = Modifier,
isDecrypt: Boolean = false
) {
OverlayFrame(modifier) {
IconButton(
onClick = onAction,
enabled = passPhrase.isNotEmpty(),
modifier = Modifier.align(Alignment.CenterStart)
) {
Icon(
if (isDecrypt) Icons.Outlined.LockOpen else Icons.Outlined.Lock,
contentDescription = null,
modifier = Modifier.padding(start = 3.dp),
tint = if (passPhrase.isNotEmpty()) colorResource(id = R.color.calc_color) else colorResource(id = R.color.calc_color).copy(alpha = 0.4f)
)
}
BasicTextField(
value = passPhrase,
onValueChange = onPassPhraseChange,
modifier = Modifier
.fillMaxWidth()
.padding(start = 58.dp, end = 48.dp)
.align(Alignment.Center)
.focusRequester(focusRequester),
textStyle = TextStyle(
colorResource(id = R.color.calc_color),
fontSize = 16.sp,
textAlign = TextAlign.Start,
fontFamily = FontFamily.Monospace
),
cursorBrush = SolidColor(colorResource(id = R.color.calc_color)),
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done,
keyboardType = KeyboardType.Password
),
keyboardActions = KeyboardActions(onDone = { if(passPhrase.isNotEmpty()) onAction() }),
singleLine = true,
decorationBox = { inner ->
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterStart) {
if (passPhrase.isEmpty()) Text(
if (isDecrypt) "Phrase" else "Phrase",
style = TextStyle(
colorResource(id = R.color.calc_color).copy(alpha = 0.6f),
fontSize = 16.sp,
textAlign = TextAlign.Start,
fontFamily = FontFamily.Monospace
)
)
inner()
}
}
)
IconButton(
onClick = onDismiss,
modifier = Modifier
.align(Alignment.CenterEnd)
.padding(end = 3.dp),
) { Icon(Icons.Default.Clear, null, tint = colorResource(id = R.color.calc_color).copy(alpha = 0.4f)) }
}
}
@Composable
fun CalendarOverlay(
timeValue: String,
onTimeValueChange: (String) -> Unit,
textValue: String,
onTextValueChange: (String) -> Unit,
onDismiss: () -> Unit,
onEnterPressed: () -> Unit,
focusRequester: FocusRequester,
modifier: Modifier = Modifier
) {
val focusManager = LocalFocusManager.current
OverlayFrame(modifier) {
Row(
modifier = Modifier
.fillMaxSize()
.padding(start = 12.dp, end = 12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
BasicTextField(
value = timeValue,
onValueChange = { input ->
val digits = input.filter { it.isDigit() }
if (digits.length <= 4) onTimeValueChange(digits)
},
modifier = Modifier
.width(54.dp)
.focusRequester(focusRequester)
.onPreviewKeyEvent { keyEvent ->
if (keyEvent.key == Key.Enter || keyEvent.key == Key.NumPadEnter) {
focusManager.moveFocus(FocusDirection.Next)
true
} else {
false
}
},
textStyle = TextStyle(colorResource(id = R.color.calc_color), fontSize = 16.sp, fontFamily = FontFamily.Monospace, textAlign = TextAlign.Center),
cursorBrush = SolidColor(colorResource(id = R.color.calc_color)),
singleLine = true,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Next
),
keyboardActions = KeyboardActions(
onNext = { focusManager.moveFocus(FocusDirection.Next) }
),
decorationBox = { inner ->
Box(contentAlignment = Alignment.Center) {
if (timeValue.isEmpty()) Text("HHMM", style = TextStyle(colorResource(id = R.color.calc_color).copy(alpha = 0.5f), fontSize = 16.sp, fontFamily = FontFamily.Monospace, textAlign = TextAlign.Center))
inner()
}
}
)
Box(
Modifier
.width(1.dp)
.height(24.dp)
.background(colorResource(id = R.color.calc_color).copy(alpha = 0.1f))
)
BasicTextField(
value = textValue,
onValueChange = onTextValueChange,
modifier = Modifier
.weight(1f),
textStyle = TextStyle(colorResource(id = R.color.calc_color), fontSize = 16.sp, fontFamily = FontFamily.Monospace),
cursorBrush = SolidColor(colorResource(id = R.color.calc_color)),
singleLine = true,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { onEnterPressed() }),
decorationBox = { inner ->
Box(contentAlignment = Alignment.CenterStart) {
if (textValue.isEmpty()) Text("Event", style = TextStyle(colorResource(id = R.color.calc_color).copy(alpha = 0.5f), fontSize = 16.sp, fontFamily = FontFamily.Monospace))
inner()
}
}
)
IconButton(
onClick = onDismiss,
modifier = Modifier.size(30.dp)
) {
Icon(Icons.Default.Clear, null, tint = colorResource(id = R.color.calc_color).copy(alpha = 0.6f))
}
}
}
}