Skip to content
Snippets Groups Projects
KeyboardSizingCalculator.kt 15.4 KiB
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()
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,
    // bottom left of the floating keyboard, relative to bottom left of screen, .second is Y up
    val floatingBottomOriginDp: Pair<Float, Float>,
    val floatingWidthDp: 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),
        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),
        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),
        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)
        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(
                        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())
        }