Skip to content
Snippets Groups Projects
ActionBar.kt 35.08 KiB
package org.futo.inputmethod.latin.uix

import android.content.Context
import android.os.Build
import android.view.View
import androidx.annotation.RequiresApi
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
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.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.TextButton
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.Center
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.drawText
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
import org.futo.inputmethod.latin.R
import org.futo.inputmethod.latin.SuggestedWords
import org.futo.inputmethod.latin.SuggestedWords.SuggestedWordInfo
import org.futo.inputmethod.latin.SuggestedWords.SuggestedWordInfo.KIND_EMOJI_SUGGESTION
import org.futo.inputmethod.latin.SuggestedWords.SuggestedWordInfo.KIND_TYPED
import org.futo.inputmethod.latin.SuggestionBlacklist
import org.futo.inputmethod.latin.common.Constants
import org.futo.inputmethod.latin.suggestions.SuggestionStripViewListener
import org.futo.inputmethod.latin.uix.actions.FavoriteActions
import org.futo.inputmethod.latin.uix.actions.MoreActionsAction
import org.futo.inputmethod.latin.uix.actions.PinnedActions
import org.futo.inputmethod.latin.uix.actions.toActionList
import org.futo.inputmethod.latin.uix.settings.useDataStore
import org.futo.inputmethod.latin.uix.settings.useDataStoreValue
import org.futo.inputmethod.latin.uix.theme.DarkColorScheme
import org.futo.inputmethod.latin.uix.theme.Typography
import org.futo.inputmethod.latin.uix.theme.UixThemeWrapper
import kotlin.math.ceil
import kotlin.math.roundToInt

/*
 * The UIX Action Bar is intended to replace the previous top bar of the AOSP keyboard.
 * Its goal is to function similar to the old top bar by showing predictions, but also modernize
 * it with actions and new features.
 *
 * Example bar:
 * [>] word1 | word2 | word3 [mic]
 *
 * The [>] button expands the action bar, replacing word predictions with actions the user can take.
 * Actions have little icons which perform an action. Some examples:
 * - Microphone: opens the voice input menu
 * - Undo/Redo
 * - Text editing: switches to the text editing menu
 * - Settings: opens the keyboard settings menu
 * - Report problem: opens the report menu
 *
 * Generally there are a few kinds of actions:
 * - Take an action on the text being typed (undo/redo)
 * - Switch from the keyboard UI to something else (voice input, text editing)
 * - Open an app (settings, report)
 *
 * The UIX effort is to modernize the AOSP Keyboard by replacing and extending
 * parts of it with UI written in Android Compose, while keeping most of the
 * battle-tested original keyboard code the same
 *
 * TODO: Will need to make RTL languages work
 */

val ActionBarHeight = 40.dp

val ActionBarScrollIndexSetting = SettingsKey(
    intPreferencesKey("action_bar_scroll_index"),
    0
)
val ActionBarScrollOffsetSetting = SettingsKey(
    intPreferencesKey("action_bar_scroll_offset"),
    0
)

val ActionBarExpanded = SettingsKey(
    booleanPreferencesKey("actionExpanded"),
    false
)

val OldStyleActionsBar = SettingsKey(
    booleanPreferencesKey("oldActionBar"),
    false
)


interface ImportantNotice {
    @Composable fun getText(): String
    fun onDismiss(context: Context)
    fun onOpen(context: Context)
}


val suggestionStylePrimary = TextStyle(
    fontFamily = FontFamily.SansSerif,
    fontWeight = FontWeight.Medium,
    fontSize = 18.sp,
    lineHeight = 26.sp,
    letterSpacing = 0.5.sp,
    //textAlign = TextAlign.Center
)

val suggestionStyleAlternative = TextStyle(
    fontFamily = FontFamily.SansSerif,
    fontWeight = FontWeight.Normal,
    fontSize = 18.sp,
    lineHeight = 26.sp,
    letterSpacing = 0.5.sp,
    //textAlign = TextAlign.Center
)


