diff --git a/app/src/main/java/com/sadellie/unitto/UnittoApp.kt b/app/src/main/java/com/sadellie/unitto/UnittoApp.kt
index c5a0b30b..7cdf511b 100644
--- a/app/src/main/java/com/sadellie/unitto/UnittoApp.kt
+++ b/app/src/main/java/com/sadellie/unitto/UnittoApp.kt
@@ -136,6 +136,7 @@ internal fun UnittoApp(prefs: AppPreferences?) {
navController = navController,
themmoController = it,
startDestination = prefs.startingScreen,
+ rpnMode = prefs.rpnMode,
openDrawer = { drawerScope.launch { drawerState.open() } }
)
}
diff --git a/app/src/main/java/com/sadellie/unitto/UnittoNavigation.kt b/app/src/main/java/com/sadellie/unitto/UnittoNavigation.kt
index 20161a5f..df1e9dd4 100644
--- a/app/src/main/java/com/sadellie/unitto/UnittoNavigation.kt
+++ b/app/src/main/java/com/sadellie/unitto/UnittoNavigation.kt
@@ -40,7 +40,8 @@ internal fun UnittoNavigation(
navController: NavHostController,
themmoController: ThemmoController,
startDestination: String,
- openDrawer: () -> Unit
+ openDrawer: () -> Unit,
+ rpnMode: Boolean,
) {
NavHost(
navController = navController,
@@ -62,7 +63,8 @@ internal fun UnittoNavigation(
)
calculatorGraph(
- navigateToMenu = openDrawer,
+ openDrawer = openDrawer,
+ rpnMode = rpnMode,
navigateToSettings = navController::navigateToSettings
)
diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/KeyboardButton.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/KeyboardButton.kt
index a77994a7..9af4232b 100644
--- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/KeyboardButton.kt
+++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/KeyboardButton.kt
@@ -22,7 +22,6 @@ import android.view.HapticFeedbackConstants
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
@@ -30,6 +29,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalView
@@ -72,7 +72,7 @@ fun BasicKeyboardButton(
Icon(
imageVector = icon,
contentDescription = null,
- modifier = Modifier.fillMaxHeight(contentHeight),
+ modifier = Modifier.matchParentSize().scale(contentHeight),
tint = iconColor
)
}
diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Down.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Down.kt
new file mode 100644
index 00000000..8a72b795
--- /dev/null
+++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Down.kt
@@ -0,0 +1,60 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.core.ui.common.key.unittoicons
+
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.PathFillType.Companion.NonZero
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.StrokeCap.Companion.Butt
+import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.ImageVector.Builder
+import androidx.compose.ui.graphics.vector.path
+import androidx.compose.ui.unit.dp
+import com.sadellie.unitto.core.ui.common.key.UnittoIcons
+
+@Suppress("UnusedReceiverParameter")
+val UnittoIcons.Down: ImageVector
+ get() {
+ if (_down != null) {
+ return _down!!
+ }
+ _down = Builder(name = "Down", defaultWidth = 170.0.dp, defaultHeight = 170.0.dp,
+ viewportWidth = 170.0f, viewportHeight = 170.0f).apply {
+ path(fill = SolidColor(Color(0xFF1C1B1F)), stroke = null, strokeLineWidth = 0.0f,
+ strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f,
+ pathFillType = NonZero) {
+ moveTo(79.0f, 37.0f)
+ verticalLineTo(110.05f)
+ lineTo(45.4f, 76.45f)
+ lineTo(37.0f, 85.0f)
+ lineTo(85.0f, 133.0f)
+ lineTo(133.0f, 85.0f)
+ lineTo(124.6f, 76.45f)
+ lineTo(91.0f, 110.05f)
+ verticalLineTo(37.0f)
+ horizontalLineTo(79.0f)
+ close()
+ }
+ }
+ .build()
+ return _down!!
+ }
+
+private var _down: ImageVector? = null
diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Enter.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Enter.kt
new file mode 100644
index 00000000..71706837
--- /dev/null
+++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Enter.kt
@@ -0,0 +1,62 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.core.ui.common.key.unittoicons
+
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.PathFillType.Companion.NonZero
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.StrokeCap.Companion.Butt
+import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.ImageVector.Builder
+import androidx.compose.ui.graphics.vector.path
+import androidx.compose.ui.unit.dp
+import com.sadellie.unitto.core.ui.common.key.UnittoIcons
+
+@Suppress("UnusedReceiverParameter")
+val UnittoIcons.Enter: ImageVector
+ get() {
+ if (_enter != null) {
+ return _enter!!
+ }
+ _enter = Builder(name = "Enter", defaultWidth = 170.0.dp, defaultHeight = 170.0.dp,
+ viewportWidth = 170.0f, viewportHeight = 170.0f).apply {
+ path(fill = SolidColor(Color(0xFF1C1B1F)), stroke = null, strokeLineWidth = 0.0f,
+ strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f,
+ pathFillType = NonZero) {
+ moveTo(128.0f, 37.0f)
+ lineTo(128.0f, 104.765f)
+ lineTo(64.894f, 104.765f)
+ lineTo(85.224f, 124.953f)
+ lineTo(77.176f, 133.0f)
+ lineTo(43.294f, 99.118f)
+ lineTo(77.318f, 65.094f)
+ lineTo(85.224f, 73.141f)
+ lineTo(64.894f, 93.471f)
+ lineTo(116.706f, 93.471f)
+ lineTo(116.706f, 37.0f)
+ lineTo(128.0f, 37.0f)
+ close()
+ }
+ }
+ .build()
+ return _enter!!
+ }
+
+private var _enter: ImageVector? = null
diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Pop.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Pop.kt
new file mode 100644
index 00000000..ac4da0c5
--- /dev/null
+++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Pop.kt
@@ -0,0 +1,137 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.core.ui.common.key.unittoicons
+
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.PathFillType.Companion.NonZero
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.StrokeCap.Companion.Butt
+import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.ImageVector.Builder
+import androidx.compose.ui.graphics.vector.path
+import androidx.compose.ui.unit.dp
+import com.sadellie.unitto.core.ui.common.key.UnittoIcons
+
+@Suppress("UnusedReceiverParameter")
+val UnittoIcons.Pop: ImageVector
+ get() {
+ if (_pop != null) {
+ return _pop!!
+ }
+ _pop = Builder(name = "Pop", defaultWidth = 274.0.dp, defaultHeight = 170.0.dp,
+ viewportWidth = 274.0f, viewportHeight = 170.0f).apply {
+ path(fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f,
+ strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f,
+ pathFillType = NonZero) {
+ moveTo(47.507f, 87.46f)
+ curveTo(51.159f, 87.46f, 54.371f, 86.976f, 57.143f, 86.008f)
+ curveTo(59.959f, 85.04f, 62.313f, 83.698f, 64.205f, 81.982f)
+ curveTo(66.141f, 80.222f, 67.593f, 78.132f, 68.561f, 75.712f)
+ curveTo(69.529f, 73.292f, 70.013f, 70.63f, 70.013f, 67.726f)
+ curveTo(70.013f, 61.698f, 68.143f, 56.99f, 64.403f, 53.602f)
+ curveTo(60.707f, 50.214f, 55.075f, 48.52f, 47.507f, 48.52f)
+ horizontalLineTo(32.327f)
+ verticalLineTo(87.46f)
+ horizontalLineTo(47.507f)
+ close()
+ moveTo(47.507f, 38.422f)
+ curveTo(53.491f, 38.422f, 58.683f, 39.126f, 63.083f, 40.534f)
+ curveTo(67.527f, 41.898f, 71.201f, 43.856f, 74.105f, 46.408f)
+ curveTo(77.009f, 48.96f, 79.165f, 52.04f, 80.573f, 55.648f)
+ curveTo(82.025f, 59.256f, 82.751f, 63.282f, 82.751f, 67.726f)
+ curveTo(82.751f, 72.126f, 81.981f, 76.152f, 80.441f, 79.804f)
+ curveTo(78.901f, 83.456f, 76.635f, 86.602f, 73.643f, 89.242f)
+ curveTo(70.695f, 91.882f, 67.021f, 93.95f, 62.621f, 95.446f)
+ curveTo(58.265f, 96.898f, 53.227f, 97.624f, 47.507f, 97.624f)
+ horizontalLineTo(32.327f)
+ verticalLineTo(133.0f)
+ horizontalLineTo(19.589f)
+ verticalLineTo(38.422f)
+ horizontalLineTo(47.507f)
+ close()
+ moveTo(183.61f, 85.744f)
+ curveTo(183.61f, 92.828f, 182.488f, 99.34f, 180.244f, 105.28f)
+ curveTo(178.0f, 111.176f, 174.832f, 116.258f, 170.74f, 120.526f)
+ curveTo(166.648f, 124.794f, 161.72f, 128.116f, 155.956f, 130.492f)
+ curveTo(150.236f, 132.824f, 143.9f, 133.99f, 136.948f, 133.99f)
+ curveTo(129.996f, 133.99f, 123.66f, 132.824f, 117.94f, 130.492f)
+ curveTo(112.22f, 128.116f, 107.314f, 124.794f, 103.222f, 120.526f)
+ curveTo(99.13f, 116.258f, 95.962f, 111.176f, 93.718f, 105.28f)
+ curveTo(91.474f, 99.34f, 90.352f, 92.828f, 90.352f, 85.744f)
+ curveTo(90.352f, 78.66f, 91.474f, 72.17f, 93.718f, 66.274f)
+ curveTo(95.962f, 60.334f, 99.13f, 55.23f, 103.222f, 50.962f)
+ curveTo(107.314f, 46.65f, 112.22f, 43.306f, 117.94f, 40.93f)
+ curveTo(123.66f, 38.554f, 129.996f, 37.366f, 136.948f, 37.366f)
+ curveTo(143.9f, 37.366f, 150.236f, 38.554f, 155.956f, 40.93f)
+ curveTo(161.72f, 43.306f, 166.648f, 46.65f, 170.74f, 50.962f)
+ curveTo(174.832f, 55.23f, 178.0f, 60.334f, 180.244f, 66.274f)
+ curveTo(182.488f, 72.17f, 183.61f, 78.66f, 183.61f, 85.744f)
+ close()
+ moveTo(170.476f, 85.744f)
+ curveTo(170.476f, 79.936f, 169.684f, 74.722f, 168.1f, 70.102f)
+ curveTo(166.516f, 65.482f, 164.272f, 61.588f, 161.368f, 58.42f)
+ curveTo(158.464f, 55.208f, 154.944f, 52.744f, 150.808f, 51.028f)
+ curveTo(146.672f, 49.312f, 142.052f, 48.454f, 136.948f, 48.454f)
+ curveTo(131.888f, 48.454f, 127.29f, 49.312f, 123.154f, 51.028f)
+ curveTo(119.018f, 52.744f, 115.476f, 55.208f, 112.528f, 58.42f)
+ curveTo(109.624f, 61.588f, 107.38f, 65.482f, 105.796f, 70.102f)
+ curveTo(104.212f, 74.722f, 103.42f, 79.936f, 103.42f, 85.744f)
+ curveTo(103.42f, 91.552f, 104.212f, 96.766f, 105.796f, 101.386f)
+ curveTo(107.38f, 105.962f, 109.624f, 109.856f, 112.528f, 113.068f)
+ curveTo(115.476f, 116.236f, 119.018f, 118.678f, 123.154f, 120.394f)
+ curveTo(127.29f, 122.066f, 131.888f, 122.902f, 136.948f, 122.902f)
+ curveTo(142.052f, 122.902f, 146.672f, 122.066f, 150.808f, 120.394f)
+ curveTo(154.944f, 118.678f, 158.464f, 116.236f, 161.368f, 113.068f)
+ curveTo(164.272f, 109.856f, 166.516f, 105.962f, 168.1f, 101.386f)
+ curveTo(169.684f, 96.766f, 170.476f, 91.552f, 170.476f, 85.744f)
+ close()
+ moveTo(227.208f, 87.46f)
+ curveTo(230.86f, 87.46f, 234.072f, 86.976f, 236.844f, 86.008f)
+ curveTo(239.66f, 85.04f, 242.014f, 83.698f, 243.906f, 81.982f)
+ curveTo(245.842f, 80.222f, 247.294f, 78.132f, 248.262f, 75.712f)
+ curveTo(249.23f, 73.292f, 249.714f, 70.63f, 249.714f, 67.726f)
+ curveTo(249.714f, 61.698f, 247.844f, 56.99f, 244.104f, 53.602f)
+ curveTo(240.408f, 50.214f, 234.776f, 48.52f, 227.208f, 48.52f)
+ horizontalLineTo(212.028f)
+ verticalLineTo(87.46f)
+ horizontalLineTo(227.208f)
+ close()
+ moveTo(227.208f, 38.422f)
+ curveTo(233.192f, 38.422f, 238.384f, 39.126f, 242.784f, 40.534f)
+ curveTo(247.228f, 41.898f, 250.902f, 43.856f, 253.806f, 46.408f)
+ curveTo(256.71f, 48.96f, 258.866f, 52.04f, 260.274f, 55.648f)
+ curveTo(261.726f, 59.256f, 262.452f, 63.282f, 262.452f, 67.726f)
+ curveTo(262.452f, 72.126f, 261.682f, 76.152f, 260.142f, 79.804f)
+ curveTo(258.602f, 83.456f, 256.336f, 86.602f, 253.344f, 89.242f)
+ curveTo(250.396f, 91.882f, 246.722f, 93.95f, 242.322f, 95.446f)
+ curveTo(237.966f, 96.898f, 232.928f, 97.624f, 227.208f, 97.624f)
+ horizontalLineTo(212.028f)
+ verticalLineTo(133.0f)
+ horizontalLineTo(199.29f)
+ verticalLineTo(38.422f)
+ horizontalLineTo(227.208f)
+ close()
+ }
+ }
+ .build()
+ return _pop!!
+ }
+
+private var _pop: ImageVector? = null
diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Swap.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Swap.kt
new file mode 100644
index 00000000..0e27b343
--- /dev/null
+++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Swap.kt
@@ -0,0 +1,71 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.core.ui.common.key.unittoicons
+
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.PathFillType.Companion.NonZero
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.StrokeCap.Companion.Butt
+import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.ImageVector.Builder
+import androidx.compose.ui.graphics.vector.path
+import androidx.compose.ui.unit.dp
+import com.sadellie.unitto.core.ui.common.key.UnittoIcons
+
+@Suppress("UnusedReceiverParameter")
+val UnittoIcons.Swap: ImageVector
+ get() {
+ if (_swap != null) {
+ return _swap!!
+ }
+ _swap = Builder(name = "Swap", defaultWidth = 170.0.dp, defaultHeight = 170.0.dp,
+ viewportWidth = 170.0f, viewportHeight = 170.0f).apply {
+ path(fill = SolidColor(Color(0xFF1C1B1F)), stroke = null, strokeLineWidth = 0.0f,
+ strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f,
+ pathFillType = NonZero) {
+ moveTo(66.2f, 89.8f)
+ verticalLineTo(55.36f)
+ lineTo(53.84f, 67.72f)
+ lineTo(47.0f, 61.0f)
+ lineTo(71.0f, 37.0f)
+ lineTo(95.0f, 61.0f)
+ lineTo(88.16f, 67.72f)
+ lineTo(75.8f, 55.36f)
+ verticalLineTo(89.8f)
+ horizontalLineTo(66.2f)
+ close()
+ moveTo(99.8f, 133.0f)
+ lineTo(75.8f, 109.0f)
+ lineTo(82.64f, 102.28f)
+ lineTo(95.0f, 114.64f)
+ verticalLineTo(80.2f)
+ horizontalLineTo(104.6f)
+ verticalLineTo(114.64f)
+ lineTo(116.96f, 102.28f)
+ lineTo(123.8f, 109.0f)
+ lineTo(99.8f, 133.0f)
+ close()
+ }
+ }
+ .build()
+ return _swap!!
+ }
+
+private var _swap: ImageVector? = null
diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Unary.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Unary.kt
new file mode 100644
index 00000000..c348a11f
--- /dev/null
+++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Unary.kt
@@ -0,0 +1,75 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.core.ui.common.key.unittoicons
+
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.PathFillType.Companion.NonZero
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.StrokeCap.Companion.Butt
+import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.ImageVector.Builder
+import androidx.compose.ui.graphics.vector.path
+import androidx.compose.ui.unit.dp
+import com.sadellie.unitto.core.ui.common.key.UnittoIcons
+
+@Suppress("UnusedReceiverParameter")
+val UnittoIcons.Unary: ImageVector
+ get() {
+ if (_unary != null) {
+ return _unary!!
+ }
+ _unary = Builder(name = "Unary", defaultWidth = 170.0.dp, defaultHeight = 170.0.dp,
+ viewportWidth = 170.0f, viewportHeight = 170.0f).apply {
+ path(fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f,
+ strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f,
+ pathFillType = NonZero) {
+ moveTo(46.0f, 71.125f)
+ horizontalLineTo(124.0f)
+ verticalLineTo(81.363f)
+ horizontalLineTo(46.0f)
+ verticalLineTo(71.125f)
+ close()
+ }
+ path(fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f,
+ strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f,
+ pathFillType = NonZero) {
+ moveTo(90.85f, 37.0f)
+ lineTo(90.85f, 115.0f)
+ horizontalLineTo(80.613f)
+ lineTo(80.613f, 37.0f)
+ lineTo(90.85f, 37.0f)
+ close()
+ }
+ path(fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f,
+ strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f,
+ pathFillType = NonZero) {
+ moveTo(46.0f, 124.0f)
+ horizontalLineTo(124.0f)
+ verticalLineTo(133.0f)
+ horizontalLineTo(46.0f)
+ verticalLineTo(124.0f)
+ close()
+ }
+ }
+ .build()
+ return _unary!!
+ }
+
+private var _unary: ImageVector? = null
diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Up.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Up.kt
new file mode 100644
index 00000000..fc0dad9e
--- /dev/null
+++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Up.kt
@@ -0,0 +1,60 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.core.ui.common.key.unittoicons
+
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.PathFillType.Companion.NonZero
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.StrokeCap.Companion.Butt
+import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.ImageVector.Builder
+import androidx.compose.ui.graphics.vector.path
+import androidx.compose.ui.unit.dp
+import com.sadellie.unitto.core.ui.common.key.UnittoIcons
+
+@Suppress("UnusedReceiverParameter")
+val UnittoIcons.Up: ImageVector
+ get() {
+ if (_up != null) {
+ return _up!!
+ }
+ _up = Builder(name = "Up", defaultWidth = 170.0.dp, defaultHeight = 170.0.dp, viewportWidth
+ = 170.0f, viewportHeight = 170.0f).apply {
+ path(fill = SolidColor(Color(0xFF1C1B1F)), stroke = null, strokeLineWidth = 0.0f,
+ strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f,
+ pathFillType = NonZero) {
+ moveTo(79.0f, 133.0f)
+ verticalLineTo(59.95f)
+ lineTo(45.4f, 93.55f)
+ lineTo(37.0f, 85.0f)
+ lineTo(85.0f, 37.0f)
+ lineTo(133.0f, 85.0f)
+ lineTo(124.6f, 93.55f)
+ lineTo(91.0f, 59.95f)
+ verticalLineTo(133.0f)
+ horizontalLineTo(79.0f)
+ close()
+ }
+ }
+ .build()
+ return _up!!
+ }
+
+private var _up: ImageVector? = null
diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/FixedInputTextFIeld.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/FixedInputTextFIeld.kt
new file mode 100644
index 00000000..1629a32e
--- /dev/null
+++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/FixedInputTextFIeld.kt
@@ -0,0 +1,92 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.core.ui.common.textfield
+
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.PressInteraction
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalClipboardManager
+import androidx.compose.ui.platform.LocalTextInputService
+import androidx.compose.ui.platform.LocalTextToolbar
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import com.sadellie.unitto.core.ui.theme.LocalNumberTypography
+
+@Composable
+fun FixedInputTextField(
+ modifier: Modifier = Modifier,
+ value: String,
+ formatterSymbols: FormatterSymbols,
+ textColor: Color,
+ onClick: (cleanValue: String) -> Unit
+) {
+ val clipboardManager = LocalClipboardManager.current
+ val expression = value.take(1000)
+ var expressionValue by remember(expression) {
+ mutableStateOf(TextFieldValue(expression, TextRange(expression.length)))
+ }
+
+ val expressionInteractionSource = remember(expression) { MutableInteractionSource() }
+ LaunchedEffect(expressionInteractionSource) {
+ expressionInteractionSource.interactions.collect {
+ if (it is PressInteraction.Release) onClick(expression.clearAndFilterExpression(formatterSymbols))
+ }
+ }
+
+ CompositionLocalProvider(
+ LocalTextInputService provides null,
+ LocalTextToolbar provides UnittoTextToolbar(
+ view = LocalView.current,
+ copyCallback = {
+ clipboardManager.copyWithoutGrouping(expressionValue, formatterSymbols)
+ expressionValue = expressionValue.copy(selection = TextRange(expressionValue.selection.end))
+ }
+ )
+ ) {
+ BasicTextField(
+ value = expressionValue,
+ onValueChange = { expressionValue = it },
+ maxLines = 1,
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = 8.dp)
+ .horizontalScroll(rememberScrollState(), reverseScrolling = true),
+ textStyle = LocalNumberTypography.current.displaySmall.copy(color = textColor, textAlign = TextAlign.End),
+ readOnly = true,
+ visualTransformation = ExpressionTransformer(formatterSymbols),
+ interactionSource = expressionInteractionSource
+ )
+ }
+}
diff --git a/data/common/src/main/java/com/sadellie/unitto/data/common/BigDecimalUtils.kt b/data/common/src/main/java/com/sadellie/unitto/data/common/BigDecimalUtils.kt
index 4354814c..94f12cfb 100644
--- a/data/common/src/main/java/com/sadellie/unitto/data/common/BigDecimalUtils.kt
+++ b/data/common/src/main/java/com/sadellie/unitto/data/common/BigDecimalUtils.kt
@@ -24,6 +24,7 @@ import java.math.RoundingMode
import kotlin.math.floor
import kotlin.math.log10
+// TODO Use everywhere
fun BigDecimal.format(
scale: Int,
outputFormat: Int
@@ -34,6 +35,7 @@ fun BigDecimal.format(
.toStringWith(outputFormat)
}
+// TODO Move tests and mark as internal
/**
* Shorthand function to use correct `toString` method according to [outputFormat].
*/
@@ -76,5 +78,7 @@ fun BigDecimal.setMinimumRequiredScale(prefScale: Int): BigDecimal {
* Removes all trailing zeroes.
*/
fun BigDecimal.trimZeros(): BigDecimal {
- return if (this.compareTo(BigDecimal.ZERO) == 0) BigDecimal.ZERO else this.stripTrailingZeros()
+ return if (this.isEqualTo(BigDecimal.ZERO)) BigDecimal.ZERO else this.stripTrailingZeros()
}
+
+fun BigDecimal.isEqualTo(bd: BigDecimal): Boolean = compareTo(bd) == 0
diff --git a/data/evaluatto/src/main/java/io/github/sadellie/evaluatto/BigDecimalMath.kt b/data/evaluatto/src/main/java/io/github/sadellie/evaluatto/BigDecimalMath.kt
new file mode 100644
index 00000000..fe5031c6
--- /dev/null
+++ b/data/evaluatto/src/main/java/io/github/sadellie/evaluatto/BigDecimalMath.kt
@@ -0,0 +1,103 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package io.github.sadellie.evaluatto
+
+import java.math.BigDecimal
+import java.math.MathContext
+import java.math.RoundingMode
+import kotlin.math.acos
+import kotlin.math.asin
+import kotlin.math.atan
+import kotlin.math.pow
+
+internal fun BigDecimal.sin(radianMode: Boolean): BigDecimal {
+ val angle: Double = if (radianMode) this.toDouble() else Math.toRadians(this.toDouble())
+ return kotlin.math.sin(angle).toBigDecimal()
+}
+
+internal fun BigDecimal.arsin(radianMode: Boolean): BigDecimal {
+ val angle: Double = asin(this.toDouble())
+ return (if (radianMode) angle else Math.toDegrees(angle)).toBigDecimal()
+}
+
+internal fun BigDecimal.cos(radianMode: Boolean): BigDecimal {
+ val angle: Double = if (radianMode) this.toDouble() else Math.toRadians(this.toDouble())
+ return kotlin.math.cos(angle).toBigDecimal()
+}
+
+internal fun BigDecimal.arcos(radianMode: Boolean): BigDecimal {
+ val angle: Double = acos(this.toDouble())
+ return (if (radianMode) angle else Math.toDegrees(angle)).toBigDecimal()
+}
+
+internal fun BigDecimal.tan(radianMode: Boolean): BigDecimal {
+ val angle: Double = if (radianMode) this.toDouble() else Math.toRadians(this.toDouble())
+ return kotlin.math.tan(angle).toBigDecimal()
+}
+
+internal fun BigDecimal.artan(radianMode: Boolean): BigDecimal {
+ val angle: Double = atan(this.toDouble())
+ return (if (radianMode) angle else Math.toDegrees(angle)).toBigDecimal()
+}
+
+internal fun BigDecimal.ln(): BigDecimal {
+ return kotlin.math.ln(this.toDouble()).toBigDecimal()
+}
+
+internal fun BigDecimal.log(): BigDecimal {
+ return kotlin.math.log(this.toDouble(), 10.0).toBigDecimal()
+}
+
+internal fun BigDecimal.exp(): BigDecimal {
+ return kotlin.math.exp(this.toDouble()).toBigDecimal()
+}
+
+internal fun BigDecimal.pow(n: BigDecimal): BigDecimal {
+ val mathContext: MathContext = MathContext.DECIMAL64
+
+ var right = n
+ val signOfRight = right.signum()
+ right = right.multiply(signOfRight.toBigDecimal())
+ val remainderOfRight = right.remainder(BigDecimal.ONE)
+ val n2IntPart = right.subtract(remainderOfRight)
+ val intPow = pow(n2IntPart.intValueExact(), mathContext)
+ val doublePow = BigDecimal(
+ toDouble().pow(remainderOfRight.toDouble())
+ )
+
+ var result = intPow.multiply(doublePow, mathContext)
+ if (signOfRight == -1) result =
+ BigDecimal.ONE.divide(result, mathContext.precision, RoundingMode.HALF_UP)
+
+ return result
+}
+
+internal fun BigDecimal.factorial(): BigDecimal {
+ if (this.remainder(BigDecimal.ONE).compareTo(BigDecimal.ZERO) != 0) throw ExpressionException.FactorialCalculation()
+ if (this < BigDecimal.ZERO) throw ExpressionException.FactorialCalculation()
+ if (this > maxFactorial) throw ExpressionException.TooBig()
+
+ var expr = this
+ for (i in 1 until this.toInt()) {
+ expr *= BigDecimal(i)
+ }
+ return expr
+}
+
+private val maxFactorial by lazy { BigDecimal("9999") }
diff --git a/data/evaluatto/src/main/java/io/github/sadellie/evaluatto/Expression.kt b/data/evaluatto/src/main/java/io/github/sadellie/evaluatto/Expression.kt
index ef30d655..93164c57 100644
--- a/data/evaluatto/src/main/java/io/github/sadellie/evaluatto/Expression.kt
+++ b/data/evaluatto/src/main/java/io/github/sadellie/evaluatto/Expression.kt
@@ -21,20 +21,7 @@ package io.github.sadellie.evaluatto
import com.sadellie.unitto.core.base.MAX_PRECISION
import com.sadellie.unitto.core.base.Token
import java.math.BigDecimal
-import java.math.MathContext
import java.math.RoundingMode
-import kotlin.math.acos
-import kotlin.math.asin
-import kotlin.math.atan
-import kotlin.math.cos
-import kotlin.math.exp
-import kotlin.math.ln
-import kotlin.math.log
-import kotlin.math.pow
-import kotlin.math.sin
-import kotlin.math.tan
-
-private val maxFactorial by lazy { BigDecimal("9999") }
sealed class ExpressionException(override val message: String): Exception(message) {
class DivideByZero : ExpressionException("Can't divide by zero")
@@ -240,77 +227,3 @@ class Expression(
return expr
}
}
-
-private fun BigDecimal.sin(radianMode: Boolean): BigDecimal {
- val angle: Double = if (radianMode) this.toDouble() else Math.toRadians(this.toDouble())
- return sin(angle).toBigDecimal()
-}
-
-private fun BigDecimal.arsin(radianMode: Boolean): BigDecimal {
- val angle: Double = asin(this.toDouble())
- return (if (radianMode) angle else Math.toDegrees(angle)).toBigDecimal()
-}
-
-private fun BigDecimal.cos(radianMode: Boolean): BigDecimal {
- val angle: Double = if (radianMode) this.toDouble() else Math.toRadians(this.toDouble())
- return cos(angle).toBigDecimal()
-}
-
-private fun BigDecimal.arcos(radianMode: Boolean): BigDecimal {
- val angle: Double = acos(this.toDouble())
- return (if (radianMode) angle else Math.toDegrees(angle)).toBigDecimal()
-}
-
-private fun BigDecimal.tan(radianMode: Boolean): BigDecimal {
- val angle: Double = if (radianMode) this.toDouble() else Math.toRadians(this.toDouble())
- return tan(angle).toBigDecimal()
-}
-
-private fun BigDecimal.artan(radianMode: Boolean): BigDecimal {
- val angle: Double = atan(this.toDouble())
- return (if (radianMode) angle else Math.toDegrees(angle)).toBigDecimal()
-}
-
-private fun BigDecimal.ln(): BigDecimal {
- return ln(this.toDouble()).toBigDecimal()
-}
-
-private fun BigDecimal.log(): BigDecimal {
- return log(this.toDouble(), 10.0).toBigDecimal()
-}
-
-private fun BigDecimal.exp(): BigDecimal {
- return exp(this.toDouble()).toBigDecimal()
-}
-
-private fun BigDecimal.pow(n: BigDecimal): BigDecimal {
- val mathContext: MathContext = MathContext.DECIMAL64
-
- var right = n
- val signOfRight = right.signum()
- right = right.multiply(signOfRight.toBigDecimal())
- val remainderOfRight = right.remainder(BigDecimal.ONE)
- val n2IntPart = right.subtract(remainderOfRight)
- val intPow = pow(n2IntPart.intValueExact(), mathContext)
- val doublePow = BigDecimal(
- toDouble().pow(remainderOfRight.toDouble())
- )
-
- var result = intPow.multiply(doublePow, mathContext)
- if (signOfRight == -1) result =
- BigDecimal.ONE.divide(result, mathContext.precision, RoundingMode.HALF_UP)
-
- return result
-}
-
-private fun BigDecimal.factorial(): BigDecimal {
- if (this.remainder(BigDecimal.ONE).compareTo(BigDecimal.ZERO) != 0) throw ExpressionException.FactorialCalculation()
- if (this < BigDecimal.ZERO) throw ExpressionException.FactorialCalculation()
- if (this > maxFactorial) throw ExpressionException.TooBig()
-
- var expr = this
- for (i in 1 until this.toInt()) {
- expr *= BigDecimal(i)
- }
- return expr
-}
diff --git a/data/evaluatto/src/main/java/io/github/sadellie/evaluatto/RPNCalculation.kt b/data/evaluatto/src/main/java/io/github/sadellie/evaluatto/RPNCalculation.kt
new file mode 100644
index 00000000..0b108442
--- /dev/null
+++ b/data/evaluatto/src/main/java/io/github/sadellie/evaluatto/RPNCalculation.kt
@@ -0,0 +1,37 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package io.github.sadellie.evaluatto
+
+sealed class RPNCalculation {
+ data object Negate : RPNCalculation()
+
+ data object Clear : RPNCalculation()
+ data object Enter : RPNCalculation()
+ data object RotateUp : RPNCalculation()
+ data object RotateDown : RPNCalculation()
+ data object Swap : RPNCalculation()
+ data object Pop : RPNCalculation()
+
+ data object Plus : RPNCalculation()
+ data object Minus : RPNCalculation()
+ data object Multiply : RPNCalculation()
+ data object Divide : RPNCalculation()
+ data object Percent : RPNCalculation()
+ data object Power : RPNCalculation() // unused
+}
diff --git a/data/evaluatto/src/main/java/io/github/sadellie/evaluatto/RPNEngine.kt b/data/evaluatto/src/main/java/io/github/sadellie/evaluatto/RPNEngine.kt
new file mode 100644
index 00000000..dcf2af00
--- /dev/null
+++ b/data/evaluatto/src/main/java/io/github/sadellie/evaluatto/RPNEngine.kt
@@ -0,0 +1,191 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package io.github.sadellie.evaluatto
+
+import com.sadellie.unitto.core.base.MAX_PRECISION
+import java.math.BigDecimal
+import java.math.RoundingMode
+
+sealed class RPNResult {
+
+ /**
+ * Both input and stack were changed.
+ *
+ * @property input New input. Empty when `null`.
+ * @property stack New stack.
+ */
+ data class Result(
+ val input: BigDecimal?,
+ val stack: List,
+ ) : RPNResult()
+
+ /**
+ * Only input has been changed.
+ *
+ * @property input New input.
+ */
+ data class NewInput(
+ val input: BigDecimal,
+ ) : RPNResult()
+
+ /**
+ * Only stack has been changed.
+ *
+ * @property stack New stack.
+ */
+ data class NewStack(
+ val stack: List,
+ ) : RPNResult()
+
+ /**
+ * Something is wrong. Input/stack is empty or there ane not enough stack objects.
+ */
+ data object BadInput : RPNResult()
+
+ /**
+ * Dividing by zero, duh
+ */
+ data object DivideByZero : RPNResult()
+}
+
+// vroom vroom mfs
+// overdose on early returns
+fun RPNCalculation.perform(
+ input: String,
+ stack: List,
+): RPNResult {
+ when (this) {
+ RPNCalculation.Clear -> {
+ return RPNResult.Result(null, emptyList())
+ }
+
+ RPNCalculation.Enter -> {
+ val inputBD = input.toBigDecimalOrNull() ?: return RPNResult.BadInput
+ return RPNResult.Result(null, stack + inputBD)
+ }
+
+ RPNCalculation.Negate -> {
+ val inputBD = input.toBigDecimalOrNull() ?: return RPNResult.BadInput
+ val result = inputBD.negate()
+ return RPNResult.NewInput(result)
+ }
+
+ RPNCalculation.RotateUp -> {
+ if (stack.size < 2) return RPNResult.BadInput
+ return RPNResult.NewStack(stack.rotateUp())
+ }
+
+ RPNCalculation.RotateDown -> {
+ if (stack.size < 2) return RPNResult.BadInput
+ return RPNResult.NewStack(stack.rotateDown())
+ }
+
+ RPNCalculation.Swap -> {
+ if (stack.isEmpty()) return RPNResult.BadInput
+ if (input.isEmpty()) {
+ // Swap last 2 in stack
+ if (stack.size < 2) return RPNResult.BadInput
+ return RPNResult.NewStack(stack.swapLastTwo())
+ }
+
+ // Swap last and input
+ val (lastFromStack, inputBD) = operands(stack, input) ?: return RPNResult.BadInput
+ return RPNResult.Result(lastFromStack, stack.dropLast(1) + inputBD)
+ }
+
+ RPNCalculation.Pop -> {
+ val lastStacked = stack.lastOrNull() ?: return RPNResult.BadInput
+ return RPNResult.Result(lastStacked, stack.dropLast(1))
+ }
+
+ RPNCalculation.Plus -> {
+ val (lastFromStack, inputBD) = operands(stack, input) ?: return RPNResult.BadInput
+ val result = lastFromStack.plus(inputBD)
+ return RPNResult.Result(result, stack.dropLast(1))
+ }
+
+ RPNCalculation.Minus -> {
+ val (lastFromStack, inputBD) = operands(stack, input) ?: return RPNResult.BadInput
+ val result = lastFromStack.minus(inputBD)
+ return RPNResult.Result(result, stack.dropLast(1))
+ }
+
+ RPNCalculation.Multiply -> {
+ val (lastFromStack, inputBD) = operands(stack, input) ?: return RPNResult.BadInput
+ val result = lastFromStack.multiply(inputBD)
+ return RPNResult.Result(result, stack.dropLast(1))
+ }
+
+ RPNCalculation.Divide -> {
+ val (lastFromStack, inputBD) = operands(stack, input) ?: return RPNResult.BadInput
+ if (inputBD.compareTo(BigDecimal.ZERO) == 0) return RPNResult.DivideByZero
+
+ val result = lastFromStack.divide(inputBD, MAX_PRECISION, RoundingMode.HALF_EVEN)
+ return RPNResult.Result(result, stack.dropLast(1))
+ }
+
+ RPNCalculation.Percent -> {
+ val (lastFromStack, inputBD) = operands(stack, input) ?: return RPNResult.BadInput
+ // 100 * 24 / 100 =
+ val result = lastFromStack
+ .multiply(inputBD)
+ .divide(bigDecimalHundred, MAX_PRECISION, RoundingMode.HALF_EVEN)
+ return RPNResult.Result(result, stack.dropLast(1))
+ }
+
+ RPNCalculation.Power -> {
+ val (lastFromStack, inputBD) = operands(stack, input) ?: return RPNResult.BadInput
+ val result = lastFromStack.pow(inputBD)
+ return RPNResult.Result(result, stack.dropLast(1))
+ }
+ }
+}
+
+private val bigDecimalHundred by lazy { BigDecimal("100") }
+
+private fun operands(
+ stack: List,
+ input: String,
+): Pair? {
+ val first = stack.lastOrNull() ?: return null
+ val second = input.toBigDecimalOrNull() ?: return null
+
+ return first to second
+}
+
+private fun List.swapLastTwo(): List {
+ if (size < 2) return this
+ return this
+ .dropLast(2)
+ .plus(get(lastIndex))
+ .plus(get(lastIndex - 1))
+}
+
+private fun List.rotateUp(): List {
+ if (size < 2) return this
+ return this
+ .drop(1)
+ .plus(first())
+}
+
+private fun List.rotateDown(): List {
+ if (size < 2) return this
+ return listOf(last())
+ .plus(this.dropLast(1))
+}
diff --git a/data/evaluatto/src/test/java/io/github/sadellie/evaluatto/RPNEngineKtTest.kt b/data/evaluatto/src/test/java/io/github/sadellie/evaluatto/RPNEngineKtTest.kt
new file mode 100644
index 00000000..b8be0354
--- /dev/null
+++ b/data/evaluatto/src/test/java/io/github/sadellie/evaluatto/RPNEngineKtTest.kt
@@ -0,0 +1,256 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package io.github.sadellie.evaluatto
+
+import com.sadellie.unitto.core.base.MAX_PRECISION
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import java.math.BigDecimal
+
+class RPNEngineKtTest {
+
+ @Test
+ fun testBadOperands() {
+ // no funny business if input and/or stack is empty
+ val actual = RPNCalculation.Divide.perform(
+ input = "",
+ stack = emptyList()
+ )
+
+ assertEquals(RPNResult.BadInput, actual)
+ }
+
+ @Test
+ fun testDivide() {
+ val actual = RPNCalculation.Divide.perform(
+ input = "2",
+ stack = listOf(BigDecimal("5"))
+ )
+
+ if (actual !is RPNResult.Result) throw Exception("Wrong return")
+
+ assertEquals(BigDecimal("2.5").setScale(MAX_PRECISION), actual.input)
+ assertEquals(emptyList(), actual.stack)
+ }
+
+ @Test
+ fun testDivideByZero() {
+ val actual = RPNCalculation.Divide.perform(
+ input = "0",
+ stack = listOf(BigDecimal("5"))
+ )
+
+ assertEquals(RPNResult.DivideByZero, actual)
+ }
+
+ @Test
+ fun testMinus() {
+ val actual = RPNCalculation.Minus.perform(
+ input = "2",
+ stack = listOf(BigDecimal("5"))
+ )
+
+ if (actual !is RPNResult.Result) throw Exception("Wrong return")
+
+ assertEquals(BigDecimal("3"), actual.input)
+ assertEquals(emptyList(), actual.stack)
+ }
+
+ @Test
+ fun testMultiply() {
+ val actual = RPNCalculation.Multiply.perform(
+ input = "2",
+ stack = listOf(BigDecimal("5"))
+ )
+
+ if (actual !is RPNResult.Result) throw Exception("Wrong return")
+
+ assertEquals(BigDecimal("10"), actual.input)
+ assertEquals(emptyList(), actual.stack)
+ }
+
+ @Test
+ fun testNegate() {
+ val actual = RPNCalculation.Negate.perform(
+ input = "2",
+ stack = listOf(BigDecimal("5"))
+ )
+
+ if (actual !is RPNResult.NewInput) throw Exception("Wrong return")
+
+ assertEquals(BigDecimal("-2"), actual.input)
+ }
+
+ @Test
+ fun testPercent() {
+ val actual = RPNCalculation.Percent.perform(
+ input = "150",
+ stack = listOf(BigDecimal("69"))
+ )
+
+ if (actual !is RPNResult.Result) throw Exception("Wrong return")
+
+ assertEquals(BigDecimal("103.5").setScale(MAX_PRECISION), actual.input)
+ assertEquals(emptyList(), actual.stack)
+ }
+
+ @Test
+ fun testPlus() {
+ val actual = RPNCalculation.Plus.perform(
+ input = "150",
+ stack = listOf(BigDecimal("69"))
+ )
+
+ if (actual !is RPNResult.Result) throw Exception("Wrong return")
+
+ assertEquals(BigDecimal("219"), actual.input)
+ assertEquals(emptyList(), actual.stack)
+ }
+
+ @Test
+ fun testPower() {
+ val actual = RPNCalculation.Power.perform(
+ input = "3",
+ stack = listOf(BigDecimal("2"))
+ )
+
+ if (actual !is RPNResult.Result) throw Exception("Wrong return")
+
+ assertEquals(BigDecimal("8"), actual.input)
+ assertEquals(emptyList(), actual.stack)
+ }
+
+ @Test
+ fun testRotateUp() {
+ val actual = RPNCalculation.RotateUp.perform(
+ input = "",
+ stack = listOf(BigDecimal("1"), BigDecimal("2"), BigDecimal("3"))
+ )
+
+ if (actual !is RPNResult.NewStack) throw Exception("Wrong return")
+
+ assertEquals(listOf(BigDecimal("2"), BigDecimal("3"), BigDecimal("1")), actual.stack)
+ }
+
+ @Test
+ fun testRotateDown() {
+ val actual = RPNCalculation.RotateDown.perform(
+ input = "",
+ stack = listOf(BigDecimal("1"), BigDecimal("2"), BigDecimal("3"))
+ )
+
+ if (actual !is RPNResult.NewStack) throw Exception("Wrong return")
+
+ assertEquals(listOf(BigDecimal("3"), BigDecimal("1"), BigDecimal("2")), actual.stack)
+ }
+
+ @Test
+ fun testPop() {
+ val actual = RPNCalculation.Pop.perform(
+ input = "",
+ stack = listOf(BigDecimal("1"), BigDecimal("2"), BigDecimal("3"))
+ )
+
+ if (actual !is RPNResult.Result) throw Exception("Wrong return")
+
+ assertEquals(BigDecimal("3"), actual.input)
+ assertEquals(listOf(BigDecimal("1"), BigDecimal("2")), actual.stack)
+ }
+
+ @Test
+ fun testClear() {
+ val actual = RPNCalculation.Clear.perform(
+ input = "3",
+ stack = listOf(BigDecimal("2"))
+ )
+
+ if (actual !is RPNResult.Result) throw Exception("Wrong return")
+
+ assertEquals(null, actual.input)
+ assertEquals(emptyList(), actual.stack)
+ }
+
+ @Test
+ fun testEnter() {
+ val actual = RPNCalculation.Enter.perform(
+ input = "3",
+ stack = listOf(BigDecimal("2"))
+ )
+
+ if (actual !is RPNResult.Result) throw Exception("Wrong return")
+
+ assertEquals(null, actual.input)
+ assertEquals(listOf(BigDecimal("2"), BigDecimal("3")), actual.stack)
+ }
+
+ @Test
+ fun testSwap() {
+ val actual = RPNCalculation.Swap.perform(
+ input = "3",
+ stack = listOf(BigDecimal("2"))
+ )
+
+ if (actual !is RPNResult.Result) throw Exception("Wrong return")
+
+ assertEquals(BigDecimal("2"), actual.input)
+ assertEquals(listOf(BigDecimal("3")), actual.stack)
+ }
+
+ @Test
+ fun testSwapEmptyInput() {
+ val actual = RPNCalculation.Swap.perform(
+ input = "",
+ stack = listOf(BigDecimal("1"), BigDecimal("2"))
+ )
+
+ if (actual !is RPNResult.NewStack) throw Exception("Wrong return")
+
+ assertEquals(listOf(BigDecimal("2"), BigDecimal("1")), actual.stack)
+ }
+
+ @Test
+ fun testSwapEmptyInputNotEnoughInStack() {
+ val actual = RPNCalculation.Swap.perform(
+ input = "",
+ stack = listOf(BigDecimal("1"))
+ )
+
+ assertEquals(RPNResult.BadInput, actual)
+ }
+
+ @Test
+ fun testSwapEmptyStack() {
+ val actual = RPNCalculation.Swap.perform(
+ input = "123",
+ stack = emptyList()
+ )
+
+ assertEquals(RPNResult.BadInput, actual)
+ }
+
+ @Test
+ fun testSwapEmptyBoth() {
+ val actual = RPNCalculation.Swap.perform(
+ input = "",
+ stack = emptyList()
+ )
+
+ assertEquals(RPNResult.BadInput, actual)
+ }
+}
diff --git a/data/model/src/main/java/com/sadellie/unitto/data/model/repository/UserPreferencesRepository.kt b/data/model/src/main/java/com/sadellie/unitto/data/model/repository/UserPreferencesRepository.kt
index 1157240f..95fcce03 100644
--- a/data/model/src/main/java/com/sadellie/unitto/data/model/repository/UserPreferencesRepository.kt
+++ b/data/model/src/main/java/com/sadellie/unitto/data/model/repository/UserPreferencesRepository.kt
@@ -88,4 +88,6 @@ interface UserPreferencesRepository {
suspend fun updateAcButton(enabled: Boolean)
suspend fun updateClearInputAfterEquals(enabled: Boolean)
+
+ suspend fun updateRpnMode(enabled: Boolean)
}
diff --git a/data/model/src/main/java/com/sadellie/unitto/data/model/userprefs/AppPreferences.kt b/data/model/src/main/java/com/sadellie/unitto/data/model/userprefs/AppPreferences.kt
index 25708eae..82ba34fa 100644
--- a/data/model/src/main/java/com/sadellie/unitto/data/model/userprefs/AppPreferences.kt
+++ b/data/model/src/main/java/com/sadellie/unitto/data/model/userprefs/AppPreferences.kt
@@ -27,4 +27,5 @@ interface AppPreferences {
val startingScreen: String
val enableToolsExperiment: Boolean
val systemFont: Boolean
+ val rpnMode: Boolean
}
diff --git a/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/PreferenceModels.kt b/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/PreferenceModels.kt
index c24bb23c..03dc71e1 100644
--- a/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/PreferenceModels.kt
+++ b/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/PreferenceModels.kt
@@ -41,6 +41,7 @@ data class AppPreferencesImpl(
override val startingScreen: String,
override val enableToolsExperiment: Boolean,
override val systemFont: Boolean,
+ override val rpnMode: Boolean,
) : AppPreferences
data class GeneralPreferencesImpl(
diff --git a/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/PrefsKeys.kt b/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/PrefsKeys.kt
index 7ecd722b..c70c2a51 100644
--- a/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/PrefsKeys.kt
+++ b/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/PrefsKeys.kt
@@ -24,28 +24,35 @@ import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
internal object PrefsKeys {
+ // COMMON
val THEMING_MODE = stringPreferencesKey("THEMING_MODE_PREF_KEY")
val ENABLE_DYNAMIC_THEME = booleanPreferencesKey("ENABLE_DYNAMIC_THEME_PREF_KEY")
val ENABLE_AMOLED_THEME = booleanPreferencesKey("ENABLE_AMOLED_THEME_PREF_KEY")
val CUSTOM_COLOR = longPreferencesKey("CUSTOM_COLOR_PREF_KEY")
val MONET_MODE = stringPreferencesKey("MONET_MODE_PREF_KEY")
+ val STARTING_SCREEN = stringPreferencesKey("STARTING_SCREEN_PREF_KEY")
+ val ENABLE_TOOLS_EXPERIMENT = booleanPreferencesKey("ENABLE_TOOLS_EXPERIMENT_PREF_KEY")
+ val SYSTEM_FONT = booleanPreferencesKey("SYSTEM_FONT_PREF_KEY")
+ val ENABLE_VIBRATIONS = booleanPreferencesKey("ENABLE_VIBRATIONS_PREF_KEY")
+ val MIDDLE_ZERO = booleanPreferencesKey("MIDDLE_ZERO_PREF_KEY")
+ val AC_BUTTON = booleanPreferencesKey("AC_BUTTON_PREF_KEY")
+ val RPN_MODE = booleanPreferencesKey("RPN_MODE_PREF_KEY")
+
+ // FORMATTER
val DIGITS_PRECISION = intPreferencesKey("DIGITS_PRECISION_PREF_KEY")
val SEPARATOR = intPreferencesKey("SEPARATOR_PREF_KEY")
val OUTPUT_FORMAT = intPreferencesKey("OUTPUT_FORMAT_PREF_KEY")
+
+ // CALCULATOR
+ val RADIAN_MODE = booleanPreferencesKey("RADIAN_MODE_PREF_KEY")
+ val PARTIAL_HISTORY_VIEW = booleanPreferencesKey("PARTIAL_HISTORY_VIEW_PREF_KEY")
+ val CLEAR_INPUT_AFTER_EQUALS = booleanPreferencesKey("CLEAR_INPUT_AFTER_EQUALS_PREF_KEY")
+
+ // UNIT CONVERTER
val LATEST_LEFT_SIDE = stringPreferencesKey("LATEST_LEFT_SIDE_PREF_KEY")
val LATEST_RIGHT_SIDE = stringPreferencesKey("LATEST_RIGHT_SIDE_PREF_KEY")
val SHOWN_UNIT_GROUPS = stringPreferencesKey("SHOWN_UNIT_GROUPS_PREF_KEY")
- val ENABLE_VIBRATIONS = booleanPreferencesKey("ENABLE_VIBRATIONS_PREF_KEY")
- val ENABLE_TOOLS_EXPERIMENT = booleanPreferencesKey("ENABLE_TOOLS_EXPERIMENT_PREF_KEY")
- val STARTING_SCREEN = stringPreferencesKey("STARTING_SCREEN_PREF_KEY")
- val RADIAN_MODE = booleanPreferencesKey("RADIAN_MODE_PREF_KEY")
- val UNIT_CONVERTER_FAVORITES_ONLY =
- booleanPreferencesKey("UNIT_CONVERTER_FAVORITES_ONLY_PREF_KEY")
+ val UNIT_CONVERTER_FAVORITES_ONLY = booleanPreferencesKey("UNIT_CONVERTER_FAVORITES_ONLY_PREF_KEY")
val UNIT_CONVERTER_FORMAT_TIME = booleanPreferencesKey("UNIT_CONVERTER_FORMAT_TIME_PREF_KEY")
val UNIT_CONVERTER_SORTING = stringPreferencesKey("UNIT_CONVERTER_SORTING_PREF_KEY")
- val MIDDLE_ZERO = booleanPreferencesKey("MIDDLE_ZERO_PREF_KEY")
- val SYSTEM_FONT = booleanPreferencesKey("SYSTEM_FONT_PREF_KEY")
- val PARTIAL_HISTORY_VIEW = booleanPreferencesKey("PARTIAL_HISTORY_VIEW_PREF_KEY")
- val AC_BUTTON = booleanPreferencesKey("AC_BUTTON_PREF_KEY")
- val CLEAR_INPUT_AFTER_EQUALS = booleanPreferencesKey("CLEAR_INPUT_AFTER_EQUALS_PREF_KEY")
}
diff --git a/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/UserPreferences.kt b/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/UserPreferences.kt
index e74567b0..c126d308 100644
--- a/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/UserPreferences.kt
+++ b/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/UserPreferences.kt
@@ -63,7 +63,8 @@ class UserPreferencesRepositoryImpl @Inject constructor(
monetMode = preferences.getMonetMode(),
startingScreen = preferences.getStartingScreen(),
enableToolsExperiment = preferences.getEnableToolsExperiment(),
- systemFont = preferences.getSystemFont()
+ systemFont = preferences.getSystemFont(),
+ rpnMode = preferences.getRpnMode(),
)
}
@@ -287,6 +288,12 @@ class UserPreferencesRepositoryImpl @Inject constructor(
preferences[PrefsKeys.CLEAR_INPUT_AFTER_EQUALS] = enabled
}
}
+
+ override suspend fun updateRpnMode(enabled: Boolean) {
+ dataStore.edit { preferences ->
+ preferences[PrefsKeys.RPN_MODE] = enabled
+ }
+ }
}
private fun Preferences.getEnableDynamicTheme(): Boolean {
@@ -387,6 +394,10 @@ private fun Preferences.getClearInputAfterEquals(): Boolean {
return this[PrefsKeys.CLEAR_INPUT_AFTER_EQUALS] ?: true
}
+private fun Preferences.getRpnMode(): Boolean {
+ return this[PrefsKeys.RPN_MODE] ?: false
+}
+
private inline fun T.letTryOrNull(block: (T) -> R): R? = try {
this?.let(block)
} catch (e: Exception) {
diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/DecimalToFraction.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/DecimalToFraction.kt
index c89edcd4..61374872 100644
--- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/DecimalToFraction.kt
+++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/DecimalToFraction.kt
@@ -19,6 +19,7 @@
package com.sadellie.unitto.feature.calculator
import com.sadellie.unitto.core.base.Token
+import com.sadellie.unitto.data.common.isEqualTo
import java.math.BigDecimal
import java.math.BigInteger
import java.math.RoundingMode
@@ -101,5 +102,4 @@ private fun BigDecimal.repeatingDecimals(): String? {
return null
}
-private fun BigDecimal.isEqualTo(bd: BigDecimal): Boolean = compareTo(bd) == 0
private val maxDenominator by lazy { BigInteger("1000000000") }
diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/InputBox.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/InputBox.kt
new file mode 100644
index 00000000..006384c4
--- /dev/null
+++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/InputBox.kt
@@ -0,0 +1,120 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.feature.calculator
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+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.sadellie.unitto.core.base.OutputFormat
+import com.sadellie.unitto.core.ui.common.textfield.ExpressionTextField
+import com.sadellie.unitto.core.ui.common.textfield.FixedInputTextField
+import com.sadellie.unitto.core.ui.common.textfield.FormatterSymbols
+import com.sadellie.unitto.data.common.format
+import java.math.BigDecimal
+
+@Composable
+internal fun InputBox(
+ modifier: Modifier,
+ input: TextFieldValue,
+ onCursorChange: (TextRange) -> Unit,
+ stack: List,
+ formatterSymbols: FormatterSymbols,
+ precision: Int,
+ outputFormat: Int,
+) {
+ val listState = rememberLazyListState()
+
+ LaunchedEffect(stack) {
+ listState.animateScrollToItem(stack.lastIndex.coerceAtLeast(0))
+ }
+
+ Column(
+ modifier = modifier
+ .clip(RoundedCornerShape(24.dp))
+ .fillMaxWidth()
+ .background(MaterialTheme.colorScheme.surfaceVariant)
+ .padding(start = 12.dp, end = 12.dp, bottom = 12.dp),
+ verticalArrangement = Arrangement.Bottom
+ ) {
+ LazyColumn(
+ modifier = Modifier.weight(1f),
+ state = listState,
+ verticalArrangement = Arrangement.Bottom,
+ ) {
+ items(stack) {
+ FixedInputTextField(
+ modifier = Modifier.fillMaxWidth(),
+ value = it.format(precision, outputFormat),
+ formatterSymbols = formatterSymbols,
+ textColor = MaterialTheme.colorScheme.onSurfaceVariant,
+ onClick = {}
+ )
+ }
+ }
+
+ ExpressionTextField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .fillMaxHeight(0.25f),
+ value = input,
+ minRatio = 0.6f,
+ onCursorChange = onCursorChange,
+ formatterSymbols = formatterSymbols,
+ textColor = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+}
+
+@Preview(device = "spec:width=1080px,height=2160px,dpi=440")
+@Composable
+fun PreviewInputBox() {
+ InputBox(
+ modifier = Modifier.fillMaxSize(),
+ input = TextFieldValue("123456.789"),
+ onCursorChange = {},
+ stack = listOf(
+ BigDecimal("123456.7890"),
+ BigDecimal("123456.7890"),
+ BigDecimal("123456.7890"),
+ BigDecimal("123456.7890"),
+ BigDecimal("123456.7890"),
+ BigDecimal("123456.7890"),
+ ),
+ formatterSymbols = FormatterSymbols.Spaces,
+ precision = 3,
+ outputFormat = OutputFormat.PLAIN
+ )
+}
diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/RPNCalculatorKeyboard.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/RPNCalculatorKeyboard.kt
new file mode 100644
index 00000000..b517b75b
--- /dev/null
+++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/RPNCalculatorKeyboard.kt
@@ -0,0 +1,149 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.feature.calculator
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import com.sadellie.unitto.core.base.Token
+import com.sadellie.unitto.core.ui.common.KeyboardButtonAdditional
+import com.sadellie.unitto.core.ui.common.KeyboardButtonFilled
+import com.sadellie.unitto.core.ui.common.KeyboardButtonLight
+import com.sadellie.unitto.core.ui.common.KeyboardButtonTertiary
+import com.sadellie.unitto.core.ui.common.key.UnittoIcons
+import com.sadellie.unitto.core.ui.common.key.unittoicons.Backspace
+import com.sadellie.unitto.core.ui.common.key.unittoicons.Clear
+import com.sadellie.unitto.core.ui.common.key.unittoicons.Comma
+import com.sadellie.unitto.core.ui.common.key.unittoicons.Divide
+import com.sadellie.unitto.core.ui.common.key.unittoicons.Dot
+import com.sadellie.unitto.core.ui.common.key.unittoicons.Down
+import com.sadellie.unitto.core.ui.common.key.unittoicons.Enter
+import com.sadellie.unitto.core.ui.common.key.unittoicons.Key0
+import com.sadellie.unitto.core.ui.common.key.unittoicons.Key1
+import com.sadellie.unitto.core.ui.common.key.unittoicons.Key2
+import com.sadellie.unitto.core.ui.common.key.unittoicons.Key3
+import com.sadellie.unitto.core.ui.common.key.unittoicons.Key4
+import com.sadellie.unitto.core.ui.common.key.unittoicons.Key5
+import com.sadellie.unitto.core.ui.common.key.unittoicons.Key6
+import com.sadellie.unitto.core.ui.common.key.unittoicons.Key7
+import com.sadellie.unitto.core.ui.common.key.unittoicons.Key8
+import com.sadellie.unitto.core.ui.common.key.unittoicons.Key9
+import com.sadellie.unitto.core.ui.common.key.unittoicons.Minus
+import com.sadellie.unitto.core.ui.common.key.unittoicons.Multiply
+import com.sadellie.unitto.core.ui.common.key.unittoicons.Percent
+import com.sadellie.unitto.core.ui.common.key.unittoicons.Plus
+import com.sadellie.unitto.core.ui.common.key.unittoicons.Pop
+import com.sadellie.unitto.core.ui.common.key.unittoicons.Swap
+import com.sadellie.unitto.core.ui.common.key.unittoicons.Unary
+import com.sadellie.unitto.core.ui.common.key.unittoicons.Up
+import io.github.sadellie.evaluatto.RPNCalculation
+
+@OptIn(ExperimentalLayoutApi::class)
+@Composable
+internal fun RPNCalculatorKeyboard(
+ modifier: Modifier,
+ fractional: String,
+ middleZero: Boolean,
+ allowVibration: Boolean,
+ onCalculationClick: (RPNCalculation) -> Unit,
+ onInputEditClick: (RPNInputEdit) -> Unit,
+) {
+ val fractionalIcon = remember(fractional) { if (fractional == Token.Digit.dot) UnittoIcons.Dot else UnittoIcons.Comma }
+
+ val columns = 4
+ val mainButtonRows = 5f
+ val additionalButtonRows = 1f
+ val additionalButtonRowFactor = 0.7f // How much smaller are the additional buttons than the main buttons
+ val additionalButtonIconHeight = 0.65f
+ val fillFactor = 0.92f
+
+ val rows = remember { mainButtonRows + additionalButtonRows * additionalButtonRowFactor }
+ val height = remember { fillFactor / rows }
+ val width = remember { fillFactor / columns }
+
+ FlowRow(
+ maxItemsInEachRow = columns,
+ modifier = modifier,
+ horizontalArrangement = Arrangement.SpaceAround,
+ verticalArrangement = Arrangement.SpaceAround
+ ) {
+ val aModifier = Modifier
+ .fillMaxHeight(height * additionalButtonRowFactor)
+ .fillMaxWidth(width)
+
+ val bModifier = Modifier
+ .fillMaxHeight(height)
+ .fillMaxWidth(width)
+
+ KeyboardButtonAdditional(modifier = aModifier, icon = UnittoIcons.Swap, allowVibration = allowVibration, contentHeight = additionalButtonIconHeight) { onCalculationClick(RPNCalculation.Swap) }
+ KeyboardButtonAdditional(modifier = aModifier, icon = UnittoIcons.Up, allowVibration = allowVibration, contentHeight = additionalButtonIconHeight) { onCalculationClick(RPNCalculation.RotateUp) }
+ KeyboardButtonAdditional(modifier = aModifier, icon = UnittoIcons.Down, allowVibration = allowVibration, contentHeight = additionalButtonIconHeight) { onCalculationClick(RPNCalculation.RotateDown) }
+ KeyboardButtonAdditional(modifier = aModifier, icon = UnittoIcons.Pop, allowVibration = allowVibration, contentHeight = additionalButtonIconHeight) { onCalculationClick(RPNCalculation.Pop) }
+
+ KeyboardButtonTertiary(modifier = bModifier, icon = UnittoIcons.Clear, allowVibration = allowVibration) { onCalculationClick(RPNCalculation.Clear) }
+ KeyboardButtonFilled(modifier = bModifier, icon = UnittoIcons.Unary, allowVibration = allowVibration) { onCalculationClick(RPNCalculation.Negate) }
+ KeyboardButtonFilled(modifier = bModifier, icon = UnittoIcons.Percent, allowVibration = allowVibration) { onCalculationClick(RPNCalculation.Percent) }
+ KeyboardButtonFilled(modifier = bModifier, icon = UnittoIcons.Divide, allowVibration = allowVibration) { onCalculationClick(RPNCalculation.Divide) }
+
+ KeyboardButtonLight(modifier = bModifier, icon = UnittoIcons.Key7, allowVibration = allowVibration) { onInputEditClick(RPNInputEdit.Digit(Token.Digit._7)) }
+ KeyboardButtonLight(modifier = bModifier, icon = UnittoIcons.Key8, allowVibration = allowVibration) { onInputEditClick(RPNInputEdit.Digit(Token.Digit._8)) }
+ KeyboardButtonLight(modifier = bModifier, icon = UnittoIcons.Key9, allowVibration = allowVibration) { onInputEditClick(RPNInputEdit.Digit(Token.Digit._9)) }
+ KeyboardButtonFilled(modifier = bModifier, icon = UnittoIcons.Multiply, allowVibration = allowVibration) { onCalculationClick(RPNCalculation.Multiply) }
+
+ KeyboardButtonLight(modifier = bModifier, icon = UnittoIcons.Key4, allowVibration = allowVibration) { onInputEditClick(RPNInputEdit.Digit(Token.Digit._4)) }
+ KeyboardButtonLight(modifier = bModifier, icon = UnittoIcons.Key5, allowVibration = allowVibration) { onInputEditClick(RPNInputEdit.Digit(Token.Digit._5)) }
+ KeyboardButtonLight(modifier = bModifier, icon = UnittoIcons.Key6, allowVibration = allowVibration) { onInputEditClick(RPNInputEdit.Digit(Token.Digit._6)) }
+ KeyboardButtonFilled(modifier = bModifier, icon = UnittoIcons.Minus, allowVibration = allowVibration) { onCalculationClick(RPNCalculation.Minus) }
+
+ KeyboardButtonLight(modifier = bModifier, icon = UnittoIcons.Key1, allowVibration = allowVibration) { onInputEditClick(RPNInputEdit.Digit(Token.Digit._1)) }
+ KeyboardButtonLight(modifier = bModifier, icon = UnittoIcons.Key2, allowVibration = allowVibration) { onInputEditClick(RPNInputEdit.Digit(Token.Digit._2)) }
+ KeyboardButtonLight(modifier = bModifier, icon = UnittoIcons.Key3, allowVibration = allowVibration) { onInputEditClick(RPNInputEdit.Digit(Token.Digit._3)) }
+ KeyboardButtonFilled(modifier = bModifier, icon = UnittoIcons.Plus, allowVibration = allowVibration) { onCalculationClick(RPNCalculation.Plus) }
+
+ if (middleZero) {
+ KeyboardButtonLight(modifier = bModifier, icon = UnittoIcons.Key0, allowVibration = allowVibration) { onInputEditClick(RPNInputEdit.Digit(Token.Digit._0)) }
+ KeyboardButtonLight(modifier = bModifier, icon = fractionalIcon, allowVibration = allowVibration) { onInputEditClick(RPNInputEdit.Dot) }
+ } else {
+ KeyboardButtonLight(modifier = bModifier, icon = fractionalIcon, allowVibration = allowVibration) { onInputEditClick(RPNInputEdit.Dot) }
+ KeyboardButtonLight(modifier = bModifier, icon = UnittoIcons.Key0, allowVibration = allowVibration) { onInputEditClick(RPNInputEdit.Digit(Token.Digit._0)) }
+ }
+ KeyboardButtonLight(modifier = bModifier, icon = UnittoIcons.Backspace, allowVibration = allowVibration) { onInputEditClick(RPNInputEdit.Delete) }
+ KeyboardButtonFilled(modifier = bModifier, icon = UnittoIcons.Enter, allowVibration = allowVibration) { onCalculationClick(RPNCalculation.Enter) }
+ }
+}
+
+@Preview
+@Composable
+private fun PreviewKeyboard() {
+ RPNCalculatorKeyboard(
+ modifier = Modifier.fillMaxSize(),
+ fractional = Token.Digit.dot,
+ middleZero = false,
+ allowVibration = false,
+ onCalculationClick = {},
+ onInputEditClick = {}
+ )
+}
diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/RPNCalculatorScreen.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/RPNCalculatorScreen.kt
new file mode 100644
index 00000000..49cc302c
--- /dev/null
+++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/RPNCalculatorScreen.kt
@@ -0,0 +1,134 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.feature.calculator
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+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 androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.sadellie.unitto.core.base.OutputFormat
+import com.sadellie.unitto.core.base.R
+import com.sadellie.unitto.core.ui.common.MenuButton
+import com.sadellie.unitto.core.ui.common.SettingsButton
+import com.sadellie.unitto.core.ui.common.UnittoEmptyScreen
+import com.sadellie.unitto.core.ui.common.UnittoScreenWithTopBar
+import com.sadellie.unitto.core.ui.common.textfield.FormatterSymbols
+import io.github.sadellie.evaluatto.RPNCalculation
+import java.math.BigDecimal
+
+@Composable
+internal fun RPNCalculatorRoute(
+ openDrawer: () -> Unit,
+ navigateToSettings: () -> Unit,
+ viewModel: RPNCalculatorViewModel = hiltViewModel(),
+) {
+ when (val uiState = viewModel.uiState.collectAsStateWithLifecycle().value) {
+ RPNCalculatorUIState.Loading -> UnittoEmptyScreen()
+ is RPNCalculatorUIState.Ready -> RPNCalculatorScreen(
+ uiState = uiState,
+ openDrawer = openDrawer,
+ navigateToSettings = navigateToSettings,
+ onCursorChange = viewModel::onCursorChange,
+ onCalculationClick = viewModel::onCalculationClick,
+ onInputEditClick = viewModel::onInputEdit
+ )
+ }
+}
+
+@Composable
+internal fun RPNCalculatorScreen(
+ uiState: RPNCalculatorUIState.Ready,
+ openDrawer: () -> Unit,
+ navigateToSettings: () -> Unit,
+ onCursorChange: (TextRange) -> Unit,
+ onCalculationClick: (RPNCalculation) -> Unit,
+ onInputEditClick: (RPNInputEdit) -> Unit,
+) {
+ UnittoScreenWithTopBar(
+ title = { Text(stringResource(id = R.string.calculator_title)) },
+ navigationIcon = { MenuButton(openDrawer) },
+ actions = { SettingsButton(navigateToSettings) }
+ ) { paddingValues ->
+ Column(
+ Modifier.padding(paddingValues)
+ ) {
+ InputBox(
+ modifier = Modifier
+ .padding(8.dp)
+ .fillMaxHeight(0.3f),
+ input = uiState.input,
+ stack = uiState.stack,
+ formatterSymbols = uiState.formatterSymbols,
+ precision = uiState.precision,
+ outputFormat = uiState.outputFormat,
+ onCursorChange = onCursorChange
+ )
+ RPNCalculatorKeyboard(
+ modifier = Modifier
+ .padding(horizontal = 4.dp)
+ .fillMaxSize(),
+ fractional = uiState.formatterSymbols.fractional,
+ middleZero = uiState.middleZero,
+ allowVibration = uiState.allowVibration,
+ onCalculationClick = onCalculationClick,
+ onInputEditClick = onInputEditClick
+ )
+ }
+ }
+}
+
+@Preview(widthDp = 432, heightDp = 1008, device = "spec:parent=pixel_5,orientation=portrait")
+@Preview(widthDp = 432, heightDp = 864, device = "spec:parent=pixel_5,orientation=portrait")
+@Preview(widthDp = 597, heightDp = 1393, device = "spec:parent=pixel_5,orientation=portrait")
+@Composable
+private fun RPNCalculatorScreenPreview() {
+ RPNCalculatorScreen(
+ uiState = RPNCalculatorUIState.Ready(
+ input = TextFieldValue("test"),
+ stack = listOf(
+ BigDecimal("123456.7890"),
+ BigDecimal("123456.7890"),
+ BigDecimal("123456.7890"),
+ BigDecimal("123456.7890"),
+ BigDecimal("123456.7890"),
+ BigDecimal("123456.7890"),
+ ),
+ precision = 3,
+ outputFormat = OutputFormat.PLAIN,
+ formatterSymbols = FormatterSymbols.Spaces,
+ allowVibration = true,
+ middleZero = true,
+ ),
+ openDrawer = {},
+ navigateToSettings = {},
+ onCalculationClick = {},
+ onInputEditClick = {},
+ onCursorChange = {}
+ )
+}
diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/RPNCalculatorUIState.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/RPNCalculatorUIState.kt
new file mode 100644
index 00000000..18b6e21b
--- /dev/null
+++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/RPNCalculatorUIState.kt
@@ -0,0 +1,37 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.feature.calculator
+
+import androidx.compose.ui.text.input.TextFieldValue
+import com.sadellie.unitto.core.ui.common.textfield.FormatterSymbols
+import java.math.BigDecimal
+
+internal sealed class RPNCalculatorUIState {
+ data object Loading : RPNCalculatorUIState()
+
+ data class Ready(
+ val input: TextFieldValue,
+ val stack: List,
+ val precision: Int,
+ val outputFormat: Int,
+ val formatterSymbols: FormatterSymbols,
+ val allowVibration: Boolean,
+ val middleZero: Boolean,
+ ) : RPNCalculatorUIState()
+}
diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/RPNCalculatorViewModel.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/RPNCalculatorViewModel.kt
new file mode 100644
index 00000000..ec9b6269
--- /dev/null
+++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/RPNCalculatorViewModel.kt
@@ -0,0 +1,118 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.feature.calculator
+
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.sadellie.unitto.core.base.Token
+import com.sadellie.unitto.core.ui.common.textfield.AllFormatterSymbols
+import com.sadellie.unitto.core.ui.common.textfield.addTokens
+import com.sadellie.unitto.core.ui.common.textfield.deleteTokens
+import com.sadellie.unitto.core.ui.common.textfield.getTextField
+import com.sadellie.unitto.data.common.format
+import com.sadellie.unitto.data.common.stateIn
+import com.sadellie.unitto.data.model.repository.UserPreferencesRepository
+import dagger.hilt.android.lifecycle.HiltViewModel
+import io.github.sadellie.evaluatto.RPNCalculation
+import io.github.sadellie.evaluatto.RPNResult
+import io.github.sadellie.evaluatto.perform
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.math.BigDecimal
+import javax.inject.Inject
+
+@HiltViewModel
+internal class RPNCalculatorViewModel @Inject constructor(
+ userPrefsRepository: UserPreferencesRepository,
+ private val savedStateHandle: SavedStateHandle,
+) : ViewModel() {
+
+ private val _inputKey = "RPN_CALCULATOR_INPUT"
+ private val _input = MutableStateFlow(savedStateHandle.getTextField(_inputKey))
+ private val _stack = MutableStateFlow(emptyList())
+ private val _prefs = userPrefsRepository.calculatorPrefs.stateIn(viewModelScope, null)
+
+ val uiState = combine(
+ _prefs,
+ _input,
+ _stack
+ ) { prefs, input, stack ->
+ prefs ?: return@combine RPNCalculatorUIState.Loading
+
+ return@combine RPNCalculatorUIState.Ready(
+ input = input,
+ stack = stack,
+ precision = prefs.precision,
+ outputFormat = prefs.outputFormat,
+ formatterSymbols = AllFormatterSymbols.getById(prefs.separator),
+ allowVibration = prefs.enableVibrations,
+ middleZero = prefs.middleZero
+ )
+ }
+ .stateIn(viewModelScope, RPNCalculatorUIState.Loading)
+
+ fun onInputEdit(action: RPNInputEdit) {
+ val input = _input.value
+ val newInput = when (action) {
+ is RPNInputEdit.Digit -> input.addTokens(action.value)
+ RPNInputEdit.Dot -> {
+ if (_input.value.text.contains(Token.Digit.dot)) return
+ input.addTokens(Token.Digit.dot)
+ }
+ RPNInputEdit.Delete -> input.deleteTokens()
+ }
+
+ savedStateHandle[_inputKey] = newInput.text
+ }
+
+ fun onCalculationClick(action: RPNCalculation) = viewModelScope.launch {
+ val prefs = _prefs.value ?: return@launch
+
+ val newResult = withContext(Dispatchers.Default) {
+ action.perform(_input.value.text, _stack.value)
+ }
+
+ when (newResult) {
+ is RPNResult.Result -> {
+ val newInput = newResult.input?.format(prefs.precision, prefs.outputFormat) ?: ""
+ _input.update { TextFieldValue(newInput, TextRange(newInput.length)) }
+ _stack.update { newResult.stack }
+ }
+
+ is RPNResult.NewStack -> {
+ _stack.update { newResult.stack }
+ }
+
+ is RPNResult.NewInput -> {
+ val newInput = newResult.input.format(prefs.precision, prefs.outputFormat)
+ _input.update { TextFieldValue(newInput, TextRange(newInput.length)) }
+ }
+ RPNResult.BadInput, RPNResult.DivideByZero -> Unit
+ }
+ }
+
+ fun onCursorChange(selection: TextRange) = _input.update { it.copy(selection = selection) }
+}
diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/RPNInputEdit.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/RPNInputEdit.kt
new file mode 100644
index 00000000..b6a2fe46
--- /dev/null
+++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/RPNInputEdit.kt
@@ -0,0 +1,25 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.feature.calculator
+
+sealed class RPNInputEdit {
+ data class Digit(val value: String) : RPNInputEdit()
+ data object Delete : RPNInputEdit()
+ data object Dot : RPNInputEdit()
+}
diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/HistoryList.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/HistoryList.kt
index 544531c9..3ffb18f8 100644
--- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/HistoryList.kt
+++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/HistoryList.kt
@@ -19,53 +19,30 @@
package com.sadellie.unitto.feature.calculator.components
import androidx.compose.foundation.background
-import androidx.compose.foundation.horizontalScroll
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.History
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalFocusManager
-import androidx.compose.ui.platform.LocalTextInputService
-import androidx.compose.ui.platform.LocalTextToolbar
-import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.TextRange
-import androidx.compose.ui.text.input.TextFieldValue
-import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.sadellie.unitto.core.base.R
-import com.sadellie.unitto.core.ui.common.textfield.ExpressionTransformer
+import com.sadellie.unitto.core.ui.common.textfield.FixedInputTextField
import com.sadellie.unitto.core.ui.common.textfield.FormatterSymbols
-import com.sadellie.unitto.core.ui.common.textfield.UnittoTextToolbar
-import com.sadellie.unitto.core.ui.common.textfield.clearAndFilterExpression
-import com.sadellie.unitto.core.ui.common.textfield.copyWithoutGrouping
-import com.sadellie.unitto.core.ui.theme.LocalNumberTypography
import com.sadellie.unitto.data.model.HistoryItem
import java.text.SimpleDateFormat
import java.util.Locale
@@ -151,83 +128,23 @@ private fun HistoryListItem(
formatterSymbols: FormatterSymbols,
addTokens: (String) -> Unit,
) {
- val clipboardManager = LocalClipboardManager.current
- val expression = historyItem.expression.take(1000)
- var expressionValue by remember(expression) {
- mutableStateOf(TextFieldValue(expression, TextRange(expression.length)))
- }
- val result = historyItem.result.take(1000)
- var resultValue by remember(result) {
- mutableStateOf(TextFieldValue(result, TextRange(result.length)))
- }
-
- val expressionInteractionSource = remember(expression) { MutableInteractionSource() }
- LaunchedEffect(expressionInteractionSource) {
- expressionInteractionSource.interactions.collect {
- if (it is PressInteraction.Release) addTokens(expression.clearAndFilterExpression(formatterSymbols))
- }
- }
-
- val resultInteractionSource = remember(result) { MutableInteractionSource() }
- LaunchedEffect(resultInteractionSource) {
- resultInteractionSource.interactions.collect {
- if (it is PressInteraction.Release) addTokens(result.clearAndFilterExpression(formatterSymbols))
- }
- }
-
Column(
modifier = modifier.height(HistoryItemHeight),
verticalArrangement = Arrangement.Center
) {
- CompositionLocalProvider(
- LocalTextInputService provides null,
- LocalTextToolbar provides UnittoTextToolbar(
- view = LocalView.current,
- copyCallback = {
- clipboardManager.copyWithoutGrouping(expressionValue, formatterSymbols)
- expressionValue = expressionValue.copy(selection = TextRange(expressionValue.selection.end))
- }
- )
- ) {
- BasicTextField(
- value = expressionValue,
- onValueChange = { expressionValue = it },
- maxLines = 1,
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 8.dp)
- .horizontalScroll(rememberScrollState(), reverseScrolling = true),
- textStyle = LocalNumberTypography.current.displaySmall.copy(color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.End),
- readOnly = true,
- visualTransformation = ExpressionTransformer(formatterSymbols),
- interactionSource = expressionInteractionSource
- )
- }
+ FixedInputTextField(
+ value = historyItem.expression,
+ formatterSymbols = formatterSymbols,
+ textColor = MaterialTheme.colorScheme.onSurfaceVariant,
+ onClick = addTokens,
+ )
- CompositionLocalProvider(
- LocalTextInputService provides null,
- LocalTextToolbar provides UnittoTextToolbar(
- view = LocalView.current,
- copyCallback = {
- clipboardManager.copyWithoutGrouping(resultValue, formatterSymbols)
- resultValue = resultValue.copy(selection = TextRange(resultValue.selection.end))
- }
- )
- ) {
- BasicTextField(
- value = resultValue,
- onValueChange = { resultValue = it },
- maxLines = 1,
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 8.dp)
- .horizontalScroll(rememberScrollState(), reverseScrolling = true),
- textStyle = LocalNumberTypography.current.displaySmall.copy(color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), textAlign = TextAlign.End),
- readOnly = true,
- visualTransformation = ExpressionTransformer(formatterSymbols),
- interactionSource = resultInteractionSource
- )
- }
+ FixedInputTextField(
+ value = historyItem.result,
+ formatterSymbols = formatterSymbols,
+ textColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f),
+ onClick = addTokens,
+ )
}
}
diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/navigation/CalculatorNavigation.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/navigation/CalculatorNavigation.kt
index fbae4ef0..fbabfcc5 100644
--- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/navigation/CalculatorNavigation.kt
+++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/navigation/CalculatorNavigation.kt
@@ -24,12 +24,14 @@ import com.sadellie.unitto.core.base.TopLevelDestinations
import com.sadellie.unitto.core.ui.unittoComposable
import com.sadellie.unitto.core.ui.unittoNavigation
import com.sadellie.unitto.feature.calculator.CalculatorRoute
+import com.sadellie.unitto.feature.calculator.RPNCalculatorRoute
private val graph = TopLevelDestinations.Calculator.graph
private val start = TopLevelDestinations.Calculator.start
fun NavGraphBuilder.calculatorGraph(
- navigateToMenu: () -> Unit,
+ rpnMode: Boolean,
+ openDrawer: () -> Unit,
navigateToSettings: () -> Unit
) {
unittoNavigation(
@@ -40,10 +42,17 @@ fun NavGraphBuilder.calculatorGraph(
)
) {
unittoComposable(start) {
- CalculatorRoute(
- navigateToMenu = navigateToMenu,
- navigateToSettings = navigateToSettings
- )
+ if (rpnMode) {
+ RPNCalculatorRoute(
+ openDrawer = openDrawer,
+ navigateToSettings = navigateToSettings
+ )
+ } else {
+ CalculatorRoute(
+ navigateToMenu = openDrawer,
+ navigateToSettings = navigateToSettings
+ )
+ }
}
}
}
diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/calculator/CalculatorSettingsScreen.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/calculator/CalculatorSettingsScreen.kt
index 270b5eb1..4f5cada4 100644
--- a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/calculator/CalculatorSettingsScreen.kt
+++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/calculator/CalculatorSettingsScreen.kt
@@ -18,73 +18,111 @@
package com.sadellie.unitto.feature.settings.calculator
-import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.animation.Crossfade
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Backspace
import androidx.compose.material.icons.filled.Timer
+import androidx.compose.material3.SegmentedButton
+import androidx.compose.material3.SegmentedButtonDefaults
+import androidx.compose.material3.SingleChoiceSegmentedButtonRow
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.sadellie.unitto.core.base.OutputFormat
import com.sadellie.unitto.core.base.R
-import com.sadellie.unitto.core.base.Separator
import com.sadellie.unitto.core.ui.common.NavigateUpButton
import com.sadellie.unitto.core.ui.common.UnittoEmptyScreen
import com.sadellie.unitto.core.ui.common.UnittoListItem
import com.sadellie.unitto.core.ui.common.UnittoScreenWithLargeTopBar
-import com.sadellie.unitto.data.model.userprefs.CalculatorPreferences
-import com.sadellie.unitto.data.userprefs.CalculatorPreferencesImpl
@Composable
internal fun CalculatorSettingsRoute(
viewModel: CalculatorSettingsViewModel = hiltViewModel(),
navigateUpAction: () -> Unit,
) {
- when (val prefs = viewModel.prefs.collectAsStateWithLifecycle().value) {
- null -> UnittoEmptyScreen()
+ when (val prefs = viewModel.uiState.collectAsStateWithLifecycle().value) {
+ CalculatorSettingsUIState.Loading -> UnittoEmptyScreen()
else -> {
CalculatorSettingsScreen(
- prefs = prefs,
+ uiState = prefs,
navigateUpAction = navigateUpAction,
updatePartialHistoryView = viewModel::updatePartialHistoryView,
updateClearInputAfterEquals = viewModel::updateClearInputAfterEquals,
+ updateRpnMode = viewModel::updateRpnMode,
)
}
}
}
+// TODO Translate
@Composable
private fun CalculatorSettingsScreen(
- prefs: CalculatorPreferences,
+ uiState: CalculatorSettingsUIState,
navigateUpAction: () -> Unit,
updatePartialHistoryView: (Boolean) -> Unit,
updateClearInputAfterEquals: (Boolean) -> Unit,
+ updateRpnMode: (Boolean) -> Unit,
) {
UnittoScreenWithLargeTopBar(
title = stringResource(R.string.calculator_title),
navigationIcon = { NavigateUpButton(navigateUpAction) }
) { padding ->
- LazyColumn(contentPadding = padding) {
- item("partial history") {
- UnittoListItem(
- headlineText = stringResource(R.string.settings_partial_history_view),
- icon = Icons.Default.Timer,
- supportingText = stringResource(R.string.settings_partial_history_view_support),
- switchState = prefs.partialHistoryView,
- onSwitchChange = updatePartialHistoryView
- )
+ Column(Modifier.padding(padding)) {
+ SingleChoiceSegmentedButtonRow(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp)
+ ) {
+ SegmentedButton(
+ selected = uiState is CalculatorSettingsUIState.Standard,
+ onClick = { updateRpnMode(false) },
+ shape = SegmentedButtonDefaults.itemShape(index = 0, count = 2),
+ ) {
+ Text("Standard")
+ }
+ SegmentedButton(
+ selected = uiState == CalculatorSettingsUIState.RPN,
+ onClick = { updateRpnMode(true) },
+ shape = SegmentedButtonDefaults.itemShape(index = 1, count = 2),
+ ) {
+ Text("RPN")
+ }
}
- item("clear input") {
- UnittoListItem(
- headlineText = stringResource(R.string.settings_clear_input),
- icon = Icons.AutoMirrored.Filled.Backspace,
- supportingText = stringResource(R.string.settings_clear_input_support),
- switchState = prefs.clearInputAfterEquals,
- onSwitchChange = updateClearInputAfterEquals
- )
+ Crossfade(
+ targetState = uiState,
+ label = "Mode switch"
+ ) { state ->
+ when (state) {
+ is CalculatorSettingsUIState.Standard -> {
+ Column {
+ UnittoListItem(
+ headlineText = stringResource(R.string.settings_partial_history_view),
+ icon = Icons.Default.Timer,
+ supportingText = stringResource(R.string.settings_partial_history_view_support),
+ switchState = state.partialHistoryView,
+ onSwitchChange = updatePartialHistoryView
+ )
+
+ UnittoListItem(
+ headlineText = stringResource(R.string.settings_clear_input),
+ icon = Icons.AutoMirrored.Filled.Backspace,
+ supportingText = stringResource(R.string.settings_clear_input_support),
+ switchState = state.clearInputAfterEquals,
+ onSwitchChange = updateClearInputAfterEquals
+ )
+ }
+ }
+
+ else -> Unit
+ }
}
}
}
@@ -92,21 +130,27 @@ private fun CalculatorSettingsScreen(
@Preview
@Composable
-private fun PreviewCalculatorSettingsScreen() {
+private fun PreviewCalculatorSettingsScreenStandard() {
CalculatorSettingsScreen(
- prefs = CalculatorPreferencesImpl(
- radianMode = false,
- enableVibrations = false,
- separator = Separator.SPACE,
- middleZero = false,
+ uiState = CalculatorSettingsUIState.Standard(
partialHistoryView = true,
- precision = 3,
- outputFormat = OutputFormat.PLAIN,
- acButton = true,
- clearInputAfterEquals = true,
+ clearInputAfterEquals = false
),
navigateUpAction = {},
updatePartialHistoryView = {},
updateClearInputAfterEquals = {},
+ updateRpnMode = {}
+ )
+}
+
+@Preview
+@Composable
+private fun PreviewCalculatorSettingsScreenRPN() {
+ CalculatorSettingsScreen(
+ uiState = CalculatorSettingsUIState.RPN,
+ navigateUpAction = {},
+ updatePartialHistoryView = {},
+ updateClearInputAfterEquals = {},
+ updateRpnMode = {}
)
}
diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/calculator/CalculatorSettingsUIState.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/calculator/CalculatorSettingsUIState.kt
new file mode 100644
index 00000000..f7fa8ecf
--- /dev/null
+++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/calculator/CalculatorSettingsUIState.kt
@@ -0,0 +1,30 @@
+/*
+ * Unitto is a unit converter for Android
+ * Copyright (c) 2023 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.feature.settings.calculator
+
+internal sealed class CalculatorSettingsUIState {
+ data object Loading : CalculatorSettingsUIState()
+
+ data object RPN : CalculatorSettingsUIState()
+
+ data class Standard(
+ val partialHistoryView: Boolean,
+ val clearInputAfterEquals: Boolean,
+ ) : CalculatorSettingsUIState()
+}
diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/calculator/CalculatorSettingsViewModel.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/calculator/CalculatorSettingsViewModel.kt
index e887b5dc..e7ba599a 100644
--- a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/calculator/CalculatorSettingsViewModel.kt
+++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/calculator/CalculatorSettingsViewModel.kt
@@ -23,6 +23,7 @@ import androidx.lifecycle.viewModelScope
import com.sadellie.unitto.data.common.stateIn
import com.sadellie.unitto.data.model.repository.UserPreferencesRepository
import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -30,8 +31,20 @@ import javax.inject.Inject
internal class CalculatorSettingsViewModel @Inject constructor(
private val userPrefsRepository: UserPreferencesRepository,
) : ViewModel() {
- val prefs = userPrefsRepository.calculatorPrefs
- .stateIn(viewModelScope, null)
+ val uiState = combine(
+ userPrefsRepository.appPrefs,
+ userPrefsRepository.calculatorPrefs,
+ ) { app, calc ->
+ if (app.rpnMode) {
+ CalculatorSettingsUIState.RPN
+ } else {
+ CalculatorSettingsUIState.Standard(
+ partialHistoryView = calc.partialHistoryView,
+ clearInputAfterEquals = calc.clearInputAfterEquals
+ )
+ }
+ }
+ .stateIn(viewModelScope, CalculatorSettingsUIState.Loading)
fun updatePartialHistoryView(enabled: Boolean) = viewModelScope.launch {
userPrefsRepository.updatePartialHistoryView(enabled)
@@ -40,4 +53,8 @@ internal class CalculatorSettingsViewModel @Inject constructor(
fun updateClearInputAfterEquals(enabled: Boolean) = viewModelScope.launch {
userPrefsRepository.updateClearInputAfterEquals(enabled)
}
+
+ fun updateRpnMode(enabled: Boolean) = viewModelScope.launch {
+ userPrefsRepository.updateRpnMode(enabled)
+ }
}
diff --git a/password.txt b/password.txt
new file mode 100644
index 00000000..bd27d794
--- /dev/null
+++ b/password.txt
@@ -0,0 +1,3 @@
+009D4E
+
+super secret classified sauce, don't share