Something went wrong on our end
-
Aleksandras Kostarevas authoredAleksandras Kostarevas authored
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))
}