// Automatically try to fit the given text to the available space in one line.
// If text is too long, the text gets scaled horizontally to fit.
// TODO: Could also put ellipsis in the middle
@OptIn(ExperimentalTextApi::class)
@Composable
fun AutoFitText(
    text: String,
    modifier: Modifier = Modifier,
    style: TextStyle = TextStyle.Default,
    layoutDirection: LayoutDirection = LayoutDirection.Ltr
) {
    val measurer = rememberTextMeasurer()

    Canvas(modifier = modifier.fillMaxSize(), contentDescription = text) {
        val measurement = measurer.measure(
            text = AnnotatedString(text),
            style = style,
            overflow = TextOverflow.Visible,
            softWrap = false,
            maxLines = 1,
            constraints = Constraints(
                maxWidth = Int.MAX_VALUE,
                maxHeight = ceil(this.size.height).roundToInt()
            ),
            layoutDirection = layoutDirection,
            density = this
        )
        val scale = (size.width / measurement.size.width).coerceAtMost(1.0f)

        translate(left = (scale * (size.width - measurement.size.width)) / 2.0f, top = size.height / 2 - measurement.size.height / 2) {
            scale(scaleX = scale, scaleY = 1.0f) {
                drawText(
                    measurement
                )
            }
        }
    }
}

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun RowScope.SuggestionItem(words: SuggestedWords, idx: Int, isPrimary: Boolean, onClick: () -> Unit, onLongClick: () -> Unit) {
    val wordInfo = words.getInfoOrNull(idx)
    val isVerbatim = wordInfo?.kind == KIND_TYPED
    val word = wordInfo?.mWord

    val actualIsPrimary = isPrimary && (words.mWillAutoCorrect || ((wordInfo?.isExactMatch) == true))

    val iconColor = MaterialTheme.colorScheme.onBackground
    val topSuggestionIcon = painterResource(id = R.drawable.transformer_suggestion)
    val textButtonModifier = when (wordInfo?.mOriginatesFromTransformerLM) {
        true -> Modifier.drawBehind {
            with(topSuggestionIcon) {
                val iconSize = topSuggestionIcon.intrinsicSize
                translate(
                    left = (size.width - iconSize.width) / 2.0f,
                    top = size.height - iconSize.height * 2.0f
                ) {
                    draw(
                        topSuggestionIcon.intrinsicSize,
                        alpha = if(actualIsPrimary){ 1.0f } else { 0.66f } / 1.25f,
                        colorFilter = ColorFilter.tint(color = iconColor)
                    )
                }
            }
        }
        else -> Modifier
    }

    val textModifier = when (actualIsPrimary) {
        true -> Modifier
        false -> Modifier.alpha(0.75f)
    }

    val textStyle = when (actualIsPrimary) {
        true -> suggestionStylePrimary
        false -> suggestionStyleAlternative
    }.copy(color = MaterialTheme.colorScheme.onBackground)

    Box(
        modifier = textButtonModifier
            .weight(1.0f)
            .fillMaxHeight()
            .combinedClickable(
                enabled = word != null,
                onClick = onClick,
                onLongClick = onLongClick
            ),
    ) {
        CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onBackground) {
            if (word != null) {
                val modifier = textModifier.align(Center).padding(2.dp)
                if(isVerbatim) {
                    AutoFitText('"' + word + '"', style = textStyle.copy(fontStyle = FontStyle.Italic), modifier = modifier)
                } else {
                    AutoFitText(word, style = textStyle, modifier = modifier)
                }
            }
        }
    }
}

@Composable fun RowScope.SuggestionSeparator() {
    Box(
        modifier = Modifier
            .fillMaxHeight(0.66f)
            .align(CenterVertically)
            .background(color = MaterialTheme.colorScheme.outline)
            .width(1.dp)
    )
}


