Merge tag 'v94.1.2' into upstream-sync
commit
a77b05cdc7
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@ -1,36 +0,0 @@
|
|||||||
package org.mozilla.fenix.helpers.assertions
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import androidx.test.espresso.ViewAssertion
|
|
||||||
import mozilla.components.browser.awesomebar.BrowserAwesomeBar
|
|
||||||
|
|
||||||
class AwesomeBarAssertion {
|
|
||||||
companion object {
|
|
||||||
fun suggestionsAreGreaterThan(minimumSuggestions: Int): ViewAssertion {
|
|
||||||
return ViewAssertion { view, noViewFoundException ->
|
|
||||||
if (noViewFoundException != null) throw noViewFoundException
|
|
||||||
|
|
||||||
val suggestionsCount = getSuggestionCountFromView(view)
|
|
||||||
|
|
||||||
if (suggestionsCount <= minimumSuggestions)
|
|
||||||
throw AssertionError("The suggestion count is less than or equal to the minimum suggestions")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun suggestionsAreEqualTo(expectedItemCount: Int): ViewAssertion {
|
|
||||||
return ViewAssertion { view, noViewFoundException ->
|
|
||||||
if (noViewFoundException != null) throw noViewFoundException
|
|
||||||
|
|
||||||
val suggestionsCount = getSuggestionCountFromView(view)
|
|
||||||
|
|
||||||
if (suggestionsCount != expectedItemCount)
|
|
||||||
throw AssertionError("The expected item count is $expectedItemCount, and the suggestions count within the AwesomeBar is $suggestionsCount")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getSuggestionCountFromView(view: View): Int {
|
|
||||||
return (view as BrowserAwesomeBar).adapter?.itemCount
|
|
||||||
?: throw AssertionError("This view is not of type BrowserAwesomeBar")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
package="org.mozilla.fenix"
|
|
||||||
android:sharedUserId="${sharedUserId}">
|
|
||||||
<application
|
|
||||||
android:name="org.mozilla.fenix.MigratingFenixApplication"
|
|
||||||
tools:replace="android:name">
|
|
||||||
|
|
||||||
<activity android:name=".autofill.AutofillUnlockActivity"
|
|
||||||
android:exported="false"
|
|
||||||
android:theme="@style/Theme.AppCompat.Translucent" />
|
|
||||||
|
|
||||||
<activity android:name=".autofill.AutofillConfirmActivity"
|
|
||||||
android:exported="false"
|
|
||||||
android:theme="@style/Theme.AppCompat.Translucent" />
|
|
||||||
|
|
||||||
<activity android:name=".autofill.AutofillSearchActivity"
|
|
||||||
android:exported="false"
|
|
||||||
android:theme="@style/DialogActivityTheme" />
|
|
||||||
|
|
||||||
<service
|
|
||||||
android:name=".autofill.AutofillService"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.service.autofill.AutofillService"/>
|
|
||||||
</intent-filter>
|
|
||||||
</service>
|
|
||||||
|
|
||||||
<!-- Overriding the alias of the main manifest to route app launches through our
|
|
||||||
MigrationDecisionActivity which will show the migration screen before launching
|
|
||||||
into the app if needed. -->
|
|
||||||
<activity-alias
|
|
||||||
android:name="${applicationId}.App"
|
|
||||||
android:targetActivity="org.mozilla.fenix.MigrationDecisionActivity"
|
|
||||||
tools:replace="android:targetActivity" />
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name="org.mozilla.fenix.MigrationDecisionActivity"
|
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
<service android:name="org.mozilla.fenix.MigrationService" />
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
@ -0,0 +1,81 @@
|
|||||||
|
/* 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 org.mozilla.fenix.android
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.ColorDrawable
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import androidx.appcompat.app.AppCompatDialogFragment
|
||||||
|
import androidx.appcompat.view.ContextThemeWrapper
|
||||||
|
import com.google.android.material.R
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||||
|
import org.mozilla.fenix.HomeActivity
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base [AppCompatDialogFragment] that adds behaviour to create a top or bottom dialog.
|
||||||
|
*/
|
||||||
|
abstract class FenixDialogFragment : AppCompatDialogFragment() {
|
||||||
|
/**
|
||||||
|
* Indicates the position of the dialog top or bottom.
|
||||||
|
*/
|
||||||
|
abstract val gravity: Int
|
||||||
|
/**
|
||||||
|
* The layout id that will be render on the dialog.
|
||||||
|
*/
|
||||||
|
abstract val layoutId: Int
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
return if (gravity == Gravity.BOTTOM) {
|
||||||
|
BottomSheetDialog(requireContext(), this.theme).apply {
|
||||||
|
setOnShowListener {
|
||||||
|
val bottomSheet =
|
||||||
|
findViewById<View>(R.id.design_bottom_sheet) as FrameLayout
|
||||||
|
val behavior = BottomSheetBehavior.from(bottomSheet)
|
||||||
|
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Dialog(requireContext()).applyCustomizationsForTopDialog(inflateRootView())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Dialog.applyCustomizationsForTopDialog(rootView: View): Dialog {
|
||||||
|
addContentView(
|
||||||
|
rootView,
|
||||||
|
LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
window?.apply {
|
||||||
|
setGravity(gravity)
|
||||||
|
setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||||
|
// This must be called after addContentView, or it won't fully fill to the edge.
|
||||||
|
setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun inflateRootView(container: ViewGroup? = null): View {
|
||||||
|
val contextThemeWrapper = ContextThemeWrapper(
|
||||||
|
activity,
|
||||||
|
(activity as HomeActivity).themeManager.currentThemeResource
|
||||||
|
)
|
||||||
|
return LayoutInflater.from(contextThemeWrapper).inflate(
|
||||||
|
layoutId,
|
||||||
|
container,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,102 @@
|
|||||||
|
/* 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 org.mozilla.fenix.compose
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.text.ClickableText
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import mozilla.components.ui.colors.PhotonColors
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Text] containing a substring styled as an URL informing when this is clicked.
|
||||||
|
*
|
||||||
|
* @param text Full text that will be displayed
|
||||||
|
* @param textColor [Color] of the normal text. The URL substring will have a default URL style applied.
|
||||||
|
* @param clickableStartIndex [text] index at which the URL substring starts.
|
||||||
|
* @param clickableEndIndex [text] index at which the URL substring ends.
|
||||||
|
* @param onClick Callback to be invoked only when the URL substring is clicked.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun ClickableSubstringLink(
|
||||||
|
text: String,
|
||||||
|
textColor: Color,
|
||||||
|
clickableStartIndex: Int,
|
||||||
|
clickableEndIndex: Int,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val annotatedText = buildAnnotatedString {
|
||||||
|
append(text)
|
||||||
|
|
||||||
|
addStyle(
|
||||||
|
SpanStyle(textColor),
|
||||||
|
start = 0,
|
||||||
|
end = clickableStartIndex
|
||||||
|
)
|
||||||
|
|
||||||
|
addStyle(
|
||||||
|
SpanStyle(
|
||||||
|
color = when (isSystemInDarkTheme()) {
|
||||||
|
true -> PhotonColors.Violet40
|
||||||
|
false -> PhotonColors.Violet70
|
||||||
|
}
|
||||||
|
),
|
||||||
|
start = clickableStartIndex,
|
||||||
|
end = clickableEndIndex
|
||||||
|
)
|
||||||
|
|
||||||
|
addStyle(
|
||||||
|
SpanStyle(textColor),
|
||||||
|
start = clickableEndIndex,
|
||||||
|
end = text.length
|
||||||
|
)
|
||||||
|
|
||||||
|
addStyle(
|
||||||
|
SpanStyle(fontSize = 12.sp),
|
||||||
|
start = 0,
|
||||||
|
end = clickableEndIndex
|
||||||
|
)
|
||||||
|
|
||||||
|
addStringAnnotation(
|
||||||
|
tag = "link",
|
||||||
|
annotation = "",
|
||||||
|
start = clickableStartIndex,
|
||||||
|
end = clickableEndIndex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ClickableText(
|
||||||
|
text = annotatedText,
|
||||||
|
onClick = {
|
||||||
|
annotatedText
|
||||||
|
.getStringAnnotations("link", it, it)
|
||||||
|
.firstOrNull()?.let {
|
||||||
|
onClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
private fun ClickableSubstringTextPreview() {
|
||||||
|
val text = "This text contains a link"
|
||||||
|
Box(modifier = Modifier.background(PhotonColors.White)) {
|
||||||
|
ClickableSubstringLink(
|
||||||
|
text,
|
||||||
|
PhotonColors.DarkGrey90,
|
||||||
|
text.indexOf("link"),
|
||||||
|
text.length
|
||||||
|
) { }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
/* 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 org.mozilla.fenix.compose
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import mozilla.components.support.images.compose.loader.ImageLoader
|
||||||
|
import mozilla.components.support.images.compose.loader.WithImage
|
||||||
|
import org.mozilla.fenix.components.components
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A composable that lays out and draws the image from a given URL while showing a default placeholder
|
||||||
|
* while that image is downloaded or a default fallback image when downloading failed.
|
||||||
|
*
|
||||||
|
* @param url URL from where the to download the image to be shown.
|
||||||
|
* @param modifier [Modifier] to be applied to the layout.
|
||||||
|
* @param private Whether or not this is a private request. Like in private browsing mode,
|
||||||
|
* private requests will not cache anything on disk and not send any cookies shared with the browser.
|
||||||
|
* @param targetSize Image size (width and height) the loaded image should be scaled to.
|
||||||
|
* @param contentDescription Localized text used by accessibility services to describe what this image represents.
|
||||||
|
* This should always be provided unless this image is used for decorative purposes, and does not represent
|
||||||
|
* a meaningful action that a user can take.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
@Suppress("LongParameterList")
|
||||||
|
fun Image(
|
||||||
|
url: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
private: Boolean = false,
|
||||||
|
targetSize: Dp = 100.dp,
|
||||||
|
contentDescription: String? = null
|
||||||
|
) {
|
||||||
|
ImageLoader(
|
||||||
|
url = url,
|
||||||
|
client = components.core.client,
|
||||||
|
private = private,
|
||||||
|
targetSize = targetSize
|
||||||
|
) {
|
||||||
|
WithImage { painter ->
|
||||||
|
androidx.compose.foundation.Image(
|
||||||
|
painter = painter,
|
||||||
|
modifier = modifier,
|
||||||
|
contentDescription = contentDescription,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
WithDefaultPlaceholder(modifier, contentDescription)
|
||||||
|
|
||||||
|
WithDefaultFallback(modifier, contentDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
private fun ImagePreview() {
|
||||||
|
Image(
|
||||||
|
"https://mozilla.com",
|
||||||
|
Modifier.height(100.dp).width(200.dp)
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,87 @@
|
|||||||
|
/* 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 org.mozilla.fenix.compose
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.painter.ColorPainter
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import mozilla.components.support.images.compose.loader.Fallback
|
||||||
|
import mozilla.components.support.images.compose.loader.ImageLoaderScope
|
||||||
|
import mozilla.components.support.images.compose.loader.Placeholder
|
||||||
|
import mozilla.components.ui.colors.PhotonColors
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the app default image placeholder while the image is still getting loaded.
|
||||||
|
*
|
||||||
|
* @param modifier [Modifier] allowing to control among others the dimensions and shape of the image.
|
||||||
|
* @param contentDescription Text provided to accessibility services to describe what this image represents.
|
||||||
|
* Defaults to [null] suited for an image used only for decorative purposes and not to be read by
|
||||||
|
* accessibility services.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
internal fun ImageLoaderScope.WithDefaultPlaceholder(
|
||||||
|
modifier: Modifier,
|
||||||
|
contentDescription: String? = null
|
||||||
|
) {
|
||||||
|
Placeholder {
|
||||||
|
DefaultImagePlaceholder(modifier, contentDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the app default image placeholder if loading the image failed.
|
||||||
|
*
|
||||||
|
* @param modifier [Modifier] allowing to control among others the dimensions and shape of the image.
|
||||||
|
* @param contentDescription Text provided to accessibility services to describe what this image represents.
|
||||||
|
* Defaults to [null] suited for an image used only for decorative purposes and not to be read by
|
||||||
|
* accessibility services.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
internal fun ImageLoaderScope.WithDefaultFallback(
|
||||||
|
modifier: Modifier,
|
||||||
|
contentDescription: String? = null
|
||||||
|
) {
|
||||||
|
Fallback {
|
||||||
|
DefaultImagePlaceholder(modifier, contentDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Application default image placeholder.
|
||||||
|
*
|
||||||
|
* @param modifier [Modifier] allowing to control among others the dimensions and shape of the image.
|
||||||
|
* @param contentDescription Text provided to accessibility services to describe what this image represents.
|
||||||
|
* Defaults to [null] suited for an image used only for decorative purposes and not to be read by
|
||||||
|
* accessibility services.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
internal fun DefaultImagePlaceholder(
|
||||||
|
modifier: Modifier,
|
||||||
|
contentDescription: String? = null
|
||||||
|
) {
|
||||||
|
val color = when (isSystemInDarkTheme()) {
|
||||||
|
true -> PhotonColors.DarkGrey30
|
||||||
|
false -> PhotonColors.LightGrey30
|
||||||
|
}
|
||||||
|
|
||||||
|
Image(ColorPainter(color), contentDescription, modifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
private fun DefaultImagePlaceholderPreview() {
|
||||||
|
DefaultImagePlaceholder(
|
||||||
|
Modifier
|
||||||
|
.size(200.dp, 100.dp)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
)
|
||||||
|
}
|
@ -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 org.mozilla.fenix.compose
|
||||||
|
|
||||||
|
import androidx.compose.foundation.gestures.FlingBehavior
|
||||||
|
import androidx.compose.foundation.gestures.ScrollScope
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [FlingBehavior] for a [LazyRow] that will automatically scroll the list in the fling direction
|
||||||
|
* to fully show the next item.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun EagerFlingBehavior(
|
||||||
|
lazyRowState: LazyListState
|
||||||
|
): FlingBehavior {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
return LazyListEagerFlingBehavior(lazyRowState, scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class LazyListEagerFlingBehavior(
|
||||||
|
private val lazyRowState: LazyListState,
|
||||||
|
private val scope: CoroutineScope
|
||||||
|
) : FlingBehavior {
|
||||||
|
override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
|
||||||
|
val firstItemIndex = lazyRowState.firstVisibleItemIndex
|
||||||
|
|
||||||
|
val itemIndexToScrollTo = when (initialVelocity <= 0) {
|
||||||
|
true -> firstItemIndex
|
||||||
|
false -> firstItemIndex + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
lazyRowState.animateScrollToItem(itemIndexToScrollTo)
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0f // we've consumed the entire fling
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,148 @@
|
|||||||
|
/* 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 org.mozilla.fenix.compose
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.Card
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import org.mozilla.fenix.theme.FirefoxTheme
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default layout of a large tab shown in a list taking String arguments for title and caption.
|
||||||
|
* Has the following structure:
|
||||||
|
* ```
|
||||||
|
* ---------------------------------------------
|
||||||
|
* | -------------- Title |
|
||||||
|
* | | Image | wrapped on |
|
||||||
|
* | | from | three rows if needed |
|
||||||
|
* | | imageUrl | |
|
||||||
|
* | -------------- Optional caption |
|
||||||
|
* ---------------------------------------------
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param imageUrl URL from where the to download a header image of the tab this composable renders.
|
||||||
|
* @param title Title off the tab this composable renders.
|
||||||
|
* @param caption Optional caption text.
|
||||||
|
* @param onClick Optional callback to be invoked when this composable is clicked.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun ListItemTabLarge(
|
||||||
|
imageUrl: String,
|
||||||
|
title: String,
|
||||||
|
caption: String? = null,
|
||||||
|
onClick: (() -> Unit)? = null
|
||||||
|
) {
|
||||||
|
ListItemTabSurface(imageUrl, onClick) {
|
||||||
|
TabTitle(text = title, maxLines = 3)
|
||||||
|
|
||||||
|
if (caption != null) {
|
||||||
|
TabSubtitle(text = caption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default layout of a large tab shown in a list taking composable arguments for title and caption
|
||||||
|
* allowing as an exception to customize these elements.
|
||||||
|
* Has the following structure:
|
||||||
|
* ```
|
||||||
|
* ---------------------------------------------
|
||||||
|
* | -------------- -------------------------- |
|
||||||
|
* | | | | Title | |
|
||||||
|
* | | Image | | composable | |
|
||||||
|
* | | from | -------------------------- |
|
||||||
|
* | | imageUrl | -------------------------- |
|
||||||
|
* | | | | Optional composable | |
|
||||||
|
* | -------------- -------------------------- |
|
||||||
|
* ---------------------------------------------
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param imageUrl URL from where the to download a header image of the tab this composable renders.
|
||||||
|
* @param title Composable rendering the title of the tab this composable represents.
|
||||||
|
* @param subtitle Optional tab caption composable.
|
||||||
|
* @param onClick Optional callback to be invoked when this composable is clicked.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun ListItemTabLarge(
|
||||||
|
imageUrl: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
title: @Composable () -> Unit,
|
||||||
|
subtitle: @Composable (() -> Unit)? = null
|
||||||
|
) {
|
||||||
|
ListItemTabSurface(imageUrl, onClick) {
|
||||||
|
title()
|
||||||
|
|
||||||
|
subtitle?.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared default configuration of a ListItemTabLarge Composable.
|
||||||
|
*
|
||||||
|
* @param imageUrl URL from where the to download a header image of the tab this composable renders.
|
||||||
|
* @param onClick Optional callback to be invoked when this composable is clicked.
|
||||||
|
* @param tabDetails [Composable] Displayed to the the end of the image. Allows for variation in the item text style.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun ListItemTabSurface(
|
||||||
|
imageUrl: String,
|
||||||
|
onClick: (() -> Unit)? = null,
|
||||||
|
tabDetails: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
var modifier = Modifier.size(328.dp, 116.dp)
|
||||||
|
if (onClick != null) modifier = modifier.then(Modifier.clickable { onClick() })
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = modifier,
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
backgroundColor = FirefoxTheme.colors.surface,
|
||||||
|
elevation = 6.dp
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
) {
|
||||||
|
val (imageWidth, imageHeight) = 116.dp to 84.dp
|
||||||
|
val imageModifier = Modifier
|
||||||
|
.size(imageWidth, imageHeight)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
|
||||||
|
Image(imageUrl, imageModifier, false, imageWidth)
|
||||||
|
|
||||||
|
Spacer(Modifier.width(16.dp))
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
verticalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
tabDetails()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
private fun ListItemTabLargePreview() {
|
||||||
|
FirefoxTheme {
|
||||||
|
ListItemTabLarge(
|
||||||
|
imageUrl = "",
|
||||||
|
title = "This is a very long title for a tab but needs to be so for this preview",
|
||||||
|
caption = "And this is a caption"
|
||||||
|
) { }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
/* 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 org.mozilla.fenix.compose
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.Card
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
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.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import org.mozilla.fenix.theme.FirefoxTheme
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Placeholder of a [ListItemTabLarge] with the same dimensions but only a centered text.
|
||||||
|
* Has the following structure:
|
||||||
|
* ```
|
||||||
|
* ---------------------------------------------
|
||||||
|
* | |
|
||||||
|
* | |
|
||||||
|
* | Placeholder text |
|
||||||
|
* | |
|
||||||
|
* | |
|
||||||
|
* ---------------------------------------------
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param text The only [String] that this will display.
|
||||||
|
* @param onClick Optional callback to be invoked when this composable is clicked.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun ListItemTabLargePlaceholder(
|
||||||
|
text: String,
|
||||||
|
onClick: () -> Unit = { }
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(328.dp, 116.dp)
|
||||||
|
.clickable { onClick() },
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
backgroundColor = FirefoxTheme.colors.surface,
|
||||||
|
elevation = 6.dp,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
color = FirefoxTheme.colors.textPrimary,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
maxLines = 1,
|
||||||
|
style = TextStyle(fontSize = 20.sp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
private fun ListItemTabLargePlaceholderPreview() {
|
||||||
|
FirefoxTheme {
|
||||||
|
ListItemTabLargePlaceholder(text = "Item placeholder")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
/* 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 org.mozilla.fenix.compose
|
||||||
|
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.Font
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.theme.FirefoxTheme
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default layout for the header of a screen section.
|
||||||
|
*
|
||||||
|
* @param text [String] to be styled as header and displayed.
|
||||||
|
* @param modifier [Modifier] to be applied to the [Text].
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun SectionHeader(
|
||||||
|
text: String,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = modifier,
|
||||||
|
text = text,
|
||||||
|
style = TextStyle(
|
||||||
|
fontFamily = FontFamily(Font(R.font.metropolis_semibold)),
|
||||||
|
fontSize = 20.sp,
|
||||||
|
lineHeight = 20.sp
|
||||||
|
),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
color = FirefoxTheme.colors.textPrimary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default layout for the header of a screen section.
|
||||||
|
*
|
||||||
|
* @param text [String] to be styled as header and displayed.
|
||||||
|
* @param modifier [Modifier] to be applied to the [Text].
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun HomeSectionHeader(
|
||||||
|
text: String,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = modifier,
|
||||||
|
text = text,
|
||||||
|
style = TextStyle(
|
||||||
|
fontFamily = FontFamily(Font(R.font.metropolis_semibold)),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
lineHeight = 20.sp
|
||||||
|
),
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
color = FirefoxTheme.colors.textPrimary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
private fun HeadingTextPreview() {
|
||||||
|
SectionHeader(text = "Section title")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
private fun HomeHeadingTextPreview() {
|
||||||
|
HomeSectionHeader(text = "Home section title")
|
||||||
|
}
|
@ -0,0 +1,77 @@
|
|||||||
|
/* 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 org.mozilla.fenix.compose
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.selection.selectable
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.capitalize
|
||||||
|
import androidx.compose.ui.text.intl.Locale
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import mozilla.components.ui.colors.PhotonColors
|
||||||
|
import org.mozilla.fenix.theme.FirefoxTheme
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default layout of a selectable chip.
|
||||||
|
*
|
||||||
|
* @param text [String] displayed in this chip. Ideally should only be one word.
|
||||||
|
* @param isSelected Whether this should be shown as selected.
|
||||||
|
* @param onClick Callback for when the user taps this.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun SelectableChip(
|
||||||
|
text: String,
|
||||||
|
isSelected: Boolean,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val contentColor = when (isSystemInDarkTheme()) {
|
||||||
|
true -> PhotonColors.LightGrey10
|
||||||
|
false -> if (isSelected) PhotonColors.LightGrey10 else PhotonColors.DarkGrey90
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MagicNumber")
|
||||||
|
val backgroundColor = when (isSystemInDarkTheme()) {
|
||||||
|
true -> if (isSelected) PhotonColors.Violet50 else PhotonColors.DarkGrey50
|
||||||
|
// Custom color codes matching the Figma design.
|
||||||
|
false -> if (isSelected) { Color(0xFF312A65) } else { Color(0x1420123A) }
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.selectable(isSelected) { onClick() }
|
||||||
|
.clip(MaterialTheme.shapes.small)
|
||||||
|
.background(backgroundColor)
|
||||||
|
.padding(16.dp, 10.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text.capitalize(Locale.current),
|
||||||
|
style = TextStyle(fontSize = 14.sp),
|
||||||
|
color = contentColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
private fun SelectableChipPreview() {
|
||||||
|
FirefoxTheme {
|
||||||
|
Box(Modifier.fillMaxSize().background(FirefoxTheme.colors.surface)) {
|
||||||
|
SelectableChip("Chirp", false) { }
|
||||||
|
SelectableChip(text = "Chirp", isSelected = true) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,137 @@
|
|||||||
|
/* 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 org.mozilla.fenix.compose
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.Layout
|
||||||
|
import androidx.compose.ui.layout.Placeable
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import org.mozilla.fenix.theme.FirefoxTheme
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a list of items as a staggered horizontal grid placing them on ltr rows and continuing
|
||||||
|
* on as many below rows as needed to place all items.
|
||||||
|
*
|
||||||
|
* In an effort to best utilize the available row space this can mix the items such that narrower ones
|
||||||
|
* are placed on the same row as wider ones if the otherwise next item doesn't fit.
|
||||||
|
*
|
||||||
|
* @param modifier [Modifier] to be applied to the layout.
|
||||||
|
* @param horizontalItemsSpacing Minimum horizontal space between items. Does not add spacing to layout bounds.
|
||||||
|
* @param verticalItemsSpacing Vertical space between items
|
||||||
|
* @param arrangement How the items will be horizontally aligned and spaced.
|
||||||
|
* @param content The children composables to be laid out.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun StaggeredHorizontalGrid(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
horizontalItemsSpacing: Dp = 0.dp,
|
||||||
|
verticalItemsSpacing: Dp = 8.dp,
|
||||||
|
arrangement: Arrangement.Horizontal = Arrangement.Start,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
val currentLayoutDirection = LocalLayoutDirection.current
|
||||||
|
|
||||||
|
Layout(content, modifier) { items, constraints ->
|
||||||
|
val horizontalItemsSpacingPixels = horizontalItemsSpacing.roundToPx()
|
||||||
|
val verticalItemsSpacingPixels = verticalItemsSpacing.roundToPx()
|
||||||
|
var totalHeight = 0
|
||||||
|
val itemsRows = mutableListOf<List<Placeable>>()
|
||||||
|
val notYetPlacedItems = items.map {
|
||||||
|
it.measure(constraints)
|
||||||
|
}.toMutableList()
|
||||||
|
|
||||||
|
fun getIndexOfNextPlaceableThatFitsRow(available: List<Placeable>, currentWidth: Int): Int {
|
||||||
|
return available.indexOfFirst {
|
||||||
|
currentWidth + it.width <= constraints.maxWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate each row with as many items as possible combining wider with narrower items.
|
||||||
|
// This will change the order of shown categories.
|
||||||
|
var (currentRow, currentWidth) = mutableListOf<Placeable>() to 0
|
||||||
|
while (notYetPlacedItems.isNotEmpty()) {
|
||||||
|
if (currentRow.isEmpty()) {
|
||||||
|
currentRow.add(
|
||||||
|
notYetPlacedItems[0].also {
|
||||||
|
currentWidth += it.width + horizontalItemsSpacingPixels
|
||||||
|
totalHeight += it.height + verticalItemsSpacingPixels
|
||||||
|
}
|
||||||
|
)
|
||||||
|
notYetPlacedItems.removeAt(0)
|
||||||
|
} else {
|
||||||
|
val nextPlaceableThatFitsIndex = getIndexOfNextPlaceableThatFitsRow(notYetPlacedItems, currentWidth)
|
||||||
|
if (nextPlaceableThatFitsIndex >= 0) {
|
||||||
|
currentRow.add(
|
||||||
|
notYetPlacedItems[nextPlaceableThatFitsIndex].also {
|
||||||
|
currentWidth += it.width + horizontalItemsSpacingPixels
|
||||||
|
}
|
||||||
|
)
|
||||||
|
notYetPlacedItems.removeAt(nextPlaceableThatFitsIndex)
|
||||||
|
} else {
|
||||||
|
itemsRows.add(currentRow)
|
||||||
|
currentRow = mutableListOf()
|
||||||
|
currentWidth = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentRow.isNotEmpty()) {
|
||||||
|
itemsRows.add(currentRow)
|
||||||
|
}
|
||||||
|
totalHeight -= verticalItemsSpacingPixels
|
||||||
|
|
||||||
|
// Place each item from each row on screen.
|
||||||
|
layout(constraints.maxWidth, totalHeight) {
|
||||||
|
itemsRows.forEachIndexed { rowIndex, itemRow ->
|
||||||
|
val itemsSizes = IntArray(itemRow.size) {
|
||||||
|
itemRow[it].width + when (currentLayoutDirection == LayoutDirection.Ltr) {
|
||||||
|
true -> if (it < itemRow.lastIndex) horizontalItemsSpacingPixels else 0
|
||||||
|
false -> if (it > 0) horizontalItemsSpacingPixels else 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val itemsPositions = IntArray(itemsSizes.size) { 0 }
|
||||||
|
with(arrangement) {
|
||||||
|
arrange(constraints.maxWidth, itemsSizes, currentLayoutDirection, itemsPositions)
|
||||||
|
}
|
||||||
|
|
||||||
|
itemRow.forEachIndexed { itemIndex, item ->
|
||||||
|
item.place(
|
||||||
|
x = itemsPositions[itemIndex],
|
||||||
|
y = (rowIndex * item.height) + (rowIndex * verticalItemsSpacingPixels)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
private fun StaggeredHorizontalGridPreview() {
|
||||||
|
FirefoxTheme {
|
||||||
|
Box(Modifier.background(FirefoxTheme.colors.surface)) {
|
||||||
|
StaggeredHorizontalGrid(
|
||||||
|
horizontalItemsSpacing = 8.dp,
|
||||||
|
arrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor"
|
||||||
|
.split(" ")
|
||||||
|
.forEach {
|
||||||
|
Text(text = it, color = Color.Red, modifier = Modifier.border(3.dp, Color.Blue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
/* 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 org.mozilla.fenix.compose
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import org.mozilla.fenix.theme.FirefoxTheme
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default layout for a tab composable caption.
|
||||||
|
*
|
||||||
|
* @param text Tab caption.
|
||||||
|
* @param modifier Optional [Modifier] to be applied to the layout.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun TabSubtitle(
|
||||||
|
text: String,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = modifier,
|
||||||
|
maxLines = 1,
|
||||||
|
text = text,
|
||||||
|
style = TextStyle(fontSize = 12.sp),
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
color = FirefoxTheme.colors.textSecondary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
private fun TabSubtitlePreview() {
|
||||||
|
FirefoxTheme {
|
||||||
|
Box(Modifier.background(FirefoxTheme.colors.surface)) {
|
||||||
|
TabSubtitle(
|
||||||
|
"Awesome tab subtitle",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,95 @@
|
|||||||
|
/* 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 org.mozilla.fenix.compose
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.Layout
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import org.mozilla.fenix.theme.FirefoxTheme
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Special caption text for a tab layout shown on one line.
|
||||||
|
*
|
||||||
|
* This will combine [firstText] with a interdot and then [secondText] ensuring that the second text
|
||||||
|
* (which is assumed to be smaller) always fills as much space as needed with the [firstText] automatically
|
||||||
|
* being resized to be smaller with an added ellipsis characters if needed.
|
||||||
|
*
|
||||||
|
* Possible results:
|
||||||
|
* ```
|
||||||
|
* - when both texts would fit the screen
|
||||||
|
* ------------------------------------------
|
||||||
|
* |firstText · secondText |
|
||||||
|
* ------------------------------------------
|
||||||
|
*
|
||||||
|
* - when both text do not fit, second is shown in entirety, first is ellipsised.
|
||||||
|
* ------------------------------------------
|
||||||
|
* |longerFirstTextOrSmallSc... · secondText|
|
||||||
|
* ------------------------------------------
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param firstText Text shown at the start of the row.
|
||||||
|
* @param secondText Text shown at the end of the row.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun TabSubtitleWithInterdot(
|
||||||
|
firstText: String,
|
||||||
|
secondText: String,
|
||||||
|
) {
|
||||||
|
val currentLayoutDirection = LocalLayoutDirection.current
|
||||||
|
|
||||||
|
Layout(
|
||||||
|
content = {
|
||||||
|
TabSubtitle(text = firstText)
|
||||||
|
TabSubtitle(text = " \u00b7 ")
|
||||||
|
TabSubtitle(text = secondText)
|
||||||
|
}
|
||||||
|
) { items, constraints ->
|
||||||
|
|
||||||
|
// We need to measure from the end to start to ensure the secondItem will always be on screen
|
||||||
|
// and depending on secondItem's width and interdot's width the firstItem is automatically resized.
|
||||||
|
val secondItem = items[2].measure(constraints)
|
||||||
|
val interdot = items[1].measure(
|
||||||
|
constraints.copy(maxWidth = constraints.maxWidth - secondItem.width)
|
||||||
|
)
|
||||||
|
val firstItem = items[0].measure(
|
||||||
|
constraints.copy(maxWidth = constraints.maxWidth - secondItem.width - interdot.width)
|
||||||
|
)
|
||||||
|
|
||||||
|
layout(constraints.maxWidth, constraints.maxHeight) {
|
||||||
|
val itemsPositions = IntArray(items.size)
|
||||||
|
with(Arrangement.Start) {
|
||||||
|
arrange(
|
||||||
|
constraints.maxWidth,
|
||||||
|
intArrayOf(firstItem.width, interdot.width, secondItem.width),
|
||||||
|
currentLayoutDirection,
|
||||||
|
itemsPositions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val placementHeight = constraints.maxHeight - firstItem.height
|
||||||
|
listOf(firstItem, interdot, secondItem).forEachIndexed { index, item ->
|
||||||
|
item.place(itemsPositions[index], placementHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
private fun TabSubtitleWithInterdotPreview() {
|
||||||
|
FirefoxTheme {
|
||||||
|
Box(Modifier.background(FirefoxTheme.colors.surface)) {
|
||||||
|
TabSubtitleWithInterdot(
|
||||||
|
firstText = "firstText",
|
||||||
|
secondText = "secondText",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
/* 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 org.mozilla.fenix.compose
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import org.mozilla.fenix.theme.FirefoxTheme
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default layout for a tab composable title.
|
||||||
|
*
|
||||||
|
* @param text Tab title
|
||||||
|
* @param maxLines Maximum number of lines for [text] to span, wrapping if necessary.
|
||||||
|
* If the text exceeds the given number of lines it will be ellipsized.
|
||||||
|
* @param modifier Optional [Modifier] to be applied to the layout.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun TabTitle(
|
||||||
|
text: String,
|
||||||
|
maxLines: Int,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = modifier,
|
||||||
|
maxLines = maxLines,
|
||||||
|
text = text,
|
||||||
|
style = TextStyle(fontSize = 14.sp),
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
color = FirefoxTheme.colors.textPrimary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TabTitlePreview() {
|
||||||
|
FirefoxTheme {
|
||||||
|
Box(Modifier.background(FirefoxTheme.colors.surface)) {
|
||||||
|
TabTitle(
|
||||||
|
"Awesome tab title",
|
||||||
|
2
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue