[Tooltip] Adding caret feature to rich tooltip variant
Adding an enum that determines what type of caret is drawn in drawCaretWithPath(). Updating the rich tooltip positioning logic to allow for carets to be drawn.
Test: Adding a caret-anchor positioning test for rich tooltip. Change assert to use .isWithin().of() for a more flexible test.
Bug: 297037973
Relnote: Adding a default caret for rich tooltip, new rich tooltip API now allows for custom caret to be drawn given anchor LayoutCoordinates.
Change-Id: Ifd42c2be34f72060cccce6414e28c1b2c01e025a
diff --git a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/TooltipBenchmark.kt b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/TooltipBenchmark.kt
index 765d502..d0095e9 100644
--- a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/TooltipBenchmark.kt
+++ b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/TooltipBenchmark.kt
@@ -127,7 +127,7 @@
}
@Composable
- private fun RichTooltipTest() {
+ private fun CaretScope.RichTooltipTest() {
RichTooltip(
title = { Text("Subhead") },
action = {
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index 5cad6eb..0d4ebce 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -1965,7 +1965,6 @@
}
public final class TooltipKt {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void RichTooltip(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit>? action, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.RichTooltipColors colors, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> text);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void TooltipBox(androidx.compose.ui.window.PopupPositionProvider positionProvider, kotlin.jvm.functions.Function1<? super androidx.compose.material3.CaretScope,kotlin.Unit> tooltip, androidx.compose.material3.TooltipState state, optional androidx.compose.ui.Modifier modifier, optional boolean focusable, optional boolean enableUserInput, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.material3.TooltipState TooltipState(optional boolean initialIsVisible, optional boolean isPersistent, optional androidx.compose.foundation.MutatorMutex mutatorMutex);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.TooltipState rememberTooltipState(optional boolean initialIsVisible, optional boolean isPersistent, optional androidx.compose.foundation.MutatorMutex mutatorMutex);
@@ -1978,6 +1977,7 @@
public final class Tooltip_androidKt {
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void PlainTooltip(androidx.compose.material3.CaretScope, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.CaretProperties? caretProperties, optional androidx.compose.ui.graphics.Shape shape, optional long contentColor, optional long containerColor, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void RichTooltip(androidx.compose.material3.CaretScope, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit>? action, optional androidx.compose.material3.CaretProperties? caretProperties, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.RichTooltipColors colors, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> text);
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class TopAppBarColors {
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index 5cad6eb..0d4ebce 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -1965,7 +1965,6 @@
}
public final class TooltipKt {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void RichTooltip(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit>? action, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.RichTooltipColors colors, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> text);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void TooltipBox(androidx.compose.ui.window.PopupPositionProvider positionProvider, kotlin.jvm.functions.Function1<? super androidx.compose.material3.CaretScope,kotlin.Unit> tooltip, androidx.compose.material3.TooltipState state, optional androidx.compose.ui.Modifier modifier, optional boolean focusable, optional boolean enableUserInput, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.material3.TooltipState TooltipState(optional boolean initialIsVisible, optional boolean isPersistent, optional androidx.compose.foundation.MutatorMutex mutatorMutex);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.TooltipState rememberTooltipState(optional boolean initialIsVisible, optional boolean isPersistent, optional androidx.compose.foundation.MutatorMutex mutatorMutex);
@@ -1978,6 +1977,7 @@
public final class Tooltip_androidKt {
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void PlainTooltip(androidx.compose.material3.CaretScope, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.CaretProperties? caretProperties, optional androidx.compose.ui.graphics.Shape shape, optional long contentColor, optional long containerColor, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void RichTooltip(androidx.compose.material3.CaretScope, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit>? action, optional androidx.compose.material3.CaretProperties? caretProperties, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.RichTooltipColors colors, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> text);
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class TopAppBarColors {
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
index f90bda5..4b9f5d2 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
@@ -105,6 +105,7 @@
import androidx.compose.material3.samples.PinnedTopAppBar
import androidx.compose.material3.samples.PlainTooltipSample
import androidx.compose.material3.samples.PlainTooltipWithCaret
+import androidx.compose.material3.samples.PlainTooltipWithCustomCaret
import androidx.compose.material3.samples.PlainTooltipWithManualInvocationSample
import androidx.compose.material3.samples.PrimaryIconTabs
import androidx.compose.material3.samples.PrimaryTextTabs
@@ -116,6 +117,8 @@
import androidx.compose.material3.samples.RangeSliderSample
import androidx.compose.material3.samples.RangeSliderWithCustomComponents
import androidx.compose.material3.samples.RichTooltipSample
+import androidx.compose.material3.samples.RichTooltipWithCaretSample
+import androidx.compose.material3.samples.RichTooltipWithCustomCaretSample
import androidx.compose.material3.samples.RichTooltipWithManualInvocationSample
import androidx.compose.material3.samples.ScaffoldWithCoroutinesSnackbar
import androidx.compose.material3.samples.ScaffoldWithCustomSnackbar
@@ -1231,6 +1234,13 @@
PlainTooltipWithCaret()
},
Example(
+ name = ::PlainTooltipWithCustomCaret.name,
+ description = TooltipsExampleDescription,
+ sourceUrl = TooltipsExampleSourceUrl
+ ) {
+ PlainTooltipWithCustomCaret()
+ },
+ Example(
name = ::RichTooltipSample.name,
description = TooltipsExampleDescription,
sourceUrl = TooltipsExampleSourceUrl
@@ -1243,5 +1253,19 @@
sourceUrl = TooltipsExampleSourceUrl
) {
RichTooltipWithManualInvocationSample()
+ },
+ Example(
+ name = ::RichTooltipWithCaretSample.name,
+ description = TooltipsExampleDescription,
+ sourceUrl = TooltipsExampleSourceUrl
+ ) {
+ RichTooltipWithCaretSample()
+ },
+ Example(
+ name = ::RichTooltipWithCustomCaretSample.name,
+ description = TooltipsExampleDescription,
+ sourceUrl = TooltipsExampleSourceUrl
+ ) {
+ RichTooltipWithCustomCaretSample()
}
)
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TooltipSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TooltipSamples.kt
index bb5327a..f2c1c0d 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TooltipSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TooltipSamples.kt
@@ -24,6 +24,7 @@
import androidx.compose.material.icons.filled.AddCircle
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.Info
+import androidx.compose.material3.CaretProperties
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@@ -130,6 +131,32 @@
@OptIn(ExperimentalMaterial3Api::class)
@Sampled
@Composable
+fun PlainTooltipWithCustomCaret() {
+ TooltipBox(
+ positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
+ tooltip = {
+ PlainTooltip(
+ caretProperties = CaretProperties(12.dp, 24.dp)
+ ) {
+ Text("Add to favorites")
+ }
+ },
+ state = rememberTooltipState()
+ ) {
+ IconButton(
+ onClick = { /* Icon button's click event */ }
+ ) {
+ Icon(
+ imageVector = Icons.Filled.Favorite,
+ contentDescription = "Localized Description"
+ )
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Sampled
+@Composable
fun RichTooltipSample() {
val tooltipState = rememberTooltipState(isPersistent = true)
val scope = rememberCoroutineScope()
@@ -201,6 +228,74 @@
}
}
+@OptIn(ExperimentalMaterial3Api::class)
+@Sampled
+@Composable
+fun RichTooltipWithCaretSample() {
+ val tooltipState = rememberTooltipState(isPersistent = true)
+ val scope = rememberCoroutineScope()
+ TooltipBox(
+ positionProvider = TooltipDefaults.rememberRichTooltipPositionProvider(),
+ tooltip = {
+ RichTooltip(
+ title = { Text(richTooltipSubheadText) },
+ action = {
+ TextButton(
+ onClick = { scope.launch { tooltipState.dismiss() } }
+ ) { Text(richTooltipActionText) }
+ },
+ caretProperties = TooltipDefaults.caretProperties
+ ) {
+ Text(richTooltipText)
+ }
+ },
+ state = tooltipState
+ ) {
+ IconButton(
+ onClick = { /* Icon button's click event */ }
+ ) {
+ Icon(
+ imageVector = Icons.Filled.Info,
+ contentDescription = "Localized Description"
+ )
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Sampled
+@Composable
+fun RichTooltipWithCustomCaretSample() {
+ val tooltipState = rememberTooltipState(isPersistent = true)
+ val scope = rememberCoroutineScope()
+ TooltipBox(
+ positionProvider = TooltipDefaults.rememberRichTooltipPositionProvider(),
+ tooltip = {
+ RichTooltip(
+ title = { Text(richTooltipSubheadText) },
+ action = {
+ TextButton(
+ onClick = { scope.launch { tooltipState.dismiss() } }
+ ) { Text(richTooltipActionText) }
+ },
+ caretProperties = CaretProperties(16.dp, 32.dp)
+ ) {
+ Text(richTooltipText)
+ }
+ },
+ state = tooltipState
+ ) {
+ IconButton(
+ onClick = { /* Icon button's click event */ }
+ ) {
+ Icon(
+ imageVector = Icons.Filled.Info,
+ contentDescription = "Localized Description"
+ )
+ }
+ }
+}
+
const val richTooltipSubheadText = "Permissions"
const val richTooltipText =
"Configure permissions for selected service accounts. " +
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TooltipTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TooltipTest.kt
index 72e05c8..1cc240e 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TooltipTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TooltipTest.kt
@@ -493,7 +493,52 @@
)
}
- assertThat(anchorBounds == expectedAnchorBounds).isTrue()
+ assertThat(anchorBounds.left).isWithin(0.001f).of(expectedAnchorBounds.left)
+ assertThat(anchorBounds.top).isWithin(0.001f).of(expectedAnchorBounds.top)
+ assertThat(anchorBounds.right).isWithin(0.001f).of(expectedAnchorBounds.right)
+ assertThat(anchorBounds.bottom).isWithin(0.001f).of(expectedAnchorBounds.bottom)
+ }
+
+ @Test
+ fun richTooltip_caretAnchorPositioning() {
+ var anchorBounds = Rect.Zero
+ rule.setContent {
+ TooltipBox(
+ positionProvider = TooltipDefaults.rememberRichTooltipPositionProvider(),
+ state = rememberTooltipState(initialIsVisible = true, isPersistent = true),
+ tooltip = {
+ RichTooltip(
+ modifier = Modifier.drawCaret {
+ it?.let { anchorBounds = it.boundsInWindow() }
+ onDrawBehind {}
+ }
+ ) {}
+ }
+ ) {
+ Icon(
+ Icons.Filled.Favorite,
+ modifier = Modifier.testTag(AnchorTestTag),
+ contentDescription = null
+ )
+ }
+ }
+
+ rule.waitForIdle()
+ val expectedAnchorBoundsDp =
+ rule.onNodeWithTag(AnchorTestTag, true).getUnclippedBoundsInRoot()
+ val expectedAnchorBounds = with(rule.density) {
+ Rect(
+ expectedAnchorBoundsDp.left.roundToPx().toFloat(),
+ expectedAnchorBoundsDp.top.roundToPx().toFloat(),
+ expectedAnchorBoundsDp.right.roundToPx().toFloat(),
+ expectedAnchorBoundsDp.bottom.roundToPx().toFloat()
+ )
+ }
+
+ assertThat(anchorBounds.left).isWithin(0.001f).of(expectedAnchorBounds.left)
+ assertThat(anchorBounds.top).isWithin(0.001f).of(expectedAnchorBounds.top)
+ assertThat(anchorBounds.right).isWithin(0.001f).of(expectedAnchorBounds.right)
+ assertThat(anchorBounds.bottom).isWithin(0.001f).of(expectedAnchorBounds.bottom)
}
@Test
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Tooltip.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Tooltip.android.kt
index e6368fc..4a98b2c 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Tooltip.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Tooltip.android.kt
@@ -18,9 +18,13 @@
import android.content.res.Configuration
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.paddingFromBaseline
+import androidx.compose.foundation.layout.requiredHeightIn
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.material3.tokens.PlainTooltipTokens
+import androidx.compose.material3.tokens.RichTooltipTokens
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
@@ -58,7 +62,7 @@
@ExperimentalMaterial3Api
actual fun CaretScope.PlainTooltip(
modifier: Modifier,
- caretProperties: (CaretProperties)?,
+ caretProperties: CaretProperties?,
shape: Shape,
contentColor: Color,
containerColor: Color,
@@ -66,23 +70,23 @@
shadowElevation: Dp,
content: @Composable () -> Unit
) {
- val customModifier =
+ val drawCaretModifier =
if (caretProperties != null) {
val density = LocalDensity.current
val configuration = LocalConfiguration.current
Modifier.drawCaret { anchorLayoutCoordinates ->
- drawCaretWithPath(
- density,
- configuration,
- containerColor,
- caretProperties,
- anchorLayoutCoordinates
- )
- }.then(modifier)
+ drawCaretWithPath(
+ CaretType.Plain,
+ density,
+ configuration,
+ containerColor,
+ caretProperties,
+ anchorLayoutCoordinates
+ )
+ }.then(modifier)
} else modifier
-
Surface(
- modifier = customModifier,
+ modifier = drawCaretModifier,
shape = shape,
color = containerColor,
tonalElevation = tonalElevation,
@@ -108,8 +112,123 @@
}
}
+/**
+ * Rich text tooltip that allows the user to pass in a title, text, and action.
+ * Tooltips are used to provide a descriptive message.
+ *
+ * Usually used with [TooltipBox]
+ *
+ * @param modifier the [Modifier] to be applied to the tooltip.
+ * @param title An optional title for the tooltip.
+ * @param action An optional action for the tooltip.
+ * @param caretProperties [CaretProperties] for the caret of the tooltip, if a default
+ * caret is desired with a specific dimension. Pass in null for this parameter if no
+ * caret is desired.
+ * @param shape the [Shape] that should be applied to the tooltip container.
+ * @param colors [RichTooltipColors] that will be applied to the tooltip's container and content.
+ * @param tonalElevation the tonal elevation of the tooltip.
+ * @param shadowElevation the shadow elevation of the tooltip.
+ * @param text the composable that will be used to populate the rich tooltip's text.
+ */
+@Composable
+@ExperimentalMaterial3Api
+actual fun CaretScope.RichTooltip(
+ modifier: Modifier,
+ title: (@Composable () -> Unit)?,
+ action: (@Composable () -> Unit)?,
+ caretProperties: CaretProperties?,
+ shape: Shape,
+ colors: RichTooltipColors,
+ tonalElevation: Dp,
+ shadowElevation: Dp,
+ text: @Composable () -> Unit
+) {
+ val absoluteElevation = LocalAbsoluteTonalElevation.current + tonalElevation
+ val elevatedColor =
+ MaterialTheme.colorScheme.applyTonalElevation(
+ colors.containerColor,
+ absoluteElevation
+ )
+ val drawCaretModifier =
+ if (caretProperties != null) {
+ val density = LocalDensity.current
+ val configuration = LocalConfiguration.current
+ Modifier.drawCaret { anchorLayoutCoordinates ->
+ drawCaretWithPath(
+ CaretType.Rich,
+ density,
+ configuration,
+ elevatedColor,
+ caretProperties,
+ anchorLayoutCoordinates
+ )
+ }.then(modifier)
+ } else modifier
+ Surface(
+ modifier = drawCaretModifier
+ .sizeIn(
+ minWidth = TooltipMinWidth,
+ maxWidth = RichTooltipMaxWidth,
+ minHeight = TooltipMinHeight
+ ),
+ shape = shape,
+ color = colors.containerColor,
+ tonalElevation = tonalElevation,
+ shadowElevation = shadowElevation
+ ) {
+ val actionLabelTextStyle =
+ MaterialTheme.typography.fromToken(RichTooltipTokens.ActionLabelTextFont)
+ val subheadTextStyle =
+ MaterialTheme.typography.fromToken(RichTooltipTokens.SubheadFont)
+ val supportingTextStyle =
+ MaterialTheme.typography.fromToken(RichTooltipTokens.SupportingTextFont)
+
+ Column(
+ modifier = Modifier.padding(horizontal = RichTooltipHorizontalPadding)
+ ) {
+ title?.let {
+ Box(
+ modifier = Modifier.paddingFromBaseline(top = HeightToSubheadFirstLine)
+ ) {
+ CompositionLocalProvider(
+ LocalContentColor provides colors.titleContentColor,
+ LocalTextStyle provides subheadTextStyle,
+ content = it
+ )
+ }
+ }
+ Box(
+ modifier = Modifier.textVerticalPadding(
+ title != null,
+ action != null
+ )
+ ) {
+ CompositionLocalProvider(
+ LocalContentColor provides colors.contentColor,
+ LocalTextStyle provides supportingTextStyle,
+ content = text
+ )
+ }
+ action?.let {
+ Box(
+ modifier = Modifier
+ .requiredHeightIn(min = ActionLabelMinHeight)
+ .padding(bottom = ActionLabelBottomPadding)
+ ) {
+ CompositionLocalProvider(
+ LocalContentColor provides colors.actionContentColor,
+ LocalTextStyle provides actionLabelTextStyle,
+ content = it
+ )
+ }
+ }
+ }
+ }
+}
+
@ExperimentalMaterial3Api
private fun CacheDrawScope.drawCaretWithPath(
+ caretType: CaretType,
density: Density,
configuration: Configuration,
containerColor: Color,
@@ -140,18 +259,46 @@
val isCaretTop = anchorTop - tooltipHeight - tooltipAnchorSpacing < 0
val caretY = if (isCaretTop) { 0f } else { tooltipHeight }
- val position =
- if (anchorMid + tooltipWidth / 2 > screenWidthPx) {
- val anchorMidFromRightScreenEdge =
- screenWidthPx - anchorMid
- val caretX = tooltipWidth - anchorMidFromRightScreenEdge
- Offset(caretX, caretY)
- } else {
- val tooltipLeft =
- anchorLeft - (this.size.width / 2 - anchorWidth / 2)
- val caretX = anchorMid - maxOf(tooltipLeft, 0f)
- Offset(caretX, caretY)
+ val position: Offset
+ if (caretType == CaretType.Plain) {
+ position =
+ if (anchorMid + tooltipWidth / 2 > screenWidthPx) {
+ // Caret needs to be near the right
+ val anchorMidFromRightScreenEdge =
+ screenWidthPx - anchorMid
+ val caretX = tooltipWidth - anchorMidFromRightScreenEdge
+ Offset(caretX, caretY)
+ } else {
+ // Caret needs to be near the left
+ val tooltipLeft =
+ anchorLeft - (this.size.width / 2 - anchorWidth / 2)
+ val caretX = anchorMid - maxOf(tooltipLeft, 0f)
+ Offset(caretX, caretY)
+ }
+ } else {
+ // Default the caret to the left
+ var preferredPosition = Offset(anchorMid - anchorLeft, caretY)
+ if (anchorLeft + tooltipWidth > screenWidthPx) {
+ // Need to move the caret to the right
+ preferredPosition = Offset(anchorMid - (anchorRight - tooltipWidth), caretY)
+ if (anchorRight - tooltipWidth < 0) {
+ // Need to center the caret
+ // Caret might need to be offset depending on where
+ // the tooltip is placed relative to the anchor
+ if (anchorLeft - tooltipWidth / 2 + anchorWidth / 2 <= 0) {
+ preferredPosition = Offset(anchorMid, caretY)
+ } else if (anchorRight + tooltipWidth / 2 - anchorWidth / 2 >= screenWidthPx) {
+ val anchorMidFromRightScreenEdge =
+ screenWidthPx - anchorMid
+ val caretX = tooltipWidth - anchorMidFromRightScreenEdge
+ preferredPosition = Offset(caretX, caretY)
+ } else {
+ preferredPosition = Offset(tooltipWidth / 2, caretY)
+ }
+ }
}
+ position = preferredPosition
+ }
if (isCaretTop) {
path.apply {
@@ -182,3 +329,8 @@
}
}
}
+
+@ExperimentalMaterial3Api
+private enum class CaretType {
+ Plain, Rich
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tooltip.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tooltip.kt
index a0dc11e..458be3a 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tooltip.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tooltip.kt
@@ -26,16 +26,12 @@
import androidx.compose.foundation.MutatePriority
import androidx.compose.foundation.MutatorMutex
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.paddingFromBaseline
-import androidx.compose.foundation.layout.requiredHeightIn
-import androidx.compose.foundation.layout.sizeIn
import androidx.compose.material3.tokens.PlainTooltipTokens
import androidx.compose.material3.tokens.RichTooltipTokens
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
@@ -84,6 +80,10 @@
*
* @sample androidx.compose.material3.samples.PlainTooltipWithCaret
*
+ * Plain tooltip shown on long press with a custom [CaretProperties]
+ *
+ * @sample androidx.compose.material3.samples.PlainTooltipWithCustomCaret
+ *
* Tooltip that is invoked when the anchor is long pressed:
*
* @sample androidx.compose.material3.samples.RichTooltipSample
@@ -92,6 +92,14 @@
*
* @sample androidx.compose.material3.samples.RichTooltipWithManualInvocationSample
*
+ * Rich tooltip with caret shown on long press:
+ *
+ * @sample androidx.compose.material3.samples.RichTooltipWithCaretSample
+ *
+ * Rich tooltip shown on long press with a custom [CaretProperties]
+ *
+ * @sample androidx.compose.material3.samples.RichTooltipWithCustomCaretSample
+ *
* @param positionProvider [PopupPositionProvider] that will be used to place the tooltip
* relative to the anchor content.
* @param tooltip the composable that will be used to populate the tooltip's content.
@@ -185,7 +193,7 @@
@ExperimentalMaterial3Api
expect fun CaretScope.PlainTooltip(
modifier: Modifier = Modifier,
- caretProperties: (CaretProperties)? = null,
+ caretProperties: CaretProperties? = null,
shape: Shape = TooltipDefaults.plainTooltipContainerShape,
contentColor: Color = TooltipDefaults.plainTooltipContentColor,
containerColor: Color = TooltipDefaults.plainTooltipContainerColor,
@@ -195,6 +203,38 @@
)
/**
+ * Rich text tooltip that allows the user to pass in a title, text, and action.
+ * Tooltips are used to provide a descriptive message.
+ *
+ * Usually used with [TooltipBox]
+ *
+ * @param modifier the [Modifier] to be applied to the tooltip.
+ * @param title An optional title for the tooltip.
+ * @param action An optional action for the tooltip.
+ * @param caretProperties [CaretProperties] for the caret of the tooltip, if a default
+ * caret is desired with a specific dimension. Pass in null for this parameter if no
+ * caret is desired.
+ * @param shape the [Shape] that should be applied to the tooltip container.
+ * @param colors [RichTooltipColors] that will be applied to the tooltip's container and content.
+ * @param tonalElevation the tonal elevation of the tooltip.
+ * @param shadowElevation the shadow elevation of the tooltip.
+ * @param text the composable that will be used to populate the rich tooltip's text.
+ */
+@Composable
+@ExperimentalMaterial3Api
+expect fun CaretScope.RichTooltip(
+ modifier: Modifier = Modifier,
+ title: (@Composable () -> Unit)? = null,
+ action: (@Composable () -> Unit)? = null,
+ caretProperties: CaretProperties? = null,
+ shape: Shape = TooltipDefaults.richTooltipContainerShape,
+ colors: RichTooltipColors = TooltipDefaults.richTooltipColors(),
+ tonalElevation: Dp = RichTooltipTokens.ContainerElevation,
+ shadowElevation: Dp = RichTooltipTokens.ContainerElevation,
+ text: @Composable () -> Unit
+)
+
+/**
* Properties for the caret of the tooltip if enabled.
*
* @param caretHeight the height of the caret
@@ -208,92 +248,6 @@
)
/**
- * Rich text tooltip that allows the user to pass in a title, text, and action.
- * Tooltips are used to provide a descriptive message.
- *
- * Usually used with [TooltipBox]
- *
- * @param modifier the [Modifier] to be applied to the tooltip.
- * @param title An optional title for the tooltip.
- * @param action An optional action for the tooltip.
- * @param shape the [Shape] that should be applied to the tooltip container.
- * @param colors [RichTooltipColors] that will be applied to the tooltip's container and content.
- * @param tonalElevation the tonal elevation of the tooltip.
- * @param shadowElevation the shadow elevation of the tooltip.
- * @param text the composable that will be used to populate the rich tooltip's text.
- */
-@Composable
-@ExperimentalMaterial3Api
-fun RichTooltip(
- modifier: Modifier = Modifier,
- title: (@Composable () -> Unit)? = null,
- action: (@Composable () -> Unit)? = null,
- shape: Shape = TooltipDefaults.richTooltipContainerShape,
- colors: RichTooltipColors = TooltipDefaults.richTooltipColors(),
- tonalElevation: Dp = RichTooltipTokens.ContainerElevation,
- shadowElevation: Dp = RichTooltipTokens.ContainerElevation,
- text: @Composable () -> Unit
-) {
- Surface(
- modifier = modifier
- .sizeIn(
- minWidth = TooltipMinWidth,
- maxWidth = RichTooltipMaxWidth,
- minHeight = TooltipMinHeight
- ),
- shape = shape,
- color = colors.containerColor,
- tonalElevation = tonalElevation,
- shadowElevation = shadowElevation
- ) {
- val actionLabelTextStyle =
- MaterialTheme.typography.fromToken(RichTooltipTokens.ActionLabelTextFont)
- val subheadTextStyle =
- MaterialTheme.typography.fromToken(RichTooltipTokens.SubheadFont)
- val supportingTextStyle =
- MaterialTheme.typography.fromToken(RichTooltipTokens.SupportingTextFont)
-
- Column(
- modifier = Modifier.padding(horizontal = RichTooltipHorizontalPadding)
- ) {
- title?.let {
- Box(
- modifier = Modifier.paddingFromBaseline(top = HeightToSubheadFirstLine)
- ) {
- CompositionLocalProvider(
- LocalContentColor provides colors.titleContentColor,
- LocalTextStyle provides subheadTextStyle,
- content = it
- )
- }
- }
- Box(
- modifier = Modifier.textVerticalPadding(title != null, action != null)
- ) {
- CompositionLocalProvider(
- LocalContentColor provides colors.contentColor,
- LocalTextStyle provides supportingTextStyle,
- content = text
- )
- }
- action?.let {
- Box(
- modifier = Modifier
- .requiredHeightIn(min = ActionLabelMinHeight)
- .padding(bottom = ActionLabelBottomPadding)
- ) {
- CompositionLocalProvider(
- LocalContentColor provides colors.actionContentColor,
- LocalTextStyle provides actionLabelTextStyle,
- content = it
- )
- }
- }
- }
- }
-}
-
-/**
* Tooltip defaults that contain default values for both [PlainTooltip] and [RichTooltip]
*/
@ExperimentalMaterial3Api
@@ -420,11 +374,11 @@
layoutDirection: LayoutDirection,
popupContentSize: IntSize
): IntOffset {
- var x = anchorBounds.right
+ var x = anchorBounds.left
// Try to shift it to the left of the anchor
// if the tooltip would collide with the right side of the screen
if (x + popupContentSize.width > windowSize.width) {
- x = anchorBounds.left - popupContentSize.width
+ x = anchorBounds.right - popupContentSize.width
// Center if it'll also collide with the left side of the screen
if (x < 0)
x = anchorBounds.left +
@@ -632,7 +586,7 @@
}
@Stable
-private fun Modifier.textVerticalPadding(
+internal fun Modifier.textVerticalPadding(
subheadExists: Boolean,
actionExists: Boolean
): Modifier {
@@ -706,13 +660,13 @@
private val PlainTooltipHorizontalPadding = 8.dp
internal val PlainTooltipContentPadding =
PaddingValues(PlainTooltipHorizontalPadding, PlainTooltipVerticalPadding)
-private val RichTooltipMaxWidth = 320.dp
+internal val RichTooltipMaxWidth = 320.dp
internal val RichTooltipHorizontalPadding = 16.dp
-private val HeightToSubheadFirstLine = 28.dp
+internal val HeightToSubheadFirstLine = 28.dp
private val HeightFromSubheadToTextFirstLine = 24.dp
private val TextBottomPadding = 16.dp
-private val ActionLabelMinHeight = 36.dp
-private val ActionLabelBottomPadding = 8.dp
+internal val ActionLabelMinHeight = 36.dp
+internal val ActionLabelBottomPadding = 8.dp
// No specification for fade in and fade out duration, so aligning it with the behavior for snack bar
internal const val TooltipFadeInDuration = 150
internal const val TooltipFadeOutDuration = 75
diff --git a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/Tooltip.desktop.kt b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/Tooltip.desktop.kt
index 6760005..f867216 100644
--- a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/Tooltip.desktop.kt
+++ b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/Tooltip.desktop.kt
@@ -17,9 +17,13 @@
package androidx.compose.material3
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.paddingFromBaseline
+import androidx.compose.foundation.layout.requiredHeightIn
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.material3.tokens.PlainTooltipTokens
+import androidx.compose.material3.tokens.RichTooltipTokens
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
@@ -47,7 +51,7 @@
@ExperimentalMaterial3Api
actual fun CaretScope.PlainTooltip(
modifier: Modifier,
- caretProperties: (CaretProperties)?,
+ caretProperties: CaretProperties?,
shape: Shape,
contentColor: Color,
containerColor: Color,
@@ -80,3 +84,93 @@
}
}
}
+
+/**
+ * Rich text tooltip that allows the user to pass in a title, text, and action.
+ * Tooltips are used to provide a descriptive message.
+ *
+ * Usually used with [TooltipBox]
+ *
+ * @param modifier the [Modifier] to be applied to the tooltip.
+ * @param title An optional title for the tooltip.
+ * @param action An optional action for the tooltip.
+ * @param caretProperties [CaretProperties] for the caret of the tooltip, if a default
+ * caret is desired with a specific dimension. Pass in null for this parameter if no
+ * caret is desired.
+ * @param shape the [Shape] that should be applied to the tooltip container.
+ * @param colors [RichTooltipColors] that will be applied to the tooltip's container and content.
+ * @param tonalElevation the tonal elevation of the tooltip.
+ * @param shadowElevation the shadow elevation of the tooltip.
+ * @param text the composable that will be used to populate the rich tooltip's text.
+ */
+@Composable
+@ExperimentalMaterial3Api
+actual fun CaretScope.RichTooltip(
+ modifier: Modifier,
+ title: (@Composable () -> Unit)?,
+ action: (@Composable () -> Unit)?,
+ caretProperties: CaretProperties?,
+ shape: Shape,
+ colors: RichTooltipColors,
+ tonalElevation: Dp,
+ shadowElevation: Dp,
+ text: @Composable () -> Unit
+) {
+ Surface(
+ modifier = modifier
+ .sizeIn(
+ minWidth = TooltipMinWidth,
+ maxWidth = RichTooltipMaxWidth,
+ minHeight = TooltipMinHeight
+ ),
+ shape = shape,
+ color = colors.containerColor,
+ tonalElevation = tonalElevation,
+ shadowElevation = shadowElevation
+ ) {
+ val actionLabelTextStyle =
+ MaterialTheme.typography.fromToken(RichTooltipTokens.ActionLabelTextFont)
+ val subheadTextStyle =
+ MaterialTheme.typography.fromToken(RichTooltipTokens.SubheadFont)
+ val supportingTextStyle =
+ MaterialTheme.typography.fromToken(RichTooltipTokens.SupportingTextFont)
+
+ Column(
+ modifier = Modifier.padding(horizontal = RichTooltipHorizontalPadding)
+ ) {
+ title?.let {
+ Box(
+ modifier = Modifier.paddingFromBaseline(top = HeightToSubheadFirstLine)
+ ) {
+ CompositionLocalProvider(
+ LocalContentColor provides colors.titleContentColor,
+ LocalTextStyle provides subheadTextStyle,
+ content = it
+ )
+ }
+ }
+ Box(
+ modifier = Modifier.textVerticalPadding(title != null, action != null)
+ ) {
+ CompositionLocalProvider(
+ LocalContentColor provides colors.contentColor,
+ LocalTextStyle provides supportingTextStyle,
+ content = text
+ )
+ }
+ action?.let {
+ Box(
+ modifier = Modifier
+ .requiredHeightIn(min = ActionLabelMinHeight)
+ .padding(bottom = ActionLabelBottomPadding)
+ ) {
+ CompositionLocalProvider(
+ LocalContentColor provides colors.actionContentColor,
+ LocalTextStyle provides actionLabelTextStyle,
+ content = it
+ )
+ }
+ }
+ }
+ }
+}