data class SuggestionLayout(
    /** Set to the word to be autocorrected to */
    val autocorrectMatch: SuggestedWordInfo?,

    /** Other words, sorted by likelihood */
    val sortedMatches: List<SuggestedWordInfo>,

    /** Emoji suggestions if they are to be shown */
    val emojiMatches: List<SuggestedWordInfo>,

    /** The exact word the user typed */
    val verbatimWord: SuggestedWordInfo?,

    /** Set to true if the best match is so unlikely that we should show verbatim instead */
    val areSuggestionsClueless: Boolean,

    /** Set to true if this is a gesture update, and we should only show one suggestion */
    val isGestureBatch: Boolean,

    val presentableSuggestions: List<SuggestedWordInfo>
)

fun SuggestedWords.getInfoOrNull(idx: Int): SuggestedWordInfo? = try {
    getInfo(idx)
} catch(e: IndexOutOfBoundsException) {
    null
}

fun makeSuggestionLayout(words: SuggestedWords, blacklist: SuggestionBlacklist): SuggestionLayout {
    val typedWord = words.getInfoOrNull(SuggestedWords.INDEX_OF_TYPED_WORD)?.let {
        if(it.kind == KIND_TYPED) { it } else { null }
    }?.let {
        if(blacklist.isSuggestedWordOk(it)) {
            it
        } else {
            null
        }
    }

    val autocorrectMatch = words.getInfoOrNull(SuggestedWords.INDEX_OF_AUTO_CORRECTION)?.let {
        if(words.mWillAutoCorrect) { it } else { null }
    }

    // We actually have to avoid sorting these because they are provided sorted in an important order

    val emojiMatches = words.mSuggestedWordInfoList.filter {
        it.kind == KIND_EMOJI_SUGGESTION
    }

    val sortedMatches = words.mSuggestedWordInfoList.filter {
        it != typedWord && it.kind != KIND_TYPED && it != autocorrectMatch && !emojiMatches.contains(it)
    }

    val areSuggestionsClueless = (autocorrectMatch ?: sortedMatches.getOrNull(0))?.let {
        it.mOriginatesFromTransformerLM && it.mScore < -50
    } ?: false

    val isGestureBatch = words.mInputStyle == SuggestedWords.INPUT_STYLE_UPDATE_BATCH

    val presentableSuggestions = (
            listOf(
                typedWord,
                autocorrectMatch,
            ) + sortedMatches
    ).filterNotNull()

    return SuggestionLayout(
        autocorrectMatch = autocorrectMatch,
        sortedMatches = sortedMatches,
        emojiMatches = emojiMatches,
        verbatimWord = typedWord,
        areSuggestionsClueless = areSuggestionsClueless,
        isGestureBatch = isGestureBatch,
        presentableSuggestions = presentableSuggestions
    )
}

