Coverage Summary for Class: NumpadHostKt (com.opty.numpad)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| NumpadHostKt |
100%
(1/1)
|
76.5%
(13/17)
|
81.2%
(26/32)
|
95.7%
(88/92)
|
94.8%
(1112/1173)
|
/*
* Copyright 2024 Opty All rights reserved.
*
* This software and associated documentation files (the "Software") are protected by copyright law and
* international treaties. This software has been licensed to users under the terms of the End User
* License Agreement (EULA) and Terms of Service (ToS) available within the application.
*
* Unauthorized copying, reproduction, modification, distribution, display, performance, sublicense, sale,
* access or use of this Software, or any part thereof, is strictly prohibited and will be prosecuted
* under applicable laws.
*
* The Software contains valuable trade secrets and proprietary information of Opty.
* No part of the Software may be reproduced, distributed, or transmitted in any form or by any means,
* including photocopying, recording, or other electronic or mechanical methods without the prior written
* permission of Opty.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
* LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
* For licensing inquiries, please contact: support@optymacros.com
*/
package com.opty.numpad
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.backhandler.BackHandler
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.opty.common.contract.UiContract
import com.opty.designsystem.Theme
import com.opty.designsystem.components.Button
import com.opty.designsystem.components.ButtonSize
import com.opty.designsystem.components.ButtonVariant
import com.opty.designsystem.components.Surface
import com.opty.designsystem.helper.LocalCustomInputMethodBottomInset
import opty.core.localizations.generated.resources.Res
import opty.core.localizations.generated.resources.app_action_delete
import opty.core.localizations.generated.resources.app_cta_done_concept
import org.jetbrains.compose.resources.stringResource
val LocalNumpadController = staticCompositionLocalOf<NumpadController?> { null }
@Composable
@OptIn(ExperimentalComposeUiApi::class)
fun NumpadHost(content: @Composable () -> Unit) {
val controller = remember { NumpadController() }
var measuredNumpadHeight by remember { mutableIntStateOf(0) }
BackHandler(enabled = controller.isVisible) {
controller.dismiss()
}
CompositionLocalProvider(
LocalNumpadController provides controller,
LocalCustomInputMethodBottomInset provides
if (controller.isVisible) measuredNumpadHeight else 0,
) {
Box(modifier = Modifier.fillMaxSize()) {
content()
AnimatedVisibility(
visible = controller.isVisible,
modifier = Modifier.align(Alignment.BottomCenter),
enter = slideInVertically(initialOffsetY = { it }),
exit = slideOutVertically(targetOffsetY = { it }),
) {
Numpad(
controller = controller,
modifier = Modifier.onSizeChanged { measuredNumpadHeight = it.height },
)
}
}
}
}
@Composable
private fun Numpad(
controller: NumpadController,
modifier: Modifier = Modifier,
) {
val deleteLabel = stringResource(Res.string.app_action_delete)
val doneLabel = stringResource(Res.string.app_cta_done_concept)
val decimalSeparator = controller.session?.spec?.decimalSeparator ?: '.'
val showDecimalSeparator = controller.session?.spec?.allowsDecimal == true
Surface(
color = Theme.colors.baseBackground,
modifier = modifier.fillMaxWidth(),
) {
Column(
modifier =
Modifier
.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom))
.padding(horizontal = 12.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
listOf(
listOf(1, 2, 3),
listOf(4, 5, 6),
listOf(7, 8, 9),
).forEach { row ->
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
row.forEach { digit ->
NumpadButton(
text = digit.toString(),
onClick = { controller.inputDigit(digit) },
modifier = Modifier.weight(1f),
)
}
}
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
NumpadButton(
text = if (showDecimalSeparator) decimalSeparator.toString() else "",
enabled = showDecimalSeparator,
onClick = controller::inputDecimalSeparator,
modifier = Modifier.weight(1f),
)
NumpadButton(
text = "0",
onClick = { controller.inputDigit(0) },
modifier = Modifier.weight(1f),
)
NumpadButton(
text = "⌫",
onClick = controller::backspace,
modifier =
Modifier
.weight(1f)
.semantics { contentDescription = deleteLabel },
)
}
Button(
text = doneLabel,
onClick = controller::done,
variant = ButtonVariant.Primary,
size = ButtonSize.LARGE,
modifier = Modifier.fillMaxWidth(),
)
}
}
}
@Composable
private fun NumpadButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
) {
Button(
text = text,
enabled = enabled,
onClick = onClick,
variant = ButtonVariant.Secondary,
size = ButtonSize.LARGE,
modifier = modifier,
)
}
@Preview
@UiContract("NUM-001")
@Composable
private fun QuantityNumpadPreview() {
Theme {
NumpadHost {
val controller = requireNotNull(LocalNumpadController.current)
LaunchedEffect(controller) {
controller.show(
owner = this,
value = TextFieldValue("100", selection = TextRange(3)),
spec = NumpadInputSpec(maxValue = 999.0, decimalSeparator = '.', maxFractionDigits = 0),
onValueChange = {},
onDone = {},
)
}
}
}
}