diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/tooltip/ElementTooltipDefaults.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/tooltip/ElementTooltipDefaults.kt new file mode 100644 index 0000000000..d1713d984c --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/tooltip/ElementTooltipDefaults.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.designsystem.components.tooltip + +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TooltipDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntRect +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.PopupPositionProvider + +object ElementTooltipDefaults { + /** + * Creates a [PopupPositionProvider] that allows adding padding between the edge of the + * window and the tooltip. + * + * It is a wrapper around [TooltipDefaults.rememberPlainTooltipPositionProvider] and is + * designed for use with a [PlainTooltip]. + * + * @param spacingBetweenTooltipAndAnchor the spacing between the tooltip and the anchor. + * @param windowPadding the padding between the tooltip and the edge of the window. + * + * @return a [PopupPositionProvider]. + */ + @OptIn(ExperimentalMaterial3Api::class) + @Composable + fun rememberPlainTooltipPositionProvider( + spacingBetweenTooltipAndAnchor: Dp = 8.dp, + windowPadding: Dp = 12.dp, + ): PopupPositionProvider { + val windowPaddingPx = with(LocalDensity.current) { windowPadding.roundToPx() } + val plainTooltipPositionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider( + spacingBetweenTooltipAndAnchor = spacingBetweenTooltipAndAnchor, + ) + return remember(windowPaddingPx, plainTooltipPositionProvider) { + object : PopupPositionProvider { + override fun calculatePosition( + anchorBounds: IntRect, + windowSize: IntSize, + layoutDirection: LayoutDirection, + popupContentSize: IntSize + ): IntOffset = plainTooltipPositionProvider + .calculatePosition( + anchorBounds = anchorBounds, + windowSize = windowSize, + layoutDirection = layoutDirection, + popupContentSize = popupContentSize + ) + .let { + val maxX = windowSize.width - popupContentSize.width - windowPaddingPx + val maxY = windowSize.height - popupContentSize.height - windowPaddingPx + if (maxX <= windowPaddingPx || maxY <= windowPaddingPx) { + return@let it + } + IntOffset( + x = it.x.coerceIn( + minimumValue = windowPaddingPx, + maximumValue = maxX, + ), + y = it.y.coerceIn( + minimumValue = windowPaddingPx, + maximumValue = maxY, + ) + ) + } + } + } + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/tooltip/PlainTooltip.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/tooltip/PlainTooltip.kt new file mode 100644 index 0000000000..78f381a685 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/tooltip/PlainTooltip.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.designsystem.components.tooltip + +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TooltipDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import io.element.android.libraries.theme.ElementTheme +import androidx.compose.material3.PlainTooltip as M3PlainTooltip + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PlainTooltip( + modifier: Modifier = Modifier, + contentColor: Color = ElementTheme.colors.textOnSolidPrimary, + containerColor: Color = ElementTheme.colors.bgActionPrimaryRest, + shape: Shape = TooltipDefaults.plainTooltipContainerShape, + content: @Composable () -> Unit, +) = M3PlainTooltip( + modifier = modifier, + contentColor = contentColor, + containerColor = containerColor, + shape = shape, + content = content, +) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/tooltip/TooltipBox.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/tooltip/TooltipBox.kt new file mode 100644 index 0000000000..589f1fddcd --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/tooltip/TooltipBox.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.designsystem.components.tooltip + +import androidx.compose.material3.TooltipState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.window.PopupPositionProvider +import androidx.compose.material3.TooltipBox as M3TooltipBox + +@Composable +fun TooltipBox( + positionProvider: PopupPositionProvider, + tooltip: @Composable () -> Unit, + state: TooltipState, + modifier: Modifier = Modifier, + focusable: Boolean = true, + enableUserInput: Boolean = true, + content: @Composable () -> Unit, +) = M3TooltipBox( + positionProvider = positionProvider, + tooltip = tooltip, + state = state, + modifier = modifier, + focusable = focusable, + enableUserInput = enableUserInput, + content = content, +) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordButton.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordButton.kt index e76a1b16b0..0a0710095e 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordButton.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordButton.kt @@ -14,24 +14,38 @@ * limitations under the License. */ +@file:OptIn(ExperimentalMaterial3Api::class) + package io.element.android.libraries.textcomposer.components +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TooltipState +import androidx.compose.material3.rememberTooltipState import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.components.tooltip.ElementTooltipDefaults +import io.element.android.libraries.designsystem.components.tooltip.PlainTooltip +import io.element.android.libraries.designsystem.components.tooltip.TooltipBox import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.IconButton +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.utils.CommonDrawables +import io.element.android.libraries.textcomposer.R import io.element.android.libraries.textcomposer.utils.PressState import io.element.android.libraries.textcomposer.utils.PressStateEffects import io.element.android.libraries.textcomposer.utils.rememberPressState @@ -39,9 +53,11 @@ import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.coroutines.launch +@OptIn(ExperimentalMaterial3Api::class) @Composable internal fun RecordButton( modifier: Modifier = Modifier, + initialTooltipIsVisible: Boolean = false, onPressStart: () -> Unit = {}, onLongPressEnd: () -> Unit = {}, onTap: () -> Unit = {}, @@ -54,6 +70,10 @@ internal fun RecordButton( hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) } + val tooltipState = rememberTooltipState( + initialIsVisible = initialTooltipIsVisible + ) + PressStateEffects( pressState = pressState.value, onPressStart = { @@ -67,26 +87,34 @@ internal fun RecordButton( onTap = { onTap() performHapticFeedback() + coroutineScope.launch { tooltipState.show() } }, ) - - RecordButtonView( - isPressed = pressState.value is PressState.Pressing, - modifier = modifier - .pointerInput(Unit) { - awaitPointerEventScope { - while (true) { - val event = awaitPointerEvent() - coroutineScope.launch { - when (event.type) { - PointerEventType.Press -> pressState.press() - PointerEventType.Release -> pressState.release() + Box(modifier = modifier) { + HoldToRecordTooltip( + tooltipState = tooltipState, + spacingBetweenTooltipAndAnchor = 0.dp, // Accounts for the 48.dp size of the record button + anchor = { + RecordButtonView( + isPressed = pressState.value is PressState.Pressing, + modifier = Modifier + .pointerInput(Unit) { + awaitPointerEventScope { + while (true) { + val event = awaitPointerEvent() + coroutineScope.launch { + when (event.type) { + PointerEventType.Press -> pressState.press() + PointerEventType.Release -> pressState.release() + } + } + } } } - } - } + ) } - ) + ) + } } @Composable @@ -112,6 +140,34 @@ private fun RecordButtonView( } } +@Composable +private fun HoldToRecordTooltip( + tooltipState: TooltipState, + spacingBetweenTooltipAndAnchor: Dp, + modifier: Modifier = Modifier, + anchor: @Composable () -> Unit, +) { + TooltipBox( + positionProvider = ElementTooltipDefaults.rememberPlainTooltipPositionProvider( + spacingBetweenTooltipAndAnchor = spacingBetweenTooltipAndAnchor, + ), + tooltip = { + PlainTooltip { + Text( + text = stringResource(R.string.screen_room_voice_message_tooltip), + color = ElementTheme.colors.textOnSolidPrimary, + style = ElementTheme.typography.fontBodySmMedium, + ) + } + }, + state = tooltipState, + modifier = modifier, + focusable = false, + enableUserInput = false, + content = anchor, + ) +} + @PreviewsDayNight @Composable internal fun RecordButtonPreview() = ElementPreview { @@ -121,3 +177,13 @@ internal fun RecordButtonPreview() = ElementPreview { } } +@PreviewsDayNight +@Composable +internal fun HoldToRecordTooltipPreview() = ElementPreview { + Box(modifier = Modifier.fillMaxSize()) { + RecordButton( + modifier = Modifier.align(Alignment.BottomEnd), + initialTooltipIsVisible = true, + ) + } +} diff --git a/libraries/textcomposer/impl/src/main/res/values/localazy.xml b/libraries/textcomposer/impl/src/main/res/values/localazy.xml index 5017d353f8..8ba2c3b0b3 100644 --- a/libraries/textcomposer/impl/src/main/res/values/localazy.xml +++ b/libraries/textcomposer/impl/src/main/res/values/localazy.xml @@ -21,4 +21,5 @@ "Unindent" "Link" "Add attachment" + "Hold to record" diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_HoldToRecordTooltip-D-13_13_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_HoldToRecordTooltip-D-13_13_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2f3dc97e8c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_HoldToRecordTooltip-D-13_13_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e6d75686b1463d11d89e5130b160c3c446f26612e8da44981cc6994687001d80 +size 7093 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_HoldToRecordTooltip-N-13_14_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_HoldToRecordTooltip-N-13_14_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ff6633e96a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_HoldToRecordTooltip-N-13_14_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f8c96b9ce6a8d9136c56789e9e6af25b4259143f6416305c523f4e5224bc8fa8 +size 5898 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-D-13_13_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-D-14_14_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-D-13_13_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-D-14_14_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-N-13_14_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-N-14_15_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-N-13_14_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-N-14_15_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-D-14_14_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-D-15_15_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-D-14_14_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-D-15_15_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-N-14_15_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-N-15_16_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-N-14_15_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-N-15_16_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageDeleteButton-D-15_15_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageDeleteButton-D-16_16_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageDeleteButton-D-15_15_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageDeleteButton-D-16_16_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageDeleteButton-N-15_16_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageDeleteButton-N-16_17_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageDeleteButton-N-15_16_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageDeleteButton-N-16_17_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-16_16_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-17_17_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-16_16_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-17_17_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-16_17_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-17_18_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-16_17_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-17_18_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-D-17_17_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-D-18_18_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-D-17_17_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-D-18_18_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-N-17_18_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-N-18_19_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-N-17_18_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-N-18_19_null,NEXUS_5,1.0,en].png diff --git a/tools/localazy/config.json b/tools/localazy/config.json index d7de5c9eae..4fa301b914 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -53,7 +53,8 @@ { "name": ":libraries:textcomposer:impl", "includeRegex": [ - "rich_text_editor.*" + "rich_text_editor.*", + ".*voice_message_tooltip" ] }, {