@Composable
fun RowScope.SuggestionItems(words: SuggestedWords, onClick: (i: Int) -> Unit, onLongClick: (i: Int) -> Unit) {
    val layout = makeSuggestionLayout(words, LocalManager.current.getSuggestionBlacklist())

    val suggestionItem = @Composable { suggestion: SuggestedWordInfo? ->
        if(suggestion != null) {
            val idx = words.indexOf(suggestion)
            SuggestionItem(
                words,
                idx,
                isPrimary = idx == SuggestedWords.INDEX_OF_AUTO_CORRECTION,
                onClick = { onClick(idx) },
                onLongClick = { onLongClick(idx) }
            )
        } else {
            Spacer(Modifier.weight(1.0f))
        }
    }

    println(layout)
    when {
        layout.isGestureBatch ||
        layout.presentableSuggestions.size <= 1 -> suggestionItem(layout.presentableSuggestions.firstOrNull())

        layout.autocorrectMatch != null -> {
            var supplementalSuggestionIndex = 0
            if(layout.emojiMatches.isEmpty()) {
                suggestionItem(layout.sortedMatches.getOrNull(supplementalSuggestionIndex++))
            } else {
                suggestionItem(layout.emojiMatches[0])
            }
            SuggestionSeparator()
            suggestionItem(layout.autocorrectMatch)
            SuggestionSeparator()

            if(layout.verbatimWord != null && layout.verbatimWord.mWord != layout.autocorrectMatch.mWord) {
                suggestionItem(layout.verbatimWord)
            } else {
                suggestionItem(layout.sortedMatches.getOrNull(supplementalSuggestionIndex))
            }
        }

        else -> {
            var supplementalSuggestionIndex = 1
            if(layout.emojiMatches.isEmpty()) {
                suggestionItem(layout.sortedMatches.getOrNull(supplementalSuggestionIndex++))
            } else {
                suggestionItem(layout.emojiMatches[0])
            }
            SuggestionSeparator()
            suggestionItem(layout.sortedMatches.getOrNull(0))
            SuggestionSeparator()
            suggestionItem(layout.sortedMatches.getOrNull(supplementalSuggestionIndex))
        }
    }
}

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LazyItemScope.ActionItem(idx: Int, action: Action, onSelect: (Action) -> Unit, onLongSelect: (Action) -> Unit) {
    val width = 56.dp

    val modifier = Modifier
        .width(width)
        .fillMaxHeight()

    val contentCol = MaterialTheme.colorScheme.onBackground

    Box(modifier = modifier
        .clip(CircleShape)
        .combinedClickable(onLongClick = action.altPressImpl?.let { { onLongSelect(action) } },
            onClick = { onSelect(action) }), contentAlignment = Center) {
        Icon(
            painter = painterResource(id = action.icon),
            contentDescription = stringResource(action.name),
            tint = contentCol,
            modifier = Modifier.size(20.dp),
        )
    }
}

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ActionItemSmall(action: Action, onSelect: (Action) -> Unit, onLongSelect: (Action) -> Unit) {
    val bgCol = LocalKeyboardScheme.current.backgroundContainer
    val fgCol = LocalKeyboardScheme.current.onBackgroundContainer

    val circleRadius = with(LocalDensity.current) {
        16.dp.toPx()
    }

    Box(modifier = Modifier
        .width(42.dp)
        .fillMaxHeight()
        .drawBehind {
            drawCircle(
                color = bgCol,
                radius = circleRadius,
                style = Fill
            )
        }
        .clip(CircleShape)
        .combinedClickable(onLongClick = action.altPressImpl?.let { { onLongSelect(action) } }) {
            onSelect(
                action
            )
        },
        contentAlignment = Center
    ) {
        Icon(
            painter = painterResource(id = action.icon),
            contentDescription = stringResource(action.name),
            tint = fgCol,
            modifier = Modifier.size(16.dp)
        )
    }
}


