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)
}
}
}
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
@Serializable
data class SavedKeyboardSizingSettings(
val currentMode: KeyboardMode,
val heightMultiplier: Float,
val paddingDp: @Serializable(RectSerializer::class) Rect,
// Split
val splitWidthFraction: Float,
// 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,
// Floating
val floatingBottomCenterOriginDp: Pair<Float, Float>, // relative to bottom left of screen, .second is Y up
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
}
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
val DefaultKeyboardSettings = mapOf(
KeyboardSizeSettingKind.Portrait to SavedKeyboardSizingSettings(
currentMode = KeyboardMode.Regular,
heightMultiplier = 1.0f,
paddingDp = Rect(2, 4, 2, 10),
splitWidthFraction = 4.0f / 5.0f,
oneHandedDirection = OneHandedDirection.Right,
oneHandedRectDp = Rect(4, 4, 364, 30),
floatingBottomCenterOriginDp = 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),
splitWidthFraction = 3.0f / 5.0f,
oneHandedDirection = OneHandedDirection.Right,
oneHandedRectDp = Rect(4, 4, 364, 30),
floatingBottomCenterOriginDp = 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),
splitWidthFraction = 3.0f / 5.0f,
oneHandedDirection = OneHandedDirection.Right,
oneHandedRectDp = Rect(4, 4, 364, 30),
floatingBottomCenterOriginDp = 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)
239
240
241
242
243
244
245
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
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) *
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.paddingDp),
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(
dp(savedSettings.floatingBottomCenterOriginDp.first),
dp(savedSettings.floatingBottomCenterOriginDp.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 -> dp(2 * calculateSuggestionBarHeightDp())
else -> dp(calculateSuggestionBarHeightDp())
}