Newer
Older
package org.futo.inputmethod.v2keyboard
import android.content.Context
import android.graphics.Rect
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.window.layout.FoldingFeature
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.Serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.descriptors.element
import kotlinx.serialization.encodeToString
import kotlinx.serialization.encoding.CompositeDecoder
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.encoding.decodeStructure
import kotlinx.serialization.encoding.encodeStructure
import kotlinx.serialization.json.Json
import org.futo.inputmethod.latin.FoldStateProvider
import org.futo.inputmethod.latin.LatinIME
import org.futo.inputmethod.latin.uix.SettingsKey
import org.futo.inputmethod.latin.uix.UixManager
import org.futo.inputmethod.latin.uix.getSettingBlocking
import org.futo.inputmethod.latin.uix.setSettingBlocking
import org.futo.inputmethod.latin.utils.ResourceUtils
import kotlin.math.roundToInt
interface KeyboardSizeStateProvider {
val currentSizeState: KeyboardSizeSettingKind
}
sealed class ComputedKeyboardSize()
class RegularKeyboardSize(val height: Int, val width: Int, val padding: Rect) : ComputedKeyboardSize()
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
class SplitKeyboardSize(val height: Int, val width: Int, val padding: Rect, val splitLayoutWidth: Int) : ComputedKeyboardSize()
enum class OneHandedDirection {
Left,
Right
}
class OneHandedKeyboardSize(val height: Int, val width: Int, val padding: Rect, val layoutWidth: Int, val direction: OneHandedDirection): ComputedKeyboardSize()
class FloatingKeyboardSize(
val bottomOrigin: Pair<Int, Int>,
val width: Int,
val height: Int,
val decorationPadding: Rect
): ComputedKeyboardSize()
fun ComputedKeyboardSize.getHeight(): Int = when(this) {
is FloatingKeyboardSize -> height
is OneHandedKeyboardSize -> height
is RegularKeyboardSize -> height
is SplitKeyboardSize -> height
}
fun ComputedKeyboardSize.getWidth(): Int = when(this) {
is FloatingKeyboardSize -> width
is OneHandedKeyboardSize -> width
is RegularKeyboardSize -> width
is SplitKeyboardSize -> width
}
fun ComputedKeyboardSize.getPadding(): Rect = when(this) {
is FloatingKeyboardSize -> decorationPadding
is OneHandedKeyboardSize -> padding
is RegularKeyboardSize -> padding
is SplitKeyboardSize -> padding
}
fun ComputedKeyboardSize.getTotalKeyboardWidth(): Int = when(this) {
is FloatingKeyboardSize -> width - decorationPadding.left - decorationPadding.right
is OneHandedKeyboardSize -> layoutWidth
is RegularKeyboardSize -> width - padding.left - padding.right
is SplitKeyboardSize -> width - padding.left - padding.right
}
enum class KeyboardMode {
Regular,
Split,
OneHanded,
Floating
}
@Serializer(forClass = Rect::class)
object RectSerializer : KSerializer<Rect> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Rect") {
element<Int>("left")
element<Int>("top")
element<Int>("right")
element<Int>("bottom")
}
override fun serialize(encoder: Encoder, value: Rect) {
encoder.encodeStructure(descriptor) {
encodeIntElement(descriptor, 0, value.left)
encodeIntElement(descriptor, 1, value.top)
encodeIntElement(descriptor, 2, value.right)
encodeIntElement(descriptor, 3, value.bottom)
}
}
override fun deserialize(decoder: Decoder): Rect {
return decoder.decodeStructure(descriptor) {
var left = 0
var top = 0
var right = 0
var bottom = 0
while (true) {
when (val index = decodeElementIndex(descriptor)) {
0 -> left = decodeIntElement(descriptor, 0)
1 -> top = decodeIntElement(descriptor, 1)
2 -> right = decodeIntElement(descriptor, 2)
3 -> bottom = decodeIntElement(descriptor, 3)
CompositeDecoder.DECODE_DONE -> break
else -> error("Unexpected index: $index")
}
}
Rect(left, top, right, bottom)
}
}
}
@Serializable
data class SavedKeyboardSizingSettings(
val currentMode: KeyboardMode,
val heightMultiplier: Float,
val paddingDp: @Serializable(RectSerializer::class) Rect,
// Split
val splitWidthFraction: Float,
val splitPaddingDp: @Serializable(RectSerializer::class) Rect,
val splitHeightMultiplier: Float? = null,
/** One handed, values with respect to left handed mode:
* * left = padding
* * right = width + padding
* * bottom = padding for bottom */
val oneHandedRectDp: @Serializable(RectSerializer::class) Rect,
val oneHandedDirection: OneHandedDirection,
val oneHandedHeightMultiplier: Float? = null,
Aleksandras Kostarevas
committed
// bottom left of the floating keyboard, relative to bottom left of screen, .second is Y up
val floatingBottomOriginDp: Pair<Float, Float>,
val floatingWidthDp: Float,
val floatingHeightDp: Float,
) {
fun toJsonString(): String =
Json.encodeToString(this)
companion object {
@JvmStatic
fun fromJsonString(s: String): SavedKeyboardSizingSettings? =
try {
Json.decodeFromString(s)
} catch (e: Exception) {
//e.printStackTrace()
null
}
}
}
enum class KeyboardSizeSettingKind {
Portrait,
Landscape,
FoldableInnerDisplay
}
val DefaultKeyboardSettings = mapOf(
KeyboardSizeSettingKind.Portrait to SavedKeyboardSizingSettings(
currentMode = KeyboardMode.Regular,
heightMultiplier = 1.0f,
paddingDp = Rect(2, 4, 2, 10),
splitPaddingDp = Rect(2, 4, 2, 10),
splitWidthFraction = 4.0f / 5.0f,
oneHandedDirection = OneHandedDirection.Right,
oneHandedRectDp = Rect(4, 4, 364, 30),
Aleksandras Kostarevas
committed
floatingBottomOriginDp = Pair(0.0f, 0.0f),
floatingHeightDp = 240.0f,
floatingWidthDp = 360.0f
),
KeyboardSizeSettingKind.Landscape to SavedKeyboardSizingSettings(
currentMode = KeyboardMode.Split,
heightMultiplier = 0.9f,
paddingDp = Rect(8, 2, 8, 2),
splitPaddingDp = Rect(8, 2, 8, 2),
splitWidthFraction = 3.0f / 5.0f,
oneHandedDirection = OneHandedDirection.Right,
oneHandedRectDp = Rect(4, 4, 364, 30),
Aleksandras Kostarevas
committed
floatingBottomOriginDp = Pair(0.0f, 0.0f),
floatingHeightDp = 240.0f,
floatingWidthDp = 360.0f
),
KeyboardSizeSettingKind.FoldableInnerDisplay to SavedKeyboardSizingSettings(
currentMode = KeyboardMode.Split,
heightMultiplier = 0.67f,
paddingDp = Rect(44, 4, 44, 8),
splitPaddingDp = Rect(44, 4, 44, 8),
splitWidthFraction = 3.0f / 5.0f,
oneHandedDirection = OneHandedDirection.Right,
oneHandedRectDp = Rect(4, 4, 364, 30),
Aleksandras Kostarevas
committed
floatingBottomOriginDp = Pair(0.0f, 0.0f),
floatingHeightDp = 240.0f,
floatingWidthDp = 360.0f
),
val KeyboardSettings = mapOf(
KeyboardSizeSettingKind.Portrait to SettingsKey(
stringPreferencesKey("keyboard_settings_portrait"), ""),
KeyboardSizeSettingKind.Landscape to SettingsKey(
stringPreferencesKey("keyboard_settings_landscape"), ""),
KeyboardSizeSettingKind.FoldableInnerDisplay to SettingsKey(
stringPreferencesKey("keyboard_settings_fold"), ""),
class KeyboardSizingCalculator(val context: Context, val uixManager: UixManager) {
val sizeStateProvider = context as KeyboardSizeStateProvider
val foldStateProvider = context as FoldStateProvider
private fun dp(v: Number): Int =
(v.toFloat() * context.resources.displayMetrics.density).toInt()
private fun dp(v: Rect): Rect =
Rect(dp(v.left), dp(v.top), dp(v.right), dp(v.bottom))
private fun limitFloating(rectPx: Rect): Rect {
val width = rectPx.width()
val height = rectPx.height()
val minWidth = dp(160)
val minHeight = dp(160)
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
if(width < minWidth) {
val delta = minWidth - width
rectPx.left -= delta / 2
rectPx.right += delta / 2
}
if(height < minHeight) {
val delta = minHeight - height
rectPx.top -= delta
rectPx.bottom += delta
}
val maxWidth = context.resources.displayMetrics.widthPixels * 2 / 3
val maxHeight = context.resources.displayMetrics.heightPixels * 2 / 3
if(width > maxWidth) {
val delta = width - maxWidth
rectPx.left += delta / 2
rectPx.right -= delta / 2
}
if(height > maxHeight) {
val delta = height - maxHeight
rectPx.top += delta / 2
rectPx.bottom -= delta / 2
}
val originX = rectPx.left
val originY = rectPx.top
if(originX < 0){
rectPx.left -= originX
rectPx.right -= originX
}
if(originY < 0) {
rectPx.top -= originY
rectPx.bottom -= originY
}
if(rectPx.right > context.resources.displayMetrics.widthPixels) {
val delta = rectPx.right - context.resources.displayMetrics.widthPixels
rectPx.right -= delta
rectPx.left -= delta
}
if(rectPx.bottom < 0) {
val delta = rectPx.bottom
rectPx.top -= delta
rectPx.bottom -= delta
}
return rectPx
}
fun getSavedSettings(): SavedKeyboardSizingSettings =
SavedKeyboardSizingSettings.fromJsonString(context.getSettingBlocking(
KeyboardSettings[sizeStateProvider.currentSizeState]!!
)) ?: DefaultKeyboardSettings[sizeStateProvider.currentSizeState]!!
fun editSavedSettings(transform: (SavedKeyboardSizingSettings) -> SavedKeyboardSizingSettings) {
val sizeState = sizeStateProvider.currentSizeState
val savedSettings = SavedKeyboardSizingSettings.fromJsonString(context.getSettingBlocking(
KeyboardSettings[sizeState]!!
)) ?: DefaultKeyboardSettings[sizeState]!!
val transformed = transform(savedSettings)
if(transformed != savedSettings) {
context.setSettingBlocking(KeyboardSettings[sizeState]!!.key, transformed.toJsonString())
}
}
fun calculate(layoutName: String, isNumberRowActive: Boolean): ComputedKeyboardSize {
val savedSettings = getSavedSettings()
val layout = LayoutManager.getLayout(context, layoutName)
val effectiveRowCount = layout.effectiveRows.size
val displayMetrics = context.resources.displayMetrics
val singularRowHeight = (ResourceUtils.getDefaultKeyboardHeight(context.resources) / 4.0) *
(when(savedSettings.currentMode) {
KeyboardMode.Regular -> null
KeyboardMode.Split -> savedSettings.splitHeightMultiplier
KeyboardMode.OneHanded -> savedSettings.oneHandedHeightMultiplier
KeyboardMode.Floating -> null
} ?: savedSettings.heightMultiplier)
val numRows = 4.0 +
((effectiveRowCount - 5) / 2.0).coerceAtLeast(0.0) +
if(isNumberRowActive) { 0.5 } else { 0.0 }
val recommendedHeight = numRows * singularRowHeight
val foldState = foldStateProvider.foldState.feature
val window = (context as LatinIME).window.window
val width = ResourceUtils.getDefaultKeyboardWidth(window, context.resources)
return when {
// Special case: 50% screen height no matter the row count or settings
foldState != null && foldState.state == FoldingFeature.State.HALF_OPENED && foldState.orientation == FoldingFeature.Orientation.HORIZONTAL ->
SplitKeyboardSize(
height = displayMetrics.heightPixels / 2 - (displayMetrics.density * 80.0f).toInt(),
width = width,
padding = Rect(
(displayMetrics.density * 44.0f).roundToInt(),
(displayMetrics.density * 50.0f).roundToInt(),
(displayMetrics.density * 44.0f).roundToInt(),
(displayMetrics.density * 12.0f).roundToInt(),
),
splitLayoutWidth = displayMetrics.widthPixels * 3 / 5
)
savedSettings.currentMode == KeyboardMode.Split ->
SplitKeyboardSize(
height = recommendedHeight.roundToInt(),
width = width,
padding = dp(savedSettings.splitPaddingDp),
splitLayoutWidth = (displayMetrics.widthPixels * savedSettings.splitWidthFraction).toInt()
)
savedSettings.currentMode == KeyboardMode.OneHanded ->
OneHandedKeyboardSize(
height = recommendedHeight.roundToInt(),
width = width,
padding = dp(savedSettings.oneHandedRectDp).let { rect ->
when(savedSettings.oneHandedDirection) {
OneHandedDirection.Left -> Rect(rect.left, rect.top, rect.left, rect.bottom)
OneHandedDirection.Right -> Rect(rect.left, rect.top, rect.left, rect.bottom)
}
},
layoutWidth = dp(savedSettings.oneHandedRectDp.width()).coerceAtMost(displayMetrics.widthPixels * 9 / 10),
direction = savedSettings.oneHandedDirection
savedSettings.currentMode == KeyboardMode.Floating -> {
val singularRowHeightFloat = dp(savedSettings.floatingHeightDp) / 4.0f
val recommendedHeightFloat = singularRowHeightFloat * numRows
FloatingKeyboardSize(
bottomOrigin = Pair(
Aleksandras Kostarevas
committed
dp(savedSettings.floatingBottomOriginDp.first),
dp(savedSettings.floatingBottomOriginDp.second)
),
width = dp(savedSettings.floatingWidthDp),
height = recommendedHeightFloat.toInt(),
decorationPadding = dp(
Rect(
8,
8,
8,
8
)
)
)
}
else ->
RegularKeyboardSize(
height = recommendedHeight.roundToInt(),
width = width,
padding = dp(savedSettings.paddingDp)
)
}
}
fun calculateGap(): Float {
val displayMetrics = context.resources.displayMetrics
val widthDp = displayMetrics.widthPixels / displayMetrics.density
val heightDp = displayMetrics.heightPixels / displayMetrics.density
val minDp = Math.min(widthDp, heightDp)
return (minDp / 100.0f).coerceIn(3.0f, 6.0f)
}
fun calculateSuggestionBarHeightDp(): Float {
return 40.0f
}
fun calculateTotalActionBarHeightPx(): Int =
when {
uixManager.actionsExpanded && (uixManager.currWindowActionWindow == null) -> dp(2 * calculateSuggestionBarHeightDp())
else -> dp(calculateSuggestionBarHeightDp())
}