@Composable
fun ActionItems(onSelect: (Action) -> Unit, onLongSelect: (Action) -> Unit) {
    val context = LocalContext.current
    val lifecycle = LocalLifecycleOwner.current
    val actions = if(!LocalInspectionMode.current) {
        useDataStoreValue(FavoriteActions)
    } else {
        FavoriteActions.default
    }

    val scrollItemIndex = if(LocalInspectionMode.current) { 0 } else {
        remember {
            context.getSettingBlocking(ActionBarScrollIndexSetting)
        }
    }

    val scrollItemOffset = if(LocalInspectionMode.current) { 0 } else {
        remember {
            context.getSettingBlocking(ActionBarScrollOffsetSetting)
        }
    }

    val actionItems = remember(actions) {
        actions.toActionList()
    }

    val lazyListState = rememberLazyListState(scrollItemIndex, scrollItemOffset)

    DisposableEffect(Unit) {
        onDispose {
            lifecycle.deferSetSetting(ActionBarScrollIndexSetting, lazyListState.firstVisibleItemIndex)
            lifecycle.deferSetSetting(ActionBarScrollOffsetSetting, lazyListState.firstVisibleItemScrollOffset)
        }
    }

    val bgCol = LocalKeyboardScheme.current.backgroundContainer

    val gradientColor = if(bgCol.alpha > 0.5) {
        bgCol.copy(alpha = 0.9f)
    } else {
        MaterialTheme.colorScheme.surface.copy(alpha = 0.9f)
    }

    val drawLeftGradient = lazyListState.firstVisibleItemIndex > 0
    val drawRightGradient = lazyListState.layoutInfo.visibleItemsInfo.isNotEmpty() && actionItems.isNotEmpty() && (lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()?.key != actionItems.lastOrNull()?.name)

    Box {
        LazyRow(state = lazyListState) {
            item {
                ActionItemSmall(action = MoreActionsAction, onSelect = {
                    onSelect(MoreActionsAction)
                }, onLongSelect = { })

            }
            items(actionItems.size, key = { actionItems[it].name }) {
                ActionItem(it, actionItems[it], onSelect, onLongSelect)
            }
        }


        if(drawLeftGradient) {
            Canvas(modifier = Modifier
                .fillMaxHeight()
                .width(72.dp)
                .align(Alignment.CenterStart)) {
                drawRect(
                    Brush.linearGradient(
                        0.0f to gradientColor,
                        1.0f to Color.Transparent,
                        start = Offset.Zero,
                        end = Offset(Float.POSITIVE_INFINITY, 0.0f)
                    )
                )
            }
        }

        if(drawRightGradient) {
            Canvas(modifier = Modifier
                .fillMaxHeight()
                .width(72.dp)
                .align(Alignment.CenterEnd)) {
                drawRect(
                    Brush.linearGradient(
                        0.0f to Color.Transparent,
                        1.0f to gradientColor,
                        start = Offset.Zero,
                        end = Offset(Float.POSITIVE_INFINITY, 0.0f)
                    )
                )
            }
        }
    }
}


@Composable
fun ExpandActionsButton(isActionsOpen: Boolean, onClick: () -> Unit) {
    val bgCol = LocalKeyboardScheme.current.backgroundContainer
    val fgCol = LocalKeyboardScheme.current.onBackgroundContainer

    val circleRadius = with(LocalDensity.current) {
        16.dp.toPx()
    }

    IconButton(
        onClick = onClick,
        modifier = Modifier
            .width(42.dp)
            .rotate(
                if (isActionsOpen) {
                    -90.0f
                } else {
                    0.0f
                }
            )
            .fillMaxHeight()
            .drawBehind {
                drawCircle(
                    color = bgCol,
                    radius = circleRadius,
                    style = Fill
                )
            },

        colors = IconButtonDefaults.iconButtonColors(contentColor = fgCol)
    ) {
        Icon(
            painter = painterResource(id = R.drawable.chevron_right),
            contentDescription = "Open Actions",
            Modifier.size(20.dp)
        )
    }
}

@Composable
fun ImportantNoticeView(
    importantNotice: ImportantNotice
) {
    val context = LocalContext.current

    Row {
        TextButton(
            onClick = { importantNotice.onOpen(context) },
            modifier = Modifier
                .weight(1.0f)
                .fillMaxHeight(),
            shape = RectangleShape,
            colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.onBackground),
            enabled = true
        ) {
            AutoFitText(importantNotice.getText(), style = suggestionStylePrimary.copy(color = MaterialTheme.colorScheme.onBackground))
        }

        val color = MaterialTheme.colorScheme.primary
        IconButton(
            onClick = { importantNotice.onDismiss(context) },
            modifier = Modifier
                .width(42.dp)
                .fillMaxHeight()
                .drawBehind {
                    drawCircle(color = color, radius = size.width / 3.0f + 1.0f)
                },

            colors = IconButtonDefaults.iconButtonColors(contentColor = MaterialTheme.colorScheme.onPrimary)
        ) {
            Icon(
                painter = painterResource(id = R.drawable.close),
                contentDescription = "Close"
            )
        }
    }
}

