diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt index 9d3db35959..75b94cdfb9 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt @@ -19,7 +19,6 @@ package io.element.android.libraries.textcomposer import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable -import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -34,24 +33,15 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredHeightIn import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -59,7 +49,6 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.text.applyScaleUp 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.matrix.api.core.EventId @@ -70,20 +59,17 @@ import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag -import io.element.android.libraries.textcomposer.components.FormattingOption -import io.element.android.libraries.textcomposer.components.FormattingOptionState +import io.element.android.libraries.textcomposer.components.ComposerOptionsButton +import io.element.android.libraries.textcomposer.components.DismissTextFormattingButton +import io.element.android.libraries.textcomposer.components.SendButton +import io.element.android.libraries.textcomposer.components.TextFormatting import io.element.android.libraries.textcomposer.components.textInputRoundedCornerShape import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.wysiwyg.compose.RichTextEditor import io.element.android.wysiwyg.compose.RichTextEditorState -import io.element.android.wysiwyg.view.models.InlineFormat -import io.element.android.wysiwyg.view.models.LinkAction import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -import kotlinx.coroutines.launch -import uniffi.wysiwyg_composer.ActionState -import uniffi.wysiwyg_composer.ComposerAction @Composable fun TextComposer( @@ -313,209 +299,6 @@ private fun TextInput( } } -@Composable -private fun ComposerOptionsButton( - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - IconButton( - modifier = modifier - .size(48.dp), - onClick = onClick - ) { - Icon( - modifier = Modifier.size(30.dp.applyScaleUp()), - resourceId = CommonDrawables.ic_plus, - contentDescription = stringResource(R.string.rich_text_editor_a11y_add_attachment), - tint = ElementTheme.colors.iconPrimary, - ) - } -} - -@Composable -private fun DismissTextFormattingButton( - onClick: () -> Unit, - modifier: Modifier = Modifier -) { - IconButton( - modifier = modifier - .size(48.dp), - onClick = onClick - ) { - Icon( - modifier = Modifier.size(30.dp.applyScaleUp()), - resourceId = CommonDrawables.ic_cancel, - contentDescription = stringResource(CommonStrings.action_close), - tint = ElementTheme.colors.iconPrimary, - ) - } -} - -@Composable -private fun TextFormatting( - state: RichTextEditorState, - modifier: Modifier = Modifier, -) { - - val scrollState = rememberScrollState() - val coroutineScope = rememberCoroutineScope() - - fun onInlineFormatClick(inlineFormat: InlineFormat) { - coroutineScope.launch { - state.toggleInlineFormat(inlineFormat) - } - } - - fun onToggleListClick(ordered: Boolean) { - coroutineScope.launch { - state.toggleList(ordered) - } - } - - fun onIndentClick() { - coroutineScope.launch { - state.indent() - } - } - - fun onUnindentClick() { - coroutineScope.launch { - state.unindent() - } - } - - fun onCodeBlockClick() { - coroutineScope.launch { - state.toggleCodeBlock() - } - } - - fun onQuoteClick() { - coroutineScope.launch { - state.toggleQuote() - } - } - - fun onCreateLinkRequest(url: String, text: String) { - coroutineScope.launch { - state.insertLink(url, text) - } - } - - fun onSaveLinkRequest(url: String) { - coroutineScope.launch { - state.setLink(url) - } - } - - fun onRemoveLinkRequest() { - coroutineScope.launch { - state.removeLink() - } - } - - Row( - modifier = modifier - .horizontalScroll(scrollState), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - ) { - FormattingOption( - state = state.actions[ComposerAction.BOLD].toButtonState(), - onClick = { onInlineFormatClick(InlineFormat.Bold) }, - imageVector = ImageVector.vectorResource(CommonDrawables.ic_bold), - contentDescription = stringResource(R.string.rich_text_editor_format_bold) - ) - FormattingOption( - state = state.actions[ComposerAction.ITALIC].toButtonState(), - onClick = { onInlineFormatClick(InlineFormat.Italic) }, - imageVector = ImageVector.vectorResource(CommonDrawables.ic_italic), - contentDescription = stringResource(R.string.rich_text_editor_format_italic) - ) - FormattingOption( - state = state.actions[ComposerAction.UNDERLINE].toButtonState(), - onClick = { onInlineFormatClick(InlineFormat.Underline) }, - imageVector = ImageVector.vectorResource(CommonDrawables.ic_underline), - contentDescription = stringResource(R.string.rich_text_editor_format_underline) - ) - FormattingOption( - state = state.actions[ComposerAction.STRIKE_THROUGH].toButtonState(), - onClick = { onInlineFormatClick(InlineFormat.StrikeThrough) }, - imageVector = ImageVector.vectorResource(CommonDrawables.ic_strikethrough), - contentDescription = stringResource(R.string.rich_text_editor_format_strikethrough) - ) - - var linkDialogAction by remember { mutableStateOf(null) } - - linkDialogAction?.let { - TextComposerLinkDialog( - onDismissRequest = { linkDialogAction = null }, - onCreateLinkRequest = ::onCreateLinkRequest, - onSaveLinkRequest = ::onSaveLinkRequest, - onRemoveLinkRequest = ::onRemoveLinkRequest, - linkAction = it, - ) - } - - FormattingOption( - state = state.actions[ComposerAction.LINK].toButtonState(), - onClick = { linkDialogAction = state.linkAction }, - imageVector = ImageVector.vectorResource(CommonDrawables.ic_link), - contentDescription = stringResource(R.string.rich_text_editor_link) - ) - - FormattingOption( - state = state.actions[ComposerAction.UNORDERED_LIST].toButtonState(), - onClick = { onToggleListClick(ordered = false) }, - imageVector = ImageVector.vectorResource(CommonDrawables.ic_bullet_list), - contentDescription = stringResource(R.string.rich_text_editor_bullet_list) - ) - FormattingOption( - state = state.actions[ComposerAction.ORDERED_LIST].toButtonState(), - onClick = { onToggleListClick(ordered = true) }, - imageVector = ImageVector.vectorResource(CommonDrawables.ic_numbered_list), - contentDescription = stringResource(R.string.rich_text_editor_numbered_list) - ) - FormattingOption( - state = state.actions[ComposerAction.INDENT].toButtonState(), - onClick = { onIndentClick() }, - imageVector = ImageVector.vectorResource(CommonDrawables.ic_indent_increase), - contentDescription = stringResource(R.string.rich_text_editor_indent) - ) - FormattingOption( - state = state.actions[ComposerAction.UNINDENT].toButtonState(), - onClick = { onUnindentClick() }, - imageVector = ImageVector.vectorResource(CommonDrawables.ic_indent_decrease), - contentDescription = stringResource(R.string.rich_text_editor_unindent) - ) - FormattingOption( - state = state.actions[ComposerAction.INLINE_CODE].toButtonState(), - onClick = { onInlineFormatClick(InlineFormat.InlineCode) }, - imageVector = ImageVector.vectorResource(CommonDrawables.ic_inline_code), - contentDescription = stringResource(R.string.rich_text_editor_inline_code) - ) - FormattingOption( - state = state.actions[ComposerAction.CODE_BLOCK].toButtonState(), - onClick = { onCodeBlockClick() }, - imageVector = ImageVector.vectorResource(CommonDrawables.ic_code_block), - contentDescription = stringResource(R.string.rich_text_editor_code_block) - ) - FormattingOption( - state = state.actions[ComposerAction.QUOTE].toButtonState(), - onClick = { onQuoteClick() }, - imageVector = ImageVector.vectorResource(CommonDrawables.ic_quote), - contentDescription = stringResource(R.string.rich_text_editor_quote) - ) - } -} - -private fun ActionState?.toButtonState(): FormattingOptionState = - when (this) { - ActionState.ENABLED -> FormattingOptionState.Default - ActionState.REVERSED -> FormattingOptionState.Selected - ActionState.DISABLED, null -> FormattingOptionState.Disabled - } - @Composable private fun ComposerModeView( composerMode: MessageComposerMode, @@ -648,56 +431,6 @@ private fun ReplyToModeView( } } -@Composable -private fun SendButton( - canSendMessage: Boolean, - onClick: () -> Unit, - composerMode: MessageComposerMode, - modifier: Modifier = Modifier, -) { - IconButton( - modifier = modifier - .size(48.dp.applyScaleUp()), - onClick = onClick, - enabled = canSendMessage, - ) { - val iconId = when (composerMode) { - is MessageComposerMode.Edit -> CommonDrawables.ic_compound_check - else -> CommonDrawables.ic_september_send - } - val iconSize = when (composerMode) { - is MessageComposerMode.Edit -> 24.dp - // CommonDrawables.ic_september_send is too big... reduce its size. - else -> 18.dp - } - val iconStartPadding = when (composerMode) { - is MessageComposerMode.Edit -> 0.dp - else -> 2.dp - } - val contentDescription = when (composerMode) { - is MessageComposerMode.Edit -> stringResource(CommonStrings.action_edit) - else -> stringResource(CommonStrings.action_send) - } - Box( - modifier = Modifier - .clip(CircleShape) - .size(36.dp.applyScaleUp()) - .background(if (canSendMessage) ElementTheme.colors.iconAccentTertiary else Color.Transparent) - ) { - Icon( - modifier = Modifier - .height(iconSize.applyScaleUp()) - .padding(start = iconStartPadding) - .align(Alignment.Center), - resourceId = iconId, - contentDescription = contentDescription, - // Exception here, we use Color.White instead of ElementTheme.colors.iconOnSolidPrimary - tint = if (canSendMessage) Color.White else ElementTheme.colors.iconDisabled - ) - } - } -} - @PreviewsDayNight @Composable internal fun TextComposerSimplePreview() = ElementPreview { diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/ComposerOptionsButton.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/ComposerOptionsButton.kt new file mode 100644 index 0000000000..d1c7355861 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/ComposerOptionsButton.kt @@ -0,0 +1,57 @@ +/* + * 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.textcomposer.components + +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.text.applyScaleUp +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconButton +import io.element.android.libraries.designsystem.utils.CommonDrawables +import io.element.android.libraries.textcomposer.R +import io.element.android.libraries.theme.ElementTheme + +@Composable +internal fun ComposerOptionsButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + IconButton( + modifier = modifier + .size(48.dp), + onClick = onClick + ) { + Icon( + modifier = Modifier.size(30.dp.applyScaleUp()), + resourceId = CommonDrawables.ic_plus, + contentDescription = stringResource(R.string.rich_text_editor_a11y_add_attachment), + tint = ElementTheme.colors.iconPrimary, + ) + } +} + +@PreviewsDayNight +@Composable +internal fun ComposerOptionsButtonPreview() = ElementPreview { + ComposerOptionsButton(onClick = {}) +} + diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/DismissTextFormattingButton.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/DismissTextFormattingButton.kt new file mode 100644 index 0000000000..c6ebe270bf --- /dev/null +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/DismissTextFormattingButton.kt @@ -0,0 +1,57 @@ +/* + * 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.textcomposer.components + +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.text.applyScaleUp +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconButton +import io.element.android.libraries.designsystem.utils.CommonDrawables +import io.element.android.libraries.theme.ElementTheme +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +internal fun DismissTextFormattingButton( + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + IconButton( + modifier = modifier + .size(48.dp), + onClick = onClick + ) { + Icon( + modifier = Modifier.size(30.dp.applyScaleUp()), + resourceId = CommonDrawables.ic_cancel, + contentDescription = stringResource(CommonStrings.action_close), + tint = ElementTheme.colors.iconPrimary, + ) + } +} + +@PreviewsDayNight +@Composable +internal fun DismissTextFormattingButtonPreview() = ElementPreview { + DismissTextFormattingButton(onClick = {}) +} + diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt new file mode 100644 index 0000000000..5ac16bfd14 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt @@ -0,0 +1,104 @@ +/* + * 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.textcomposer.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.text.applyScaleUp +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconButton +import io.element.android.libraries.designsystem.utils.CommonDrawables +import io.element.android.libraries.textcomposer.MessageComposerMode +import io.element.android.libraries.theme.ElementTheme +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +internal fun SendButton( + canSendMessage: Boolean, + onClick: () -> Unit, + composerMode: MessageComposerMode, + modifier: Modifier = Modifier, +) { + IconButton( + modifier = modifier + .size(48.dp.applyScaleUp()), + onClick = onClick, + enabled = canSendMessage, + ) { + val iconId = when (composerMode) { + is MessageComposerMode.Edit -> CommonDrawables.ic_compound_check + else -> CommonDrawables.ic_september_send + } + val iconSize = when (composerMode) { + is MessageComposerMode.Edit -> 24.dp + // CommonDrawables.ic_september_send is too big... reduce its size. + else -> 18.dp + } + val iconStartPadding = when (composerMode) { + is MessageComposerMode.Edit -> 0.dp + else -> 2.dp + } + val contentDescription = when (composerMode) { + is MessageComposerMode.Edit -> stringResource(CommonStrings.action_edit) + else -> stringResource(CommonStrings.action_send) + } + Box( + modifier = Modifier + .clip(CircleShape) + .size(36.dp.applyScaleUp()) + .background(if (canSendMessage) ElementTheme.colors.iconAccentTertiary else Color.Transparent) + ) { + Icon( + modifier = Modifier + .height(iconSize.applyScaleUp()) + .padding(start = iconStartPadding) + .align(Alignment.Center), + resourceId = iconId, + contentDescription = contentDescription, + // Exception here, we use Color.White instead of ElementTheme.colors.iconOnSolidPrimary + tint = if (canSendMessage) Color.White else ElementTheme.colors.iconDisabled + ) + } + } +} + +@PreviewsDayNight +@Composable +internal fun SendButtonPreview() = ElementPreview { + val normalMode = MessageComposerMode.Normal("") + val editMode = MessageComposerMode.Edit(null, "", null) + Row { + SendButton(canSendMessage = true, onClick = {}, composerMode = normalMode) + SendButton(canSendMessage = false, onClick = {}, composerMode = normalMode) + SendButton(canSendMessage = true, onClick = {}, composerMode = editMode) + SendButton(canSendMessage = false, onClick = {}, composerMode = editMode) + } +} diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/TextFormatting.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/TextFormatting.kt new file mode 100644 index 0000000000..2df01c5f3a --- /dev/null +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/TextFormatting.kt @@ -0,0 +1,215 @@ +/* + * 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.textcomposer.components + +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.rememberScrollState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.utils.CommonDrawables +import io.element.android.libraries.textcomposer.R +import io.element.android.libraries.textcomposer.TextComposerLinkDialog +import io.element.android.wysiwyg.compose.RichTextEditorState +import io.element.android.wysiwyg.view.models.InlineFormat +import io.element.android.wysiwyg.view.models.LinkAction +import kotlinx.coroutines.launch +import uniffi.wysiwyg_composer.ActionState +import uniffi.wysiwyg_composer.ComposerAction +@Composable +internal fun TextFormatting( + state: RichTextEditorState, + modifier: Modifier = Modifier, +) { + + val scrollState = rememberScrollState() + val coroutineScope = rememberCoroutineScope() + + fun onInlineFormatClick(inlineFormat: InlineFormat) { + coroutineScope.launch { + state.toggleInlineFormat(inlineFormat) + } + } + + fun onToggleListClick(ordered: Boolean) { + coroutineScope.launch { + state.toggleList(ordered) + } + } + + fun onIndentClick() { + coroutineScope.launch { + state.indent() + } + } + + fun onUnindentClick() { + coroutineScope.launch { + state.unindent() + } + } + + fun onCodeBlockClick() { + coroutineScope.launch { + state.toggleCodeBlock() + } + } + + fun onQuoteClick() { + coroutineScope.launch { + state.toggleQuote() + } + } + + fun onCreateLinkRequest(url: String, text: String) { + coroutineScope.launch { + state.insertLink(url, text) + } + } + + fun onSaveLinkRequest(url: String) { + coroutineScope.launch { + state.setLink(url) + } + } + + fun onRemoveLinkRequest() { + coroutineScope.launch { + state.removeLink() + } + } + + Row( + modifier = modifier + .horizontalScroll(scrollState), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + FormattingOption( + state = state.actions[ComposerAction.BOLD].toButtonState(), + onClick = { onInlineFormatClick(InlineFormat.Bold) }, + imageVector = ImageVector.vectorResource(CommonDrawables.ic_bold), + contentDescription = stringResource(R.string.rich_text_editor_format_bold) + ) + FormattingOption( + state = state.actions[ComposerAction.ITALIC].toButtonState(), + onClick = { onInlineFormatClick(InlineFormat.Italic) }, + imageVector = ImageVector.vectorResource(CommonDrawables.ic_italic), + contentDescription = stringResource(R.string.rich_text_editor_format_italic) + ) + FormattingOption( + state = state.actions[ComposerAction.UNDERLINE].toButtonState(), + onClick = { onInlineFormatClick(InlineFormat.Underline) }, + imageVector = ImageVector.vectorResource(CommonDrawables.ic_underline), + contentDescription = stringResource(R.string.rich_text_editor_format_underline) + ) + FormattingOption( + state = state.actions[ComposerAction.STRIKE_THROUGH].toButtonState(), + onClick = { onInlineFormatClick(InlineFormat.StrikeThrough) }, + imageVector = ImageVector.vectorResource(CommonDrawables.ic_strikethrough), + contentDescription = stringResource(R.string.rich_text_editor_format_strikethrough) + ) + + var linkDialogAction by remember { mutableStateOf(null) } + + linkDialogAction?.let { + TextComposerLinkDialog( + onDismissRequest = { linkDialogAction = null }, + onCreateLinkRequest = ::onCreateLinkRequest, + onSaveLinkRequest = ::onSaveLinkRequest, + onRemoveLinkRequest = ::onRemoveLinkRequest, + linkAction = it, + ) + } + + FormattingOption( + state = state.actions[ComposerAction.LINK].toButtonState(), + onClick = { linkDialogAction = state.linkAction }, + imageVector = ImageVector.vectorResource(CommonDrawables.ic_link), + contentDescription = stringResource(R.string.rich_text_editor_link) + ) + + FormattingOption( + state = state.actions[ComposerAction.UNORDERED_LIST].toButtonState(), + onClick = { onToggleListClick(ordered = false) }, + imageVector = ImageVector.vectorResource(CommonDrawables.ic_bullet_list), + contentDescription = stringResource(R.string.rich_text_editor_bullet_list) + ) + FormattingOption( + state = state.actions[ComposerAction.ORDERED_LIST].toButtonState(), + onClick = { onToggleListClick(ordered = true) }, + imageVector = ImageVector.vectorResource(CommonDrawables.ic_numbered_list), + contentDescription = stringResource(R.string.rich_text_editor_numbered_list) + ) + FormattingOption( + state = state.actions[ComposerAction.INDENT].toButtonState(), + onClick = { onIndentClick() }, + imageVector = ImageVector.vectorResource(CommonDrawables.ic_indent_increase), + contentDescription = stringResource(R.string.rich_text_editor_indent) + ) + FormattingOption( + state = state.actions[ComposerAction.UNINDENT].toButtonState(), + onClick = { onUnindentClick() }, + imageVector = ImageVector.vectorResource(CommonDrawables.ic_indent_decrease), + contentDescription = stringResource(R.string.rich_text_editor_unindent) + ) + FormattingOption( + state = state.actions[ComposerAction.INLINE_CODE].toButtonState(), + onClick = { onInlineFormatClick(InlineFormat.InlineCode) }, + imageVector = ImageVector.vectorResource(CommonDrawables.ic_inline_code), + contentDescription = stringResource(R.string.rich_text_editor_inline_code) + ) + FormattingOption( + state = state.actions[ComposerAction.CODE_BLOCK].toButtonState(), + onClick = { onCodeBlockClick() }, + imageVector = ImageVector.vectorResource(CommonDrawables.ic_code_block), + contentDescription = stringResource(R.string.rich_text_editor_code_block) + ) + FormattingOption( + state = state.actions[ComposerAction.QUOTE].toButtonState(), + onClick = { onQuoteClick() }, + imageVector = ImageVector.vectorResource(CommonDrawables.ic_quote), + contentDescription = stringResource(R.string.rich_text_editor_quote) + ) + } +} + +private fun ActionState?.toButtonState(): FormattingOptionState = + when (this) { + ActionState.ENABLED -> FormattingOptionState.Default + ActionState.REVERSED -> FormattingOptionState.Selected + ActionState.DISABLED, null -> FormattingOptionState.Disabled + } + +@PreviewsDayNight +@Composable +internal fun TextFormattingPreview() = ElementPreview { + TextFormatting(state = RichTextEditorState()) +} diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_ComposerOptionsButton-D-7_7_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_ComposerOptionsButton-D-7_7_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d90adf9d9e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_ComposerOptionsButton-D-7_7_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:377c36c5f1019bb6838749fd93c91dec703da825f75e5600ef028d2aaeaa79ac +size 5646 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_ComposerOptionsButton-N-7_8_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_ComposerOptionsButton-N-7_8_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..073873fe76 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_ComposerOptionsButton-N-7_8_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6bbe1dbe62743ca682a48e9e777e8b2b09064afeb2ce36448a62452faaca5d67 +size 5629 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_DismissTextFormattingButton-D-8_8_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_DismissTextFormattingButton-D-8_8_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ffb2d2a6e1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_DismissTextFormattingButton-D-8_8_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb2d844445877bdd615c4b9fcedd9759ccf4fa7e72b8a740a74af18f8b7d18f8 +size 5925 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_DismissTextFormattingButton-N-8_9_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_DismissTextFormattingButton-N-8_9_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..94f6a515a8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_DismissTextFormattingButton-N-8_9_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e3fb36aa1c559c6d3ae8ca2399531b5090229131ba23b710d46639d3a26b6ec +size 5847 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_FormattingButton-D-7_7_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_FormattingButton-D-9_9_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_FormattingButton-D-7_7_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_FormattingButton-D-9_9_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_FormattingButton-N-7_8_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_FormattingButton-N-9_10_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_FormattingButton-N-7_8_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_FormattingButton-N-9_10_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-D-10_10_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-D-10_10_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d32c73e543 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-D-10_10_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60410832f66d0c70166443d855743ef956b1b97a3684624ce5797805d71e04f6 +size 8712 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-N-10_11_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-N-10_11_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..93591c72a9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-N-10_11_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54c5208fd88e1f404667cdc0b39ff87824115a1c414dfdbd55a334fa6a445548 +size 8619 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-D-11_11_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-D-11_11_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..635fbd2b36 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-D-11_11_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b43968125ef27b3b9918251590befe6e6a7d781342e8d915833fa1cba395f17d +size 7211 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-N-11_12_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-N-11_12_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..361795f953 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-N-11_12_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7ea766f311321684f8a387a7be9e2f88a4f27a4adb85a9b0f4d0b3e56854ee0 +size 7034