summaryrefslogtreecommitdiffstats
path: root/mobile/android/android-components/components/concept/menu
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-15 03:35:49 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-15 03:35:49 +0000
commitd8bbc7858622b6d9c278469aab701ca0b609cddf (patch)
treeeff41dc61d9f714852212739e6b3738b82a2af87 /mobile/android/android-components/components/concept/menu
parentReleasing progress-linux version 125.0.3-1~progress7.99u1. (diff)
downloadfirefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.tar.xz
firefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.zip
Merging upstream version 126.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'mobile/android/android-components/components/concept/menu')
-rw-r--r--mobile/android/android-components/components/concept/menu/build.gradle38
-rw-r--r--mobile/android/android-components/components/concept/menu/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/concept/menu/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/MenuButton.kt47
-rw-r--r--mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/MenuController.kt53
-rw-r--r--mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/MenuStyle.kt44
-rw-r--r--mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/Orientation.kt39
-rw-r--r--mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/Side.kt20
-rw-r--r--mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/ContainerStyle.kt16
-rw-r--r--mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/MenuCandidate.kt123
-rw-r--r--mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/MenuEffect.kt46
-rw-r--r--mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/MenuIcon.kt97
-rw-r--r--mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/SmallMenuCandidate.kt22
-rw-r--r--mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/TextStyle.kt46
-rw-r--r--mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/ext/MenuCandidate.kt63
-rw-r--r--mobile/android/android-components/components/concept/menu/src/test/java/mozilla/components/concept/menu/ext/MenuCandidateTest.kt179
16 files changed, 858 insertions, 0 deletions
diff --git a/mobile/android/android-components/components/concept/menu/build.gradle b/mobile/android/android-components/components/concept/menu/build.gradle
new file mode 100644
index 0000000000..3ba2e45a89
--- /dev/null
+++ b/mobile/android/android-components/components/concept/menu/build.gradle
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.concept.menu'
+}
+
+dependencies {
+ implementation ComponentsDependencies.androidx_annotation
+ implementation ComponentsDependencies.androidx_appcompat
+ implementation project(':support-base')
+ implementation project(':support-ktx')
+
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation project(':support-test')
+}
+
+apply from: '../../../android-lint.gradle'
+apply from: '../../../publish.gradle'
+ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description)
diff --git a/mobile/android/android-components/components/concept/menu/proguard-rules.pro b/mobile/android/android-components/components/concept/menu/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/concept/menu/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/mobile/android/android-components/components/concept/menu/src/main/AndroidManifest.xml b/mobile/android/android-components/components/concept/menu/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/concept/menu/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<manifest />
diff --git a/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/MenuButton.kt b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/MenuButton.kt
new file mode 100644
index 0000000000..c59f25cb70
--- /dev/null
+++ b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/MenuButton.kt
@@ -0,0 +1,47 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.concept.menu
+
+import androidx.annotation.ColorInt
+import mozilla.components.concept.menu.candidate.MenuEffect
+import mozilla.components.support.base.observer.Observable
+
+/**
+ * A `three-dot` button used for expanding menus.
+ *
+ * If you are using a browser toolbar, do not use this class directly.
+ */
+interface MenuButton : Observable<MenuButton.Observer> {
+
+ /**
+ * Sets a [MenuController] that will be used to create a menu when this button is clicked.
+ */
+ var menuController: MenuController?
+
+ /**
+ * Show the indicator for a browser menu effect.
+ */
+ fun setEffect(effect: MenuEffect?)
+
+ /**
+ * Sets the tint of the 3-dot menu icon.
+ */
+ fun setColorFilter(@ColorInt color: Int)
+
+ /**
+ * Observer for the menu button.
+ */
+ interface Observer {
+ /**
+ * Listener called when the menu is shown.
+ */
+ fun onShow() = Unit
+
+ /**
+ * Listener called when the menu is dismissed.
+ */
+ fun onDismiss() = Unit
+ }
+}
diff --git a/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/MenuController.kt b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/MenuController.kt
new file mode 100644
index 0000000000..d3a8e0f7e6
--- /dev/null
+++ b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/MenuController.kt
@@ -0,0 +1,53 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.concept.menu
+
+import android.view.View
+import android.widget.PopupWindow
+import mozilla.components.concept.menu.candidate.MenuCandidate
+import mozilla.components.support.base.observer.Observable
+
+/**
+ * Controls a popup menu composed of MenuCandidate objects.
+ */
+interface MenuController : Observable<MenuController.Observer> {
+
+ /**
+ * @param anchor The view on which to pin the popup window.
+ * @param orientation The preferred orientation to show the popup window.
+ * @param autoDismiss True if the popup window should be dismissed when the device orientation
+ * is changed.
+ */
+ fun show(
+ anchor: View,
+ orientation: Orientation? = null,
+ autoDismiss: Boolean = true,
+ ): PopupWindow
+
+ /**
+ * Dismiss the menu popup if the menu is visible.
+ */
+ fun dismiss()
+
+ /**
+ * Changes the contents of the menu.
+ */
+ fun submitList(list: List<MenuCandidate>)
+
+ /**
+ * Observer for the menu controller.
+ */
+ interface Observer {
+ /**
+ * Called when the menu contents have changed.
+ */
+ fun onMenuListSubmit(list: List<MenuCandidate>) = Unit
+
+ /**
+ * Called when the menu has been dismissed.
+ */
+ fun onDismiss() = Unit
+ }
+}
diff --git a/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/MenuStyle.kt b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/MenuStyle.kt
new file mode 100644
index 0000000000..7db0b70b35
--- /dev/null
+++ b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/MenuStyle.kt
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.concept.menu
+
+import android.content.res.ColorStateList
+import androidx.annotation.ColorInt
+import androidx.annotation.Px
+
+/**
+ * Declare custom styles for a menu.
+ *
+ * @property backgroundColor Custom background color for the menu.
+ * @property minWidth Custom minimum width for the menu.
+ * @property maxWidth Custom maximum width for the menu.
+ * @property horizontalOffset Custom horizontal offset for the menu.
+ * @property verticalOffset Custom vertical offset for the menu.
+ * @property completelyOverlap Forces menu to overlap the anchor completely.
+ */
+data class MenuStyle(
+ val backgroundColor: ColorStateList? = null,
+ @Px val minWidth: Int? = null,
+ @Px val maxWidth: Int? = null,
+ @Px val horizontalOffset: Int? = null,
+ @Px val verticalOffset: Int? = null,
+ val completelyOverlap: Boolean = false,
+) {
+ constructor(
+ @ColorInt backgroundColor: Int,
+ @Px minWidth: Int? = null,
+ @Px maxWidth: Int? = null,
+ @Px horizontalOffset: Int? = null,
+ @Px verticalOffset: Int? = null,
+ completelyOverlap: Boolean = false,
+ ) : this(
+ backgroundColor = ColorStateList.valueOf(backgroundColor),
+ minWidth = minWidth,
+ maxWidth = maxWidth,
+ horizontalOffset = horizontalOffset,
+ verticalOffset = verticalOffset,
+ completelyOverlap = completelyOverlap,
+ )
+}
diff --git a/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/Orientation.kt b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/Orientation.kt
new file mode 100644
index 0000000000..60d453480c
--- /dev/null
+++ b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/Orientation.kt
@@ -0,0 +1,39 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.concept.menu
+
+import android.view.Gravity
+
+/**
+ * Indicates the preferred orientation to show the menu.
+ */
+enum class Orientation {
+ /**
+ * Position the menu above the toolbar.
+ */
+ UP,
+
+ /**
+ * Position the menu below the toolbar.
+ */
+ DOWN,
+
+ ;
+
+ companion object {
+
+ /**
+ * Returns an orientation that matches the given [Gravity] value.
+ * Meant to be used with a CoordinatorLayout's gravity.
+ */
+ fun fromGravity(gravity: Int): Orientation {
+ return if ((gravity and Gravity.BOTTOM) == Gravity.BOTTOM) {
+ UP
+ } else {
+ DOWN
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/Side.kt b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/Side.kt
new file mode 100644
index 0000000000..6a956f6e73
--- /dev/null
+++ b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/Side.kt
@@ -0,0 +1,20 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.concept.menu
+
+/**
+ * Indicates the starting or ending side of the menu or an option.
+ */
+enum class Side {
+ /**
+ * Starting side (top or left).
+ */
+ START,
+
+ /**
+ * Ending side (bottom or right).
+ */
+ END,
+}
diff --git a/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/ContainerStyle.kt b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/ContainerStyle.kt
new file mode 100644
index 0000000000..df6242b0e9
--- /dev/null
+++ b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/ContainerStyle.kt
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.concept.menu.candidate
+
+/**
+ * Describes styling for the menu option container.
+ *
+ * @property isVisible When false, the option will not be displayed.
+ * @property isEnabled When false, the option will be greyed out and disabled.
+ */
+data class ContainerStyle(
+ val isVisible: Boolean = true,
+ val isEnabled: Boolean = true,
+)
diff --git a/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/MenuCandidate.kt b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/MenuCandidate.kt
new file mode 100644
index 0000000000..48a12909d1
--- /dev/null
+++ b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/MenuCandidate.kt
@@ -0,0 +1,123 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.concept.menu.candidate
+
+/**
+ * Menu option data classes to be shown in the browser menu.
+ */
+sealed class MenuCandidate {
+ abstract val containerStyle: ContainerStyle
+}
+
+/**
+ * Interactive menu option that displays some text.
+ *
+ * @property text Text to display.
+ * @property start Icon to display before the text.
+ * @property end Icon to display after the text.
+ * @property textStyle Styling to apply to the text.
+ * @property containerStyle Styling to apply to the container.
+ * @property effect Effects to apply to the option.
+ * @property onClick Click listener called when this menu option is clicked.
+ */
+data class TextMenuCandidate(
+ val text: String,
+ val start: MenuIcon? = null,
+ val end: MenuIcon? = null,
+ val textStyle: TextStyle = TextStyle(),
+ override val containerStyle: ContainerStyle = ContainerStyle(),
+ val effect: MenuCandidateEffect? = null,
+ val onClick: () -> Unit = {},
+) : MenuCandidate()
+
+/**
+ * Menu option that displays static text.
+ *
+ * @property text Text to display.
+ * @property height Custom height for the menu option.
+ * @property textStyle Styling to apply to the text.
+ * @property containerStyle Styling to apply to the container.
+ */
+data class DecorativeTextMenuCandidate(
+ val text: String,
+ val height: Int? = null,
+ val textStyle: TextStyle = TextStyle(),
+ override val containerStyle: ContainerStyle = ContainerStyle(),
+) : MenuCandidate()
+
+/**
+ * Menu option that shows a switch or checkbox.
+ *
+ * @property text Text to display.
+ * @property start Icon to display before the text.
+ * @property end Compound button to display after the text.
+ * @property textStyle Styling to apply to the text.
+ * @property containerStyle Styling to apply to the container.
+ * @property effect Effects to apply to the option.
+ * @property onCheckedChange Listener called when this menu option is checked or unchecked.
+ */
+data class CompoundMenuCandidate(
+ val text: String,
+ val isChecked: Boolean,
+ val start: MenuIcon? = null,
+ val end: ButtonType,
+ val textStyle: TextStyle = TextStyle(),
+ override val containerStyle: ContainerStyle = ContainerStyle(),
+ val effect: MenuCandidateEffect? = null,
+ val onCheckedChange: (Boolean) -> Unit = {},
+) : MenuCandidate() {
+
+ /**
+ * Compound button types to display with the compound menu option.
+ */
+ enum class ButtonType {
+ CHECKBOX,
+ SWITCH,
+ }
+}
+
+/**
+ * Menu option that opens a nested sub menu.
+ *
+ * @property id Unique ID for this nested menu. Can be a resource ID.
+ * @property text Text to display.
+ * @property start Icon to display before the text.
+ * @property end Icon to display after the text.
+ * @property subMenuItems Nested menu items to display.
+ * If null, this item will instead return to the root menu.
+ * @property textStyle Styling to apply to the text.
+ * @property containerStyle Styling to apply to the container.
+ * @property effect Effects to apply to the option.
+ */
+data class NestedMenuCandidate(
+ val id: Int,
+ val text: String,
+ val start: MenuIcon? = null,
+ val end: DrawableMenuIcon? = null,
+ val subMenuItems: List<MenuCandidate>? = emptyList(),
+ val textStyle: TextStyle = TextStyle(),
+ override val containerStyle: ContainerStyle = ContainerStyle(),
+ val effect: MenuCandidateEffect? = null,
+) : MenuCandidate()
+
+/**
+ * Displays a row of small menu options.
+ *
+ * @property items Small menu options to display.
+ * @property containerStyle Styling to apply to the container.
+ */
+data class RowMenuCandidate(
+ val items: List<SmallMenuCandidate>,
+ override val containerStyle: ContainerStyle = ContainerStyle(),
+) : MenuCandidate()
+
+/**
+ * Menu option to display a horizontal divider.
+ *
+ * @property containerStyle Styling to apply to the divider.
+ */
+data class DividerMenuCandidate(
+ override val containerStyle: ContainerStyle = ContainerStyle(),
+) : MenuCandidate()
diff --git a/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/MenuEffect.kt b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/MenuEffect.kt
new file mode 100644
index 0000000000..b30104a636
--- /dev/null
+++ b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/MenuEffect.kt
@@ -0,0 +1,46 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.concept.menu.candidate
+
+import androidx.annotation.ColorInt
+
+/**
+ * Describes an effect for the menu.
+ * Effects can also alter the button to open the menu.
+ */
+sealed class MenuEffect
+
+/**
+ * Describes an effect for a menu candidate and its container.
+ * Effects can also alter the button that opens the menu.
+ */
+sealed class MenuCandidateEffect : MenuEffect()
+
+/**
+ * Describes an effect for a menu icon.
+ * Effects can also alter the button that opens the menu.
+ */
+sealed class MenuIconEffect : MenuEffect()
+
+/**
+ * Displays a notification dot.
+ * Used for highlighting new features to the user, such as what's new or a recommended feature.
+ *
+ * @property notificationTint Tint for the notification dot displayed on the icon and menu button.
+ */
+data class LowPriorityHighlightEffect(
+ @ColorInt val notificationTint: Int,
+) : MenuIconEffect()
+
+/**
+ * Changes the background of the menu item.
+ * Used for errors that require user attention, like sync errors.
+ *
+ * @property backgroundTint Tint for the menu item background color.
+ * Also used to highlight the menu button.
+ */
+data class HighPriorityHighlightEffect(
+ @ColorInt val backgroundTint: Int,
+) : MenuCandidateEffect()
diff --git a/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/MenuIcon.kt b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/MenuIcon.kt
new file mode 100644
index 0000000000..84a8135012
--- /dev/null
+++ b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/MenuIcon.kt
@@ -0,0 +1,97 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.concept.menu.candidate
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import androidx.annotation.ColorInt
+import androidx.annotation.DrawableRes
+import androidx.appcompat.content.res.AppCompatResources
+
+/**
+ * Menu option data classes to be shown alongside menu options
+ */
+sealed class MenuIcon
+
+/**
+ * Menu icon that displays a drawable.
+ *
+ * @property drawable Drawable icon to display.
+ * @property tint Tint to apply to the drawable.
+ * @property effect Effects to apply to the icon.
+ */
+data class DrawableMenuIcon(
+ override val drawable: Drawable?,
+ @ColorInt override val tint: Int? = null,
+ val effect: MenuIconEffect? = null,
+) : MenuIcon(), MenuIconWithDrawable {
+
+ constructor(
+ context: Context,
+ @DrawableRes resource: Int,
+ @ColorInt tint: Int? = null,
+ effect: MenuIconEffect? = null,
+ ) : this(AppCompatResources.getDrawable(context, resource), tint, effect)
+}
+
+/**
+ * Menu icon that displays an image button.
+ *
+ * @property drawable Drawable icon to display.
+ * @property tint Tint to apply to the drawable.
+ * @property onClick Click listener called when this menu option is clicked.
+ */
+data class DrawableButtonMenuIcon(
+ override val drawable: Drawable?,
+ @ColorInt override val tint: Int? = null,
+ val onClick: () -> Unit = {},
+) : MenuIcon(), MenuIconWithDrawable {
+
+ constructor(
+ context: Context,
+ @DrawableRes resource: Int,
+ @ColorInt tint: Int? = null,
+ onClick: () -> Unit = {},
+ ) : this(AppCompatResources.getDrawable(context, resource), tint, onClick)
+}
+
+/**
+ * Menu icon that displays a drawable.
+ *
+ * @property loadDrawable Function that creates drawable icon to display.
+ * @property loadingDrawable Drawable that is displayed while loadDrawable is running.
+ * @property fallbackDrawable Drawable that is displayed if loadDrawable fails.
+ * @property tint Tint to apply to the drawable.
+ * @property effect Effects to apply to the icon.
+ */
+data class AsyncDrawableMenuIcon(
+ val loadDrawable: suspend (width: Int, height: Int) -> Drawable?,
+ val loadingDrawable: Drawable? = null,
+ val fallbackDrawable: Drawable? = null,
+ @ColorInt val tint: Int? = null,
+ val effect: MenuIconEffect? = null,
+) : MenuIcon()
+
+/**
+ * Menu icon to display additional text at the end of a menu option.
+ *
+ * @property text Text to display.
+ * @property backgroundTint Color to show behind text.
+ * @property textStyle Styling to apply to the text.
+ */
+data class TextMenuIcon(
+ val text: String,
+ @ColorInt val backgroundTint: Int? = null,
+ val textStyle: TextStyle = TextStyle(),
+) : MenuIcon()
+
+/**
+ * Interface shared by all [MenuIcon]s with drawables.
+ */
+interface MenuIconWithDrawable {
+ val drawable: Drawable?
+
+ @get:ColorInt val tint: Int?
+}
diff --git a/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/SmallMenuCandidate.kt b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/SmallMenuCandidate.kt
new file mode 100644
index 0000000000..51e6fd68cc
--- /dev/null
+++ b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/SmallMenuCandidate.kt
@@ -0,0 +1,22 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.concept.menu.candidate
+
+/**
+ * Small icon button menu option. Can only be used with [RowMenuCandidate].
+ *
+ * @property contentDescription Description of the icon.
+ * @property icon Icon to display.
+ * @property containerStyle Styling to apply to the container.
+ * @property onLongClick Listener called when this menu option is long clicked.
+ * @property onClick Click listener called when this menu option is clicked.
+ */
+data class SmallMenuCandidate(
+ val contentDescription: String,
+ val icon: DrawableMenuIcon,
+ val containerStyle: ContainerStyle = ContainerStyle(),
+ val onLongClick: (() -> Boolean)? = null,
+ val onClick: () -> Unit = {},
+)
diff --git a/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/TextStyle.kt b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/TextStyle.kt
new file mode 100644
index 0000000000..eb85581f53
--- /dev/null
+++ b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/candidate/TextStyle.kt
@@ -0,0 +1,46 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.concept.menu.candidate
+
+import android.graphics.Typeface
+import android.view.View
+import androidx.annotation.ColorInt
+import androidx.annotation.Dimension
+import androidx.annotation.IntDef
+
+/**
+ * Describes styling for text inside a menu option.
+ *
+ * @param size: The size of the text.
+ * @param color: The color to apply to the text.
+ */
+data class TextStyle(
+ @Dimension(unit = Dimension.PX) val size: Float? = null,
+ @ColorInt val color: Int? = null,
+ @TypefaceStyle val textStyle: Int = Typeface.NORMAL,
+ @TextAlignment val textAlignment: Int = View.TEXT_ALIGNMENT_INHERIT,
+)
+
+/**
+ * Enum for [Typeface] values.
+ */
+@IntDef(value = [Typeface.NORMAL, Typeface.BOLD, Typeface.ITALIC, Typeface.BOLD_ITALIC])
+annotation class TypefaceStyle
+
+/**
+ * Enum for text alignment values.
+ */
+@IntDef(
+ value = [
+ View.TEXT_ALIGNMENT_GRAVITY,
+ View.TEXT_ALIGNMENT_INHERIT,
+ View.TEXT_ALIGNMENT_CENTER,
+ View.TEXT_ALIGNMENT_TEXT_START,
+ View.TEXT_ALIGNMENT_TEXT_END,
+ View.TEXT_ALIGNMENT_VIEW_START,
+ View.TEXT_ALIGNMENT_VIEW_END,
+ ],
+)
+annotation class TextAlignment
diff --git a/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/ext/MenuCandidate.kt b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/ext/MenuCandidate.kt
new file mode 100644
index 0000000000..b10ebf4ee1
--- /dev/null
+++ b/mobile/android/android-components/components/concept/menu/src/main/java/mozilla/components/concept/menu/ext/MenuCandidate.kt
@@ -0,0 +1,63 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.concept.menu.ext
+
+import mozilla.components.concept.menu.candidate.CompoundMenuCandidate
+import mozilla.components.concept.menu.candidate.DecorativeTextMenuCandidate
+import mozilla.components.concept.menu.candidate.DividerMenuCandidate
+import mozilla.components.concept.menu.candidate.DrawableMenuIcon
+import mozilla.components.concept.menu.candidate.HighPriorityHighlightEffect
+import mozilla.components.concept.menu.candidate.LowPriorityHighlightEffect
+import mozilla.components.concept.menu.candidate.MenuCandidate
+import mozilla.components.concept.menu.candidate.MenuEffect
+import mozilla.components.concept.menu.candidate.MenuIcon
+import mozilla.components.concept.menu.candidate.MenuIconEffect
+import mozilla.components.concept.menu.candidate.NestedMenuCandidate
+import mozilla.components.concept.menu.candidate.RowMenuCandidate
+import mozilla.components.concept.menu.candidate.TextMenuCandidate
+
+private fun MenuIcon?.effect(): MenuIconEffect? =
+ if (this is DrawableMenuIcon) effect else null
+
+/**
+ * Find the effects used by the menu.
+ * Disabled and invisible menu items are not included.
+ */
+fun List<MenuCandidate>.effects(): Sequence<MenuEffect> = this.asSequence()
+ .filter { option -> option.containerStyle.isVisible && option.containerStyle.isEnabled }
+ .flatMap { option ->
+ when (option) {
+ is TextMenuCandidate ->
+ sequenceOf(option.effect, option.start.effect(), option.end.effect()).filterNotNull()
+ is CompoundMenuCandidate ->
+ sequenceOf(option.effect, option.start.effect()).filterNotNull()
+ is NestedMenuCandidate ->
+ sequenceOf(option.effect, option.start.effect(), option.end.effect()).filterNotNull() +
+ option.subMenuItems?.effects().orEmpty()
+ is RowMenuCandidate ->
+ option.items.asSequence()
+ .filter { it.containerStyle.isVisible && it.containerStyle.isEnabled }
+ .mapNotNull { it.icon.effect }
+ is DecorativeTextMenuCandidate, is DividerMenuCandidate -> emptySequence()
+ }
+ }
+
+/**
+ * Find a [NestedMenuCandidate] in the list with a matching [id].
+ */
+fun List<MenuCandidate>.findNestedMenuCandidate(id: Int): NestedMenuCandidate? = this.asSequence()
+ .mapNotNull { it as? NestedMenuCandidate }
+ .find { it.id == id }
+
+/**
+ * Select the highlight with the highest priority.
+ */
+fun Sequence<MenuEffect>.max() = maxByOrNull {
+ // Select the highlight with the highest priority
+ when (it) {
+ is HighPriorityHighlightEffect -> 2
+ is LowPriorityHighlightEffect -> 1
+ }
+}
diff --git a/mobile/android/android-components/components/concept/menu/src/test/java/mozilla/components/concept/menu/ext/MenuCandidateTest.kt b/mobile/android/android-components/components/concept/menu/src/test/java/mozilla/components/concept/menu/ext/MenuCandidateTest.kt
new file mode 100644
index 0000000000..5326143dd4
--- /dev/null
+++ b/mobile/android/android-components/components/concept/menu/src/test/java/mozilla/components/concept/menu/ext/MenuCandidateTest.kt
@@ -0,0 +1,179 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.concept.menu.ext
+
+import android.graphics.Color
+import mozilla.components.concept.menu.candidate.CompoundMenuCandidate
+import mozilla.components.concept.menu.candidate.ContainerStyle
+import mozilla.components.concept.menu.candidate.DecorativeTextMenuCandidate
+import mozilla.components.concept.menu.candidate.DividerMenuCandidate
+import mozilla.components.concept.menu.candidate.DrawableMenuIcon
+import mozilla.components.concept.menu.candidate.HighPriorityHighlightEffect
+import mozilla.components.concept.menu.candidate.LowPriorityHighlightEffect
+import mozilla.components.concept.menu.candidate.RowMenuCandidate
+import mozilla.components.concept.menu.candidate.SmallMenuCandidate
+import mozilla.components.concept.menu.candidate.TextMenuCandidate
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class MenuCandidateTest {
+
+ @Test
+ fun `higher priority items will be selected by max`() {
+ assertEquals(
+ HighPriorityHighlightEffect(Color.BLACK),
+ sequenceOf(
+ LowPriorityHighlightEffect(Color.BLUE),
+ HighPriorityHighlightEffect(Color.BLACK),
+ ).max(),
+ )
+ }
+
+ @Test
+ fun `items earlier in sequence will be selected by max`() {
+ assertEquals(
+ LowPriorityHighlightEffect(Color.BLUE),
+ sequenceOf(
+ LowPriorityHighlightEffect(Color.BLUE),
+ LowPriorityHighlightEffect(Color.YELLOW),
+ ).max(),
+ )
+ }
+
+ @Test
+ fun `effects returns effects from row candidate`() {
+ assertEquals(
+ listOf(
+ LowPriorityHighlightEffect(Color.BLUE),
+ LowPriorityHighlightEffect(Color.YELLOW),
+ ),
+ listOf(
+ RowMenuCandidate(
+ listOf(
+ SmallMenuCandidate(
+ "",
+ icon = DrawableMenuIcon(
+ null,
+ effect = LowPriorityHighlightEffect(Color.BLUE),
+ ),
+ ),
+ SmallMenuCandidate(
+ "",
+ icon = DrawableMenuIcon(
+ null,
+ effect = LowPriorityHighlightEffect(Color.RED),
+ ),
+ containerStyle = ContainerStyle(isVisible = false),
+ ),
+ SmallMenuCandidate(
+ "",
+ icon = DrawableMenuIcon(
+ null,
+ effect = LowPriorityHighlightEffect(Color.RED),
+ ),
+ containerStyle = ContainerStyle(isEnabled = false),
+ ),
+ SmallMenuCandidate(
+ "",
+ icon = DrawableMenuIcon(
+ null,
+ effect = LowPriorityHighlightEffect(Color.YELLOW),
+ ),
+ ),
+ ),
+ ),
+ ).effects().toList(),
+ )
+ }
+
+ @Test
+ fun `effects returns effects from text candidates`() {
+ assertEquals(
+ listOf(
+ HighPriorityHighlightEffect(Color.BLUE),
+ LowPriorityHighlightEffect(Color.YELLOW),
+ HighPriorityHighlightEffect(Color.BLACK),
+ HighPriorityHighlightEffect(Color.BLUE),
+ LowPriorityHighlightEffect(Color.RED),
+ ),
+ listOf(
+ TextMenuCandidate(
+ "",
+ start = DrawableMenuIcon(
+ null,
+ effect = LowPriorityHighlightEffect(Color.YELLOW),
+ ),
+ effect = HighPriorityHighlightEffect(Color.BLUE),
+ ),
+ DecorativeTextMenuCandidate(""),
+ TextMenuCandidate(""),
+ DividerMenuCandidate(),
+ TextMenuCandidate(
+ "",
+ effect = HighPriorityHighlightEffect(Color.BLACK),
+ ),
+ TextMenuCandidate(
+ "",
+ containerStyle = ContainerStyle(isVisible = false),
+ effect = HighPriorityHighlightEffect(Color.BLACK),
+ ),
+ TextMenuCandidate(
+ "",
+ end = DrawableMenuIcon(
+ null,
+ effect = LowPriorityHighlightEffect(Color.RED),
+ ),
+ effect = HighPriorityHighlightEffect(Color.BLUE),
+ ),
+ ).effects().toList(),
+ )
+ }
+
+ @Test
+ fun `effects returns effects from compound candidates`() {
+ assertEquals(
+ listOf(
+ HighPriorityHighlightEffect(Color.BLUE),
+ LowPriorityHighlightEffect(Color.YELLOW),
+ HighPriorityHighlightEffect(Color.BLACK),
+ LowPriorityHighlightEffect(Color.RED),
+ ),
+ listOf(
+ CompoundMenuCandidate(
+ "",
+ isChecked = true,
+ start = DrawableMenuIcon(
+ null,
+ effect = LowPriorityHighlightEffect(Color.YELLOW),
+ ),
+ end = CompoundMenuCandidate.ButtonType.CHECKBOX,
+ effect = HighPriorityHighlightEffect(Color.BLUE),
+ ),
+ CompoundMenuCandidate(
+ "",
+ isChecked = false,
+ end = CompoundMenuCandidate.ButtonType.SWITCH,
+ effect = HighPriorityHighlightEffect(Color.BLACK),
+ ),
+ CompoundMenuCandidate(
+ "",
+ isChecked = false,
+ end = CompoundMenuCandidate.ButtonType.SWITCH,
+ containerStyle = ContainerStyle(isEnabled = false),
+ effect = HighPriorityHighlightEffect(Color.BLACK),
+ ),
+ CompoundMenuCandidate(
+ "",
+ isChecked = true,
+ start = DrawableMenuIcon(
+ null,
+ effect = LowPriorityHighlightEffect(Color.RED),
+ ),
+ end = CompoundMenuCandidate.ButtonType.CHECKBOX,
+ ),
+ ).effects().toList(),
+ )
+ }
+}