From 138d3a78863e8a1f201ad5c2b1d66e557449dca7 Mon Sep 17 00:00:00 2001
From: abb128 <65567823+abb128@users.noreply.github.com>
Date: Tue, 15 Aug 2023 19:48:27 +0300
Subject: [PATCH] Replace SuggestionStripView with ActionBar
---
build.gradle | 3 +
java/res/drawable/chevron_right.xml | 13 +
java/res/drawable/mic_fill.xml | 39 ++
java/res/drawable/top_suggestion.xml | 15 +
java/res/layout/main_keyboard_frame.xml | 11 -
.../org/futo/inputmethod/latin/InputView.java | 24 +-
.../org/futo/inputmethod/latin/LatinIME.kt | 34 +-
.../inputmethod/latin/LatinIMELegacy.java | 25 +-
.../futo/inputmethod/latin/uix/ActionBar.kt | 402 ++++++++++++++++++
.../futo/inputmethod/latin/uix/theme/Color.kt | 292 +++++++++++++
.../futo/inputmethod/latin/uix/theme/Theme.kt | 77 ++++
.../futo/inputmethod/latin/uix/theme/Type.kt | 34 ++
.../latin/LatinIMELegacyForTests.java | 2 +-
13 files changed, 920 insertions(+), 51 deletions(-)
create mode 100644 java/res/drawable/chevron_right.xml
create mode 100644 java/res/drawable/mic_fill.xml
create mode 100644 java/res/drawable/top_suggestion.xml
create mode 100644 java/src/org/futo/inputmethod/latin/uix/ActionBar.kt
create mode 100644 java/src/org/futo/inputmethod/latin/uix/theme/Color.kt
create mode 100644 java/src/org/futo/inputmethod/latin/uix/theme/Theme.kt
create mode 100644 java/src/org/futo/inputmethod/latin/uix/theme/Type.kt
diff --git a/build.gradle b/build.gradle
index 907ce24cd..7c422a2ce 100644
--- a/build.gradle
+++ b/build.gradle
@@ -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'
diff --git a/java/res/drawable/chevron_right.xml b/java/res/drawable/chevron_right.xml
new file mode 100644
index 000000000..a333ad413
--- /dev/null
+++ b/java/res/drawable/chevron_right.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/java/res/drawable/mic_fill.xml b/java/res/drawable/mic_fill.xml
new file mode 100644
index 000000000..b866a57d0
--- /dev/null
+++ b/java/res/drawable/mic_fill.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
diff --git a/java/res/drawable/top_suggestion.xml b/java/res/drawable/top_suggestion.xml
new file mode 100644
index 000000000..2e02a2679
--- /dev/null
+++ b/java/res/drawable/top_suggestion.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/java/res/layout/main_keyboard_frame.xml b/java/res/layout/main_keyboard_frame.xml
index dc5ba7819..412e89a8a 100644
--- a/java/res/layout/main_keyboard_frame.xml
+++ b/java/res/layout/main_keyboard_frame.xml
@@ -24,17 +24,6 @@
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:orientation="vertical" >
-
-
-
-
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;
}
diff --git a/java/src/org/futo/inputmethod/latin/LatinIME.kt b/java/src/org/futo/inputmethod/latin/LatinIME.kt
index b0ed92e21..46f7b9d27 100644
--- a/java/src/org/futo/inputmethod/latin/LatinIME.kt
+++ b/java/src/org/futo/inputmethod/latin/LatinIME.kt
@@ -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
+ }
}
\ No newline at end of file
diff --git a/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java b/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java
index 8ba7f0536..a5c35065c 100644
--- a/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java
+++ b/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java
@@ -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());
}
}
diff --git a/java/src/org/futo/inputmethod/latin/uix/ActionBar.kt b/java/src/org/futo/inputmethod/latin/uix/ActionBar.kt
new file mode 100644
index 000000000..36b0d7b30
--- /dev/null
+++ b/java/src/org/futo/inputmethod/latin/uix/ActionBar.kt
@@ -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"
+ )
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/java/src/org/futo/inputmethod/latin/uix/theme/Color.kt b/java/src/org/futo/inputmethod/latin/uix/theme/Color.kt
new file mode 100644
index 000000000..37cd0f123
--- /dev/null
+++ b/java/src/org/futo/inputmethod/latin/uix/theme/Color.kt
@@ -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)
diff --git a/java/src/org/futo/inputmethod/latin/uix/theme/Theme.kt b/java/src/org/futo/inputmethod/latin/uix/theme/Theme.kt
new file mode 100644
index 000000000..3222a46e6
--- /dev/null
+++ b/java/src/org/futo/inputmethod/latin/uix/theme/Theme.kt
@@ -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
+ )
+}
\ No newline at end of file
diff --git a/java/src/org/futo/inputmethod/latin/uix/theme/Type.kt b/java/src/org/futo/inputmethod/latin/uix/theme/Type.kt
new file mode 100644
index 000000000..f0da9f77a
--- /dev/null
+++ b/java/src/org/futo/inputmethod/latin/uix/theme/Type.kt
@@ -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
+ )
+)
\ No newline at end of file
diff --git a/tests/src/org/futo/inputmethod/latin/LatinIMELegacyForTests.java b/tests/src/org/futo/inputmethod/latin/LatinIMELegacyForTests.java
index 31cee8518..78db36b43 100644
--- a/tests/src/org/futo/inputmethod/latin/LatinIMELegacyForTests.java
+++ b/tests/src/org/futo/inputmethod/latin/LatinIMELegacyForTests.java
@@ -18,7 +18,7 @@ package org.futo.inputmethod.latin;
public class LatinIMELegacyForTests extends LatinIMELegacy {
public LatinIMELegacyForTests() {
- super(mInputMethodService);
+ super(mInputMethodService, null);
}
@Override