@Composable
fun RowScope.PinnedActionItems(onSelect: (Action) -> Unit, onLongSelect: (Action) -> Unit) {
    val actions = if(!LocalInspectionMode.current) {
        useDataStoreValue(PinnedActions)
    } else {
        PinnedActions.default
    }

    val actionItems = remember(actions) {
        actions.toActionList()
    }

    actionItems.forEach {
        ActionItemSmall(it, onSelect, onLongSelect)
    }
}

@Composable
fun ActionSep() {
    val sepCol = LocalKeyboardScheme.current.backgroundContainer

    Box(modifier = Modifier
        .fillMaxWidth()
        .height(1.dp)
        .background(sepCol)) {}
}

@Composable
fun ActionBar(
    words: SuggestedWords?,
    suggestionStripListener: SuggestionStripViewListener,
    onActionActivated: (Action) -> Unit,
    onActionAltActivated: (Action) -> Unit,
    inlineSuggestions: List<MutableState<View?>>,
    isActionsExpanded: Boolean,
    toggleActionsExpanded: () -> Unit,
    importantNotice: ImportantNotice? = null,
    keyboardManagerForAction: KeyboardManagerForAction? = null,
) {
    val view = LocalView.current
    val context = LocalContext.current

    val oldActionBar = useDataStore(OldStyleActionsBar)

    Column(Modifier.height(ActionBarHeight * if(isActionsExpanded) 2 else 1)) {
        if(isActionsExpanded && !oldActionBar.value) {
            ActionSep()

            Surface(
                modifier = Modifier
                    .fillMaxWidth()
                    .weight(1.0f), color = MaterialTheme.colorScheme.background
            ) {
                ActionItems(onActionActivated, onActionAltActivated)
            }
        }

        ActionSep()

        Surface(
            modifier = Modifier
                .fillMaxWidth()
                .weight(1.0f), color = MaterialTheme.colorScheme.background
        ) {
            Row {
                ExpandActionsButton(isActionsExpanded) {
                    toggleActionsExpanded()

                    keyboardManagerForAction?.performHapticAndAudioFeedback(
                        Constants.CODE_TAB,
                        view
                    )
                }

                if(oldActionBar.value && isActionsExpanded) {
                    Box(modifier = Modifier
                        .weight(1.0f)
                        .fillMaxHeight()) {
                        ActionItems(onActionActivated, onActionAltActivated)
                    }
                } else {
                    if (importantNotice != null) {
                        ImportantNoticeView(importantNotice)
                    } else {
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
                            AnimatedVisibility(
                                inlineSuggestions.isNotEmpty(),
                                enter = fadeIn(),
                                exit = fadeOut()
                            ) {
                                InlineSuggestions(inlineSuggestions)
                            }
                        }

                        if (words != null && inlineSuggestions.isEmpty()) {
                            SuggestionItems(
                                words,
                                onClick = {
                                    suggestionStripListener.pickSuggestionManually(
                                        words.getInfo(it)
                                    )
                                    keyboardManagerForAction?.performHapticAndAudioFeedback(
                                        Constants.CODE_TAB,
                                        view
                                    )
                                },
                                onLongClick = {
                                    suggestionStripListener.requestForgetWord(
                                        words.getInfo(it)
                                    )
                                })
                        } else {
                            Spacer(modifier = Modifier.weight(1.0f))
                        }

                        PinnedActionItems(onActionActivated, onActionAltActivated)
                    }
                }
            }
        }
    }
}

@Composable
fun ActionWindowBar(
    windowTitleBar: @Composable RowScope.() -> Unit,
    canExpand: Boolean,
    onBack: () -> Unit,
    onExpand: () -> Unit
) {
    Column(Modifier.height(ActionBarHeight)) {
        ActionSep()
        Surface(
            modifier = Modifier
                .fillMaxWidth()
                .weight(1.0f), color = MaterialTheme.colorScheme.background
        )
        {
            Row {
                IconButton(onClick = onBack) {
                    Icon(
                        painter = painterResource(id = R.drawable.arrow_left_26),
                        contentDescription = "Back"
                    )
                }

                CompositionLocalProvider(LocalTextStyle provides Typography.titleMedium) {
                    windowTitleBar()
                }

                if (canExpand) {
                    IconButton(onClick = onExpand) {
                        Icon(
                            painter = painterResource(id = R.drawable.arrow_up),
                            contentDescription = "Show Keyboard"
                        )
                    }
                }
            }
        }
    }
}

