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 = {},
                 )
             }
         }
     }
 }