Replace SuggestionStripView with ActionBar

This commit is contained in:
abb128 2023-08-15 19:48:27 +03:00
parent 69f6c29860
commit 138d3a7886
13 changed files with 920 additions and 51 deletions

View File

@ -128,6 +128,9 @@ dependencies {
implementation 'com.google.code.findbugs:jsr305:3.0.2'
debugImplementation 'androidx.compose.ui:ui-tooling'
debugImplementation 'androidx.compose.ui:ui-test-manifest'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation "org.mockito:mockito-core:1.9.5"
androidTestImplementation 'com.google.dexmaker:dexmaker:1.2'

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="25.5dp"
android:height="25.5dp"
android:viewportWidth="25.5"
android:viewportHeight="25.5">
<path
android:pathData="M9.75,18.75L15.75,12.75L9.75,6.75"
android:strokeLineJoin="round"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,39 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="19.5dp"
android:height="19.5dp"
android:viewportWidth="19.5"
android:viewportHeight="19.5">
<path
android:pathData="M9.75,1.5C8.507,1.5 7.5,2.507 7.5,3.75L7.5,9.75C7.5,10.993 8.507,12 9.75,12C10.993,12 12,10.993 12,9.75L12,3.75C12,2.507 10.993,1.5 9.75,1.5ZZ"
android:strokeLineJoin="round"
android:fillColor="#FFFFFF"
android:strokeLineCap="round"/>
<path
android:pathData="M9.75,1.5C8.507,1.5 7.5,2.507 7.5,3.75L7.5,9.75C7.5,10.993 8.507,12 9.75,12C10.993,12 12,10.993 12,9.75L12,3.75C12,2.507 10.993,1.5 9.75,1.5ZZ"
android:strokeLineJoin="round"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"/>
<path
android:pathData="M15,8.25L15,9.75C15,12.649 12.649,15 9.75,15C6.851,15 4.5,12.649 4.5,9.75L4.5,8.25"
android:strokeLineJoin="round"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"/>
<path
android:pathData="M9.75,15L9.75,18"
android:strokeLineJoin="round"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"/>
<path
android:pathData="M6.75,18L12.75,18"
android:strokeLineJoin="round"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="14.4dp"
android:height="3dp"
android:viewportWidth="14.4"
android:viewportHeight="3">
<path
android:pathData="M0,1.5a1.5,1.5 0,1 0,3 0a1.5,1.5 0,1 0,-3 0z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M5.7,1.5a1.5,1.5 0,1 0,3 0a1.5,1.5 0,1 0,-3 0z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M11.4,1.5a1.5,1.5 0,1 0,3 0a1.5,1.5 0,1 0,-3 0z"
android:fillColor="#FFFFFF"/>
</vector>

View File

@ -24,17 +24,6 @@
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:orientation="vertical" >
<!-- To ensure that key preview popup is correctly placed when the current system locale is
one of RTL locales, layoutDirection="ltr" is needed in the SDK version 17+. -->
<org.futo.inputmethod.latin.suggestions.SuggestionStripView
android:id="@+id/suggestion_strip_view"
android:layoutDirection="ltr"
android:layout_width="match_parent"
android:layout_height="@dimen/config_suggestions_strip_height"
android:gravity="center_vertical"
style="?attr/suggestionStripViewStyle" />
<!-- To ensure that key preview popup is correctly placed when the current system locale is
one of RTL locales, layoutDirection="ltr" is needed in the SDK version 17+. -->
<org.futo.inputmethod.keyboard.MainKeyboardView

View File

@ -31,8 +31,6 @@ import org.futo.inputmethod.latin.suggestions.SuggestionStripView;
public final class InputView extends FrameLayout {
private final Rect mInputViewRect = new Rect();
private MainKeyboardView mMainKeyboardView;
private KeyboardTopPaddingForwarder mKeyboardTopPaddingForwarder;
private MoreSuggestionsViewCanceler mMoreSuggestionsViewCanceler;
private MotionEventForwarder<?, ?> mActiveForwarder;
public InputView(final Context context, final AttributeSet attrs) {
@ -41,17 +39,11 @@ public final class InputView extends FrameLayout {
@Override
protected void onFinishInflate() {
final SuggestionStripView suggestionStripView =
(SuggestionStripView)findViewById(R.id.suggestion_strip_view);
mMainKeyboardView = (MainKeyboardView)findViewById(R.id.keyboard_view);
mKeyboardTopPaddingForwarder = new KeyboardTopPaddingForwarder(
mMainKeyboardView, suggestionStripView);
mMoreSuggestionsViewCanceler = new MoreSuggestionsViewCanceler(
mMainKeyboardView, suggestionStripView);
}
public void setKeyboardTopPadding(final int keyboardTopPadding) {
mKeyboardTopPaddingForwarder.setKeyboardTopPadding(keyboardTopPadding);
}
@Override
@ -73,20 +65,6 @@ public final class InputView extends FrameLayout {
final int x = (int)me.getX(index) + rect.left;
final int y = (int)me.getY(index) + rect.top;
// The touch events that hit the top padding of keyboard should be forwarded to
// {@link SuggestionStripView}.
if (mKeyboardTopPaddingForwarder.onInterceptTouchEvent(x, y, me)) {
mActiveForwarder = mKeyboardTopPaddingForwarder;
return true;
}
// To cancel {@link MoreSuggestionsView}, we should intercept a touch event to
// {@link MainKeyboardView} and dismiss the {@link MoreSuggestionsView}.
if (mMoreSuggestionsViewCanceler.onInterceptTouchEvent(x, y, me)) {
mActiveForwarder = mMoreSuggestionsViewCanceler;
return true;
}
mActiveForwarder = null;
return false;
}

View File

@ -13,11 +13,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.ComposeView
@ -37,9 +33,13 @@ import androidx.savedstate.SavedStateRegistryController
import androidx.savedstate.SavedStateRegistryOwner
import androidx.savedstate.findViewTreeSavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import org.futo.inputmethod.latin.uix.ActionBar
class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner {
private val latinIMELegacy = LatinIMELegacy(this as InputMethodService)
class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner, LatinIMELegacy.SuggestionStripController {
private val latinIMELegacy = LatinIMELegacy(
this as InputMethodService,
this as LatinIMELegacy.SuggestionStripController
)
private val mSavedStateRegistryController = SavedStateRegistryController.create(this)
@ -115,6 +115,8 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
return composeView!!
}
private var shouldShowSuggestionStrip: Boolean = true
private var suggestedWords: SuggestedWords? = null
private fun setContent() {
composeView?.setContent {
Column {
@ -123,6 +125,12 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
touchableHeight = it.height
}, color = MaterialTheme.colorScheme.surface) {
Column {
if(shouldShowSuggestionStrip) {
ActionBar(
suggestedWords,
latinIMELegacy
)
}
key(legacyInputView) {
AndroidView(factory = {
legacyInputView!!
@ -276,4 +284,18 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
return latinIMELegacy.onKeyUp(keyCode, event) || super.onKeyUp(keyCode, event)
}
override fun updateVisibility(shouldShowSuggestionsStrip: Boolean, fullscreenMode: Boolean) {
this.shouldShowSuggestionStrip = shouldShowSuggestionsStrip
setContent()
}
override fun setSuggestions(suggestedWords: SuggestedWords?, rtlSubtype: Boolean) {
this.suggestedWords = suggestedWords
setContent()
}
override fun maybeShowImportantNoticeTitle(): Boolean {
return false
}
}

View File

@ -119,6 +119,14 @@ public class LatinIMELegacy implements KeyboardActionListener,
DictionaryFacilitator.DictionaryInitializationListener,
PermissionsManager.PermissionsResultCallback {
public interface SuggestionStripController {
public void updateVisibility(boolean shouldShowSuggestionsStrip, boolean fullscreenMode);
public void setSuggestions(SuggestedWords suggestedWords, boolean rtlSubtype);
public boolean maybeShowImportantNoticeTitle();
}
private final InputMethodService mInputMethodService;
static final String TAG = LatinIMELegacy.class.getSimpleName();
@ -160,7 +168,7 @@ public class LatinIMELegacy implements KeyboardActionListener,
private View mInputView;
private View mComposeInputView;
private InsetsUpdater mInsetsUpdater;
private SuggestionStripView mSuggestionStripView;
private final SuggestionStripController mSuggestionStripController;
private RichInputMethodManager mRichImm;
@UsedForTesting final KeyboardSwitcher mKeyboardSwitcher;
@ -586,9 +594,10 @@ public class LatinIMELegacy implements KeyboardActionListener,
JniUtils.loadNativeLibrary();
}
public LatinIMELegacy(InputMethodService inputMethodService) {
public LatinIMELegacy(InputMethodService inputMethodService, SuggestionStripController suggestionStripController) {
super();
mInputMethodService = inputMethodService;
mSuggestionStripController = suggestionStripController;
mSettings = Settings.getInstance();
mKeyboardSwitcher = KeyboardSwitcher.getInstance();
mStatsUtilsManager = StatsUtilsManager.getInstance();
@ -846,10 +855,6 @@ public class LatinIMELegacy implements KeyboardActionListener,
public void setInputView(final View view) {
mInputView = view;
mSuggestionStripView = (SuggestionStripView)view.findViewById(R.id.suggestion_strip_view);
if (hasSuggestionStripView()) {
mSuggestionStripView.setListener(this, view);
}
}
public void setCandidatesView(final View view) {
@ -1495,7 +1500,7 @@ public class LatinIMELegacy implements KeyboardActionListener,
}
public boolean hasSuggestionStripView() {
return null != mSuggestionStripView;
return null != mSuggestionStripController;
}
private void setSuggestedWords(final SuggestedWords suggestedWords) {
@ -1520,7 +1525,7 @@ public class LatinIMELegacy implements KeyboardActionListener,
|| currentSettingsValues.isApplicationSpecifiedCompletionsOn();
final boolean shouldShowSuggestionsStrip = shouldShowSuggestionsStripUnlessPassword
&& !currentSettingsValues.mInputAttributes.mIsPasswordField;
mSuggestionStripView.updateVisibility(shouldShowSuggestionsStrip, mInputMethodService.isFullscreenMode());
mSuggestionStripController.updateVisibility(shouldShowSuggestionsStrip, mInputMethodService.isFullscreenMode());
if (!shouldShowSuggestionsStrip) {
return;
}
@ -1536,7 +1541,7 @@ public class LatinIMELegacy implements KeyboardActionListener,
final boolean noSuggestionsToOverrideImportantNotice = noSuggestionsFromDictionaries
|| isBeginningOfSentencePrediction;
if (shouldShowImportantNotice && noSuggestionsToOverrideImportantNotice) {
if (mSuggestionStripView.maybeShowImportantNoticeTitle()) {
if (mSuggestionStripController.maybeShowImportantNoticeTitle()) {
return;
}
}
@ -1545,7 +1550,7 @@ public class LatinIMELegacy implements KeyboardActionListener,
|| currentSettingsValues.isApplicationSpecifiedCompletionsOn()
// We should clear the contextual strip if there is no suggestion from dictionaries.
|| noSuggestionsFromDictionaries) {
mSuggestionStripView.setSuggestions(suggestedWords,
mSuggestionStripController.setSuggestions(suggestedWords,
mRichImm.getCurrentSubtype().isRtlSubtype());
}
}

View File

@ -0,0 +1,402 @@
package org.futo.inputmethod.latin.uix
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
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.width
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Divider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
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.FontWeight
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.text.style.TextAlign
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.Dp
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.futo.inputmethod.latin.R
import org.futo.inputmethod.latin.SuggestedWords
import org.futo.inputmethod.latin.SuggestedWords.SuggestedWordInfo
import org.futo.inputmethod.latin.common.Constants
import org.futo.inputmethod.latin.suggestions.SuggestionStripView
import org.futo.inputmethod.latin.uix.theme.Typography
import org.futo.inputmethod.latin.uix.theme.WhisperVoiceInputTheme
import java.lang.Float.max
import java.lang.IndexOutOfBoundsException
import java.lang.Integer.min
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 exampleSuggestionsList = arrayListOf(
SuggestedWordInfo("verylongword123", "", 100, 1, null, 0, 0),
SuggestedWordInfo("world understanding of patience", "", 99, 1, null, 0, 0),
SuggestedWordInfo("b", "", 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
)
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
)
@OptIn(ExperimentalTextApi::class)
@Composable fun RowScope.SuggestionItem(words: SuggestedWords, idx: Int, onClick: () -> Unit) {
val word = try {
words.getWord(idx)
} catch(e: IndexOutOfBoundsException) {
null
}
val topSuggestionIcon = painterResource(id = R.drawable.top_suggestion)
val textButtonModifier = when (idx) {
0 -> 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)
}
}
}
else -> Modifier
}
val textModifier = when (idx) {
0 -> Modifier
else -> Modifier.alpha(0.75f)
}
val textStyle = when (idx) {
0 -> suggestionStylePrimary
else -> suggestionStyleAlternative
}.copy(color = MaterialTheme.colorScheme.onPrimary)
TextButton(
onClick = onClick,
modifier = textButtonModifier
.weight(1.0f)
.fillMaxHeight(),
shape = RectangleShape,
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.onSurface),
enabled = word != null
) {
if(word != null) {
val measurer = rememberTextMeasurer()
Canvas(modifier = textModifier.fillMaxSize()) {
val measurement = measurer.measure(
text = AnnotatedString(word),
style = textStyle,
overflow = TextOverflow.Visible,
softWrap = false,
maxLines = 1,
constraints = Constraints(
maxWidth = Int.MAX_VALUE,
maxHeight = ceil(this.size.height).roundToInt()
),
layoutDirection = LayoutDirection.Ltr,
density = this
)
val scale = Math.min(1.0f, size.width / measurement.size.width)
translate(left = (scale * (size.width - measurement.size.width)) / 2.0f) {
scale(scaleX = scale, scaleY = 1.0f) {
drawText(
measurement
)
}
}
}
/*
Text(
word,
modifier = textModifier,
style = textStyle,
overflow = TextOverflow.Visible,
softWrap = false,
maxLines = 1
)
*/
}
}
}
@Composable fun RowScope.SuggestionSeparator() {
Box(
modifier = Modifier
.fillMaxHeight(0.66f)
.align(CenterVertically)
.background(color = MaterialTheme.colorScheme.outline)
.width((1f / LocalDensity.current.density).dp)
)
}
// Show the most probable in the middle, then left, then right
val ORDER_OF_SUGGESTIONS = listOf(1, 0, 2)
@Composable
fun RowScope.SuggestionItems(words: SuggestedWords, onClick: (i: Int) -> Unit) {
val maxSuggestions = min(ORDER_OF_SUGGESTIONS.size, words.size())
if(maxSuggestions == 0) {
Spacer(modifier = Modifier.weight(1.0f))
return
}
for (i in 0 until maxSuggestions) {
val remapped = ORDER_OF_SUGGESTIONS[i]
SuggestionItem(words, remapped) { onClick(remapped) }
if (i < maxSuggestions - 1) SuggestionSeparator()
}
}
@Composable
fun RowScope.ActionItem() {
val col = MaterialTheme.colorScheme.secondary
IconButton(onClick = { /*TODO*/ }, modifier = Modifier
.drawBehind {
val radius = size.height / 4.0f
drawRoundRect(
col,
topLeft = Offset(size.width * 0.1f, size.height * 0.1f),
size = Size(size.width * 0.8f, size.height * 0.8f),
cornerRadius = CornerRadius(radius, radius)
)
}
.width(50.dp)
.fillMaxHeight()) {
Icon(
painter = painterResource(id = R.drawable.mic_fill),
contentDescription = "Voice Input"
)
}
}
@Composable
fun RowScope.ActionItems() {
ActionItem()
ActionItem()
ActionItem()
Spacer(modifier = Modifier.weight(1.0f))
}
class ExampleListener : SuggestionStripView.Listener {
override fun showImportantNoticeContents() {
}
override fun pickSuggestionManually(word: SuggestedWordInfo?) {
}
override fun onCodeInput(primaryCode: Int, x: Int, y: Int, isKeyRepeat: Boolean) {
}
}
@Composable
@Preview
fun ActionBar(
words: SuggestedWords? = exampleSuggestedWords,
suggestionStripListener: SuggestionStripView.Listener = ExampleListener()
) {
val isActionsOpen = remember { mutableStateOf(false) }
val actionsForcedOpenByUser = remember { mutableStateOf(false) }
//LaunchedEffect(words?.isEmpty) {
// isActionsOpen.value = actionsForcedOpenByUser.value || words == null || words.isEmpty
//}
LaunchedEffect(actionsForcedOpenByUser.value) {
isActionsOpen.value = actionsForcedOpenByUser.value //|| (words == null || words.isEmpty)
}
WhisperVoiceInputTheme {
Surface(modifier = Modifier
.fillMaxWidth()
.height(40.dp), color = MaterialTheme.colorScheme.surface)
{
Row {
val moreActionsColor = MaterialTheme.colorScheme.primary
val moreActionsFill = if(isActionsOpen.value) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.surface
}
IconButton(
onClick = { actionsForcedOpenByUser.value = !actionsForcedOpenByUser.value },
modifier = Modifier
.width(42.dp)
.rotate(
if (isActionsOpen.value) {
180.0f
} else {
0.0f
}
)
.fillMaxHeight()
.drawBehind {
drawCircle(color = moreActionsColor, radius = size.width / 3.0f + 1.0f)
drawCircle(color = moreActionsFill, radius = size.width / 3.0f - 2.0f)
}
) {
Icon(
painter = painterResource(id = R.drawable.chevron_right),
contentDescription = "Open Actions"
)
}
if(isActionsOpen.value) {
ActionItems()
} else if(words != null) {
SuggestionItems(words) {
suggestionStripListener.pickSuggestionManually(
words.getInfo(it)
)
}
} else {
Spacer(modifier = Modifier.weight(1.0f))
}
// TODO: For now, this calls CODE_SHORTCUT. In the future, we will want to
// ask the main UI to hide the keyboard and show our own voice input menu
IconButton(onClick = {
suggestionStripListener.onCodeInput(
Constants.CODE_SHORTCUT,
Constants.SUGGESTION_STRIP_COORDINATE,
Constants.SUGGESTION_STRIP_COORDINATE,
false
);
}, modifier = Modifier
.width(42.dp)
.fillMaxHeight()) {
Icon(
painter = painterResource(id = R.drawable.mic_fill),
contentDescription = "Voice Input"
)
}
}
}
}
}

View File

@ -0,0 +1,292 @@
@file:Suppress("unused")
package org.futo.inputmethod.latin.uix.theme
import androidx.compose.ui.graphics.Color
// Colors from Tailwind - https://tailwindcss.com/docs/customizing-colors
val Slate50 = Color(0xfff8fafc)
val Slate100 = Color(0xfff1f5f9)
val Slate200 = Color(0xffe2e8f0)
val Slate300 = Color(0xffcbd5e1)
val Slate400 = Color(0xff94a3b8)
val Slate500 = Color(0xff64748b)
val Slate600 = Color(0xff475569)
val Slate700 = Color(0xff334155)
val Slate800 = Color(0xff1e293b)
val Slate900 = Color(0xff0f172a)
val Slate950 = Color(0xff020617)
val Gray50 = Color(0xfff9fafb)
val Gray100 = Color(0xfff3f4f6)
val Gray200 = Color(0xffe5e7eb)
val Gray300 = Color(0xffd1d5db)
val Gray400 = Color(0xff9ca3af)
val Gray500 = Color(0xff6b7280)
val Gray600 = Color(0xff4b5563)
val Gray700 = Color(0xff374151)
val Gray800 = Color(0xff1f2937)
val Gray900 = Color(0xff111827)
val Gray950 = Color(0xff030712)
val Zinc50 = Color(0xfffafafa)
val Zinc100 = Color(0xfff4f4f5)
val Zinc200 = Color(0xffe4e4e7)
val Zinc300 = Color(0xffd4d4d8)
val Zinc400 = Color(0xffa1a1aa)
val Zinc500 = Color(0xff71717a)
val Zinc600 = Color(0xff52525b)
val Zinc700 = Color(0xff3f3f46)
val Zinc800 = Color(0xff27272a)
val Zinc900 = Color(0xff18181b)
val Zinc950 = Color(0xff09090b)
val Neutral50 = Color(0xfffafafa)
val Neutral100 = Color(0xfff5f5f5)
val Neutral200 = Color(0xffe5e5e5)
val Neutral300 = Color(0xffd4d4d4)
val Neutral400 = Color(0xffa3a3a3)
val Neutral500 = Color(0xff737373)
val Neutral600 = Color(0xff525252)
val Neutral700 = Color(0xff404040)
val Neutral800 = Color(0xff262626)
val Neutral900 = Color(0xff171717)
val Neutral950 = Color(0xff0a0a0a)
val Stone50 = Color(0xfffafaf9)
val Stone100 = Color(0xfff5f5f4)
val Stone200 = Color(0xffe7e5e4)
val Stone300 = Color(0xffd6d3d1)
val Stone400 = Color(0xffa8a29e)
val Stone500 = Color(0xff78716c)
val Stone600 = Color(0xff57534e)
val Stone700 = Color(0xff44403c)
val Stone800 = Color(0xff292524)
val Stone900 = Color(0xff1c1917)
val Stone950 = Color(0xff0c0a09)
val Red50 = Color(0xfffef2f2)
val Red100 = Color(0xfffee2e2)
val Red200 = Color(0xfffecaca)
val Red300 = Color(0xfffca5a5)
val Red400 = Color(0xfff87171)
val Red500 = Color(0xffef4444)
val Red600 = Color(0xffdc2626)
val Red700 = Color(0xffb91c1c)
val Red800 = Color(0xff991b1b)
val Red900 = Color(0xff7f1d1d)
val Red950 = Color(0xff450a0a)
val Orange50 = Color(0xfffff7ed)
val Orange100 = Color(0xffffedd5)
val Orange200 = Color(0xfffed7aa)
val Orange300 = Color(0xfffdba74)
val Orange400 = Color(0xfffb923c)
val Orange500 = Color(0xfff97316)
val Orange600 = Color(0xffea580c)
val Orange700 = Color(0xffc2410c)
val Orange800 = Color(0xff9a3412)
val Orange900 = Color(0xff7c2d12)
val Orange950 = Color(0xff431407)
val Amber50 = Color(0xfffffbeb)
val Amber100 = Color(0xfffef3c7)
val Amber200 = Color(0xfffde68a)
val Amber300 = Color(0xfffcd34d)
val Amber400 = Color(0xfffbbf24)
val Amber500 = Color(0xfff59e0b)
val Amber600 = Color(0xffd97706)
val Amber700 = Color(0xffb45309)
val Amber800 = Color(0xff92400e)
val Amber900 = Color(0xff78350f)
val Amber950 = Color(0xff451a03)
val Yellow50 = Color(0xfffefce8)
val Yellow100 = Color(0xfffef9c3)
val Yellow200 = Color(0xfffef08a)
val Yellow300 = Color(0xfffde047)
val Yellow400 = Color(0xfffacc15)
val Yellow500 = Color(0xffeab308)
val Yellow600 = Color(0xffca8a04)
val Yellow700 = Color(0xffa16207)
val Yellow800 = Color(0xff854d0e)
val Yellow900 = Color(0xff713f12)
val Yellow950 = Color(0xff422006)
val Lime50 = Color(0xfff7fee7)
val Lime100 = Color(0xffecfccb)
val Lime200 = Color(0xffd9f99d)
val Lime300 = Color(0xffbef264)
val Lime400 = Color(0xffa3e635)
val Lime500 = Color(0xff84cc16)
val Lime600 = Color(0xff65a30d)
val Lime700 = Color(0xff4d7c0f)
val Lime800 = Color(0xff3f6212)
val Lime900 = Color(0xff365314)
val Lime950 = Color(0xff1a2e05)
val Green50 = Color(0xfff0fdf4)
val Green100 = Color(0xffdcfce7)
val Green200 = Color(0xffbbf7d0)
val Green300 = Color(0xff86efac)
val Green400 = Color(0xff4ade80)
val Green500 = Color(0xff22c55e)
val Green600 = Color(0xff16a34a)
val Green700 = Color(0xff15803d)
val Green800 = Color(0xff166534)
val Green900 = Color(0xff14532d)
val Green950 = Color(0xff052e16)
val Emerald50 = Color(0xffecfdf5)
val Emerald100 = Color(0xffd1fae5)
val Emerald200 = Color(0xffa7f3d0)
val Emerald300 = Color(0xff6ee7b7)
val Emerald400 = Color(0xff34d399)
val Emerald500 = Color(0xff10b981)
val Emerald600 = Color(0xff059669)
val Emerald700 = Color(0xff047857)
val Emerald800 = Color(0xff065f46)
val Emerald900 = Color(0xff064e3b)
val Emerald950 = Color(0xff022c22)
val Teal50 = Color(0xfff0fdfa)
val Teal100 = Color(0xffccfbf1)
val Teal200 = Color(0xff99f6e4)
val Teal300 = Color(0xff5eead4)
val Teal400 = Color(0xff2dd4bf)
val Teal500 = Color(0xff14b8a6)
val Teal600 = Color(0xff0d9488)
val Teal700 = Color(0xff0f766e)
val Teal800 = Color(0xff115e59)
val Teal900 = Color(0xff134e4a)
val Teal950 = Color(0xff042f2e)
val Cyan50 = Color(0xffecfeff)
val Cyan100 = Color(0xffcffafe)
val Cyan200 = Color(0xffa5f3fc)
val Cyan300 = Color(0xff67e8f9)
val Cyan400 = Color(0xff22d3ee)
val Cyan500 = Color(0xff06b6d4)
val Cyan600 = Color(0xff0891b2)
val Cyan700 = Color(0xff0e7490)
val Cyan800 = Color(0xff155e75)
val Cyan900 = Color(0xff164e63)
val Cyan950 = Color(0xff083344)
val Sky50 = Color(0xfff0f9ff)
val Sky100 = Color(0xffe0f2fe)
val Sky200 = Color(0xffbae6fd)
val Sky300 = Color(0xff7dd3fc)
val Sky400 = Color(0xff38bdf8)
val Sky500 = Color(0xff0ea5e9)
val Sky600 = Color(0xff0284c7)
val Sky700 = Color(0xff0369a1)
val Sky800 = Color(0xff075985)
val Sky900 = Color(0xff0c4a6e)
val Sky950 = Color(0xff082f49)
val Blue50 = Color(0xffeff6ff)
val Blue100 = Color(0xffdbeafe)
val Blue200 = Color(0xffbfdbfe)
val Blue300 = Color(0xff93c5fd)
val Blue400 = Color(0xff60a5fa)
val Blue500 = Color(0xff3b82f6)
val Blue600 = Color(0xff2563eb)
val Blue700 = Color(0xff1d4ed8)
val Blue800 = Color(0xff1e40af)
val Blue900 = Color(0xff1e3a8a)
val Blue950 = Color(0xff172554)
val Indigo50 = Color(0xffeef2ff)
val Indigo100 = Color(0xffe0e7ff)
val Indigo200 = Color(0xffc7d2fe)
val Indigo300 = Color(0xffa5b4fc)
val Indigo400 = Color(0xff818cf8)
val Indigo500 = Color(0xff6366f1)
val Indigo600 = Color(0xff4f46e5)
val Indigo700 = Color(0xff4338ca)
val Indigo800 = Color(0xff3730a3)
val Indigo900 = Color(0xff312e81)
val Indigo950 = Color(0xff1e1b4b)
val Violet50 = Color(0xfff5f3ff)
val Violet100 = Color(0xffede9fe)
val Violet200 = Color(0xffddd6fe)
val Violet300 = Color(0xffc4b5fd)
val Violet400 = Color(0xffa78bfa)
val Violet500 = Color(0xff8b5cf6)
val Violet600 = Color(0xff7c3aed)
val Violet700 = Color(0xff6d28d9)
val Violet800 = Color(0xff5b21b6)
val Violet900 = Color(0xff4c1d95)
val Violet950 = Color(0xff2e1065)
val Purple50 = Color(0xfffaf5ff)
val Purple100 = Color(0xfff3e8ff)
val Purple200 = Color(0xffe9d5ff)
val Purple300 = Color(0xffd8b4fe)
val Purple400 = Color(0xffc084fc)
val Purple500 = Color(0xffa855f7)
val Purple600 = Color(0xff9333ea)
val Purple700 = Color(0xff7e22ce)
val Purple800 = Color(0xff6b21a8)
val Purple900 = Color(0xff581c87)
val Purple950 = Color(0xff3b0764)
val Fuchsia50 = Color(0xfffdf4ff)
val Fuchsia100 = Color(0xfffae8ff)
val Fuchsia200 = Color(0xfff5d0fe)
val Fuchsia300 = Color(0xfff0abfc)
val Fuchsia400 = Color(0xffe879f9)
val Fuchsia500 = Color(0xffd946ef)
val Fuchsia600 = Color(0xffc026d3)
val Fuchsia700 = Color(0xffa21caf)
val Fuchsia800 = Color(0xff86198f)
val Fuchsia900 = Color(0xff701a75)
val Fuchsia950 = Color(0xff4a044e)
val Pink50 = Color(0xfffdf2f8)
val Pink100 = Color(0xfffce7f3)
val Pink200 = Color(0xfffbcfe8)
val Pink300 = Color(0xfff9a8d4)
val Pink400 = Color(0xfff472b6)
val Pink500 = Color(0xffec4899)
val Pink600 = Color(0xffdb2777)
val Pink700 = Color(0xffbe185d)
val Pink800 = Color(0xff9d174d)
val Pink900 = Color(0xff831843)
val Pink950 = Color(0xff500724)
val Rose50 = Color(0xfffff1f2)
val Rose100 = Color(0xffffe4e6)
val Rose200 = Color(0xfffecdd3)
val Rose300 = Color(0xfffda4af)
val Rose400 = Color(0xfffb7185)
val Rose500 = Color(0xfff43f5e)
val Rose600 = Color(0xffe11d48)
val Rose700 = Color(0xffbe123c)
val Rose800 = Color(0xff9f1239)
val Rose900 = Color(0xff881337)
val Rose950 = Color(0xff4c0519)

View File

@ -0,0 +1,77 @@
package org.futo.inputmethod.latin.uix.theme
import android.app.Activity
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
private val DarkColorScheme = darkColorScheme(
primary = Slate600,
onPrimary = Slate50,
primaryContainer = Slate700,
onPrimaryContainer = Slate50,
secondary = Slate700,
onSecondary = Slate50,
secondaryContainer = Slate600,
onSecondaryContainer = Slate50,
tertiary = Stone700,
onTertiary = Stone50,
tertiaryContainer = Stone600,
onTertiaryContainer = Stone50,
background = Slate900,
onBackground = Slate50,
surface = Slate800,
onSurface = Slate50,
outline = Slate300,
surfaceVariant = Slate800,
onSurfaceVariant = Slate300
)
@Composable
fun WhisperVoiceInputTheme(content: @Composable () -> Unit) {
/*
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
*/
val colorScheme = DarkColorScheme // TODO: Figure out light/dynamic if it's worth it
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
if(view.context is Activity) {
val window = (view.context as Activity).window
window.statusBarColor = colorScheme.primary.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars =
false
}
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}

View File

@ -0,0 +1,34 @@
package org.futo.inputmethod.latin.uix.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.SansSerif,
fontWeight = FontWeight.Light,
fontSize = 20.sp,
lineHeight = 26.sp,
letterSpacing = 0.5.sp
),
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
*/
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
)

View File

@ -18,7 +18,7 @@ package org.futo.inputmethod.latin;
public class LatinIMELegacyForTests extends LatinIMELegacy {
public LatinIMELegacyForTests() {
super(mInputMethodService);
super(mInputMethodService, null);
}
@Override