@Composable
fun CollapsibleSuggestionsBar(
    onClose: () -> Unit,
    onCollapse: () -> Unit,
    words: SuggestedWords?,
    suggestionStripListener: SuggestionStripViewListener,
) {
    Column(Modifier.height(ActionBarHeight)) {
        ActionSep()
        Surface(
            modifier = Modifier
                .fillMaxWidth()
                .weight(1.0f), color = MaterialTheme.colorScheme.background
        )
        {
            Row {
                val color = MaterialTheme.colorScheme.primary

                IconButton(
                    onClick = onClose,
                    modifier = Modifier
                        .width(42.dp)
                        .fillMaxHeight()
                        .drawBehind {
                            drawCircle(color = color, radius = size.width / 3.0f + 1.0f)
                        },

                    colors = IconButtonDefaults.iconButtonColors(contentColor = MaterialTheme.colorScheme.onPrimary)
                ) {
                    Icon(
                        painter = painterResource(id = R.drawable.close),
                        contentDescription = "Close"
                    )
                }

                if (words != null) {
                    SuggestionItems(
                        words,
                        onClick = {
                            suggestionStripListener.pickSuggestionManually(
                                words.getInfo(it)
                            )
                        },
                        onLongClick = { suggestionStripListener.requestForgetWord(words.getInfo(it)) })
                } else {
                    Spacer(modifier = Modifier.weight(1.0f))
                }

                IconButton(
                    onClick = onCollapse,
                    modifier = Modifier
                        .width(42.dp)
                        .fillMaxHeight(),
                    colors = IconButtonDefaults.iconButtonColors(contentColor = MaterialTheme.colorScheme.onBackground)
                ) {
                    Icon(
                        painter = painterResource(id = R.drawable.arrow_down),
                        contentDescription = "Collapse"
                    )
                }
            }
        }
    }
}




/* ---- Previews ---- */

class ExampleListener : SuggestionStripViewListener {
    override fun showImportantNoticeContents() {
    }

    override fun pickSuggestionManually(word: SuggestedWordInfo?) {
    }

    override fun requestForgetWord(word: SuggestedWordInfo?) {
    }

    override fun onCodeInput(primaryCode: Int, x: Int, y: Int, isKeyRepeat: Boolean) {
    }
}

val exampleSuggestionsList = arrayListOf(
    SuggestedWordInfo("verylongword123", "", 100, 1, null, 0, 0),
    SuggestedWordInfo("world understanding of patience", "",  99, 1, null, 0, 0),
    SuggestedWordInfo("short", "", 98, 1, null, 0, 0),
    SuggestedWordInfo("extra1", "", 97, 1, null, 0, 0),
    SuggestedWordInfo("extra2", "", 96, 1, null, 0, 0),
    SuggestedWordInfo("extra3", "", 95, 1, null, 0, 0)
)

val exampleSuggestedWords = SuggestedWords(
    exampleSuggestionsList,
    exampleSuggestionsList,
    exampleSuggestionsList[0],
    true,
    true,
    false,
    0,
    0
)

val exampleSuggestedWordsEmpty = SuggestedWords(
    arrayListOf(),
    arrayListOf(),
    exampleSuggestionsList[0],
    true,
    true,
    false,
    0,
    0
)

@Composable
@Preview
fun PreviewActionBarWithSuggestions(colorScheme: ColorScheme = DarkColorScheme) {
    UixThemeWrapper(wrapColorScheme(colorScheme)) {
        ActionBar(
            words = exampleSuggestedWords,
            suggestionStripListener = ExampleListener(),
            onActionActivated = { },
            inlineSuggestions = listOf(),
            isActionsExpanded = false,
            toggleActionsExpanded = { },
            onActionAltActivated = { }
        )
    }
}

@Composable
@Preview
fun PreviewActionBarWithNotice(colorScheme: ColorScheme = DarkColorScheme) {
    UixThemeWrapper(wrapColorScheme(colorScheme)) {
        ActionBar(
            words = exampleSuggestedWords,
            suggestionStripListener = ExampleListener(),
            onActionActivated = { },
            inlineSuggestions = listOf(),
            isActionsExpanded = true,
            toggleActionsExpanded = { },
            onActionAltActivated = { },
            importantNotice = object : ImportantNotice {
                @Composable
                override fun getText(): String {
                    return "Update available: v1.2.3"
                }

                override fun onDismiss(context: Context) {
                    TODO("Not yet implemented")
                }

                override fun onOpen(context: Context) {
                    TODO("Not yet implemented")
                }

            }
        )
    }
}

@Composable
@Preview
fun PreviewActionBarWithEmptySuggestions(colorScheme: ColorScheme = DarkColorScheme) {
    UixThemeWrapper(wrapColorScheme(colorScheme)) {
        ActionBar(
            words = exampleSuggestedWordsEmpty,
            suggestionStripListener = ExampleListener(),
            onActionActivated = { },
            inlineSuggestions = listOf(),
            isActionsExpanded = true,
            toggleActionsExpanded = { },
            onActionAltActivated = { }
        )
    }
}

@Composable
@Preview
fun PreviewExpandedActionBar(colorScheme: ColorScheme = DarkColorScheme) {
    UixThemeWrapper(wrapColorScheme(colorScheme)) {
        ActionBar(
            words = exampleSuggestedWordsEmpty,
            suggestionStripListener = ExampleListener(),
            onActionActivated = { },
            inlineSuggestions = listOf(),
            isActionsExpanded = true,
            toggleActionsExpanded = { },
            onActionAltActivated = { }
        )
    }
}

@Composable
@Preview
fun PreviewCollapsibleBar(colorScheme: ColorScheme = DarkColorScheme) {
    CollapsibleSuggestionsBar(
        onCollapse = { },
        onClose = { },
        words = exampleSuggestedWords,
        suggestionStripListener = ExampleListener()
    )
}


@RequiresApi(Build.VERSION_CODES.S)
@Composable
@Preview
fun PreviewActionBarWithSuggestionsDynamicLight() {
    PreviewActionBarWithSuggestions(dynamicLightColorScheme(LocalContext.current))
}

@RequiresApi(Build.VERSION_CODES.S)
@Composable
@Preview
fun PreviewActionBarWithEmptySuggestionsDynamicLight() {
    PreviewActionBarWithEmptySuggestions(dynamicLightColorScheme(LocalContext.current))
}

@RequiresApi(Build.VERSION_CODES.S)
@Composable
@Preview
fun PreviewExpandedActionBarDynamicLight() {
    PreviewExpandedActionBar(dynamicLightColorScheme(LocalContext.current))
}

@RequiresApi(Build.VERSION_CODES.S)
@Composable
@Preview
fun PreviewActionBarWithSuggestionsDynamicDark() {
    PreviewActionBarWithSuggestions(dynamicDarkColorScheme(LocalContext.current))
}

@RequiresApi(Build.VERSION_CODES.S)
@Composable
@Preview
fun PreviewActionBarWithEmptySuggestionsDynamicDark() {
    PreviewActionBarWithEmptySuggestions(dynamicDarkColorScheme(LocalContext.current))
}

@RequiresApi(Build.VERSION_CODES.S)
@Composable
@Preview
fun PreviewExpandedActionBarDynamicDark() {
    PreviewExpandedActionBar(dynamicDarkColorScheme(LocalContext.current))
}