mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 12:07:28 +00:00
New timeline event renderer: add new modules (#7721)
* Added HTML parser to abstract tree nodes * Add HTML renderer using `HtmlMessageContent` * Add custom rendering for inline code nodes besides the monospace font * Support `<s>` and `<strike>` too * Add `ContentAvoidingLayoutData` support * Improve the last block node behaviour and its integration with `ContentAvoidingLayout` * Move `ContentAvoidingLayout` to `:libraries:ui-common` so it can be shared * Add missing `:libraries:htmlrenderer:impl` entry in `DependencyHandlerScope.allLibrariesImpl` * Update screenshots --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
committed by
GitHub
parent
97d9707d6a
commit
c0bb68c872
@@ -59,6 +59,7 @@ dependencies {
|
||||
implementation(projects.libraries.voicerecorder.api)
|
||||
implementation(projects.libraries.mediaplayer.api)
|
||||
implementation(projects.libraries.push.api)
|
||||
implementation(projects.libraries.uiCommon)
|
||||
implementation(projects.libraries.uiUtils)
|
||||
implementation(projects.libraries.testtags)
|
||||
implementation(projects.libraries.wellknown.api)
|
||||
|
||||
+1
-1
@@ -36,7 +36,6 @@ import io.element.android.features.messages.impl.timeline.components.TimelineIte
|
||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionBottomSheet
|
||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvent
|
||||
import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryEvent
|
||||
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryView
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
@@ -58,6 +57,7 @@ import io.element.android.libraries.emoji.api.picker.EmojiPickerRenderer
|
||||
import io.element.android.libraries.emoji.api.picker.NoOpEmojiPickerRenderer
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.ui.common.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.analytics.compose.LocalAnalyticsService
|
||||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||
|
||||
+2
-2
@@ -72,8 +72,6 @@ import io.element.android.features.messages.impl.timeline.TimelineEvent
|
||||
import io.element.android.features.messages.impl.timeline.TimelineRoomInfo
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||
import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayout
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.features.messages.impl.timeline.components.receipt.ReadReceiptViewState
|
||||
import io.element.android.features.messages.impl.timeline.components.receipt.TimelineItemReadReceiptView
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
@@ -139,6 +137,8 @@ import io.element.android.libraries.matrix.ui.messages.sender.SenderName
|
||||
import io.element.android.libraries.matrix.ui.messages.sender.SenderNameMode
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
import io.element.android.libraries.testtags.testTag
|
||||
import io.element.android.libraries.ui.common.layout.ContentAvoidingLayout
|
||||
import io.element.android.libraries.ui.common.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.libraries.ui.strings.CommonPlurals
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
|
||||
|
||||
+1
-1
@@ -23,7 +23,6 @@ import io.element.android.features.messages.impl.timeline.aRedactedMessagesGroup
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineRoomInfo
|
||||
import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView
|
||||
import io.element.android.features.messages.impl.timeline.components.group.GroupHeaderView
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.features.messages.impl.timeline.components.receipt.ReadReceiptViewState
|
||||
import io.element.android.features.messages.impl.timeline.components.receipt.TimelineItemReadReceiptView
|
||||
import io.element.android.features.messages.impl.timeline.groups.isRedactedMessagesGroup
|
||||
@@ -35,6 +34,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.ui.common.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.wysiwyg.link.Link
|
||||
|
||||
@Composable
|
||||
|
||||
+1
-1
@@ -29,7 +29,6 @@ import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.messages.impl.timeline.TimelineEvent
|
||||
import io.element.android.features.messages.impl.timeline.TimelineRoomInfo
|
||||
import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.event.RtcNotificationState
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent
|
||||
@@ -46,6 +45,7 @@ import io.element.android.libraries.designsystem.text.toPx
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.ui.common.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
|
||||
import io.element.android.wysiwyg.link.Link
|
||||
|
||||
+2
-2
@@ -22,12 +22,12 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayout
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationValue
|
||||
import io.element.android.libraries.ui.common.layout.ContentAvoidingLayout
|
||||
import io.element.android.libraries.ui.common.layout.ContentAvoidingLayoutData
|
||||
|
||||
/**
|
||||
* package-private, you should only use TimelineItemFileView and TimelineItemAudioView.
|
||||
|
||||
+1
-1
@@ -12,11 +12,11 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContentPreviewParam
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationValue
|
||||
import io.element.android.libraries.ui.common.layout.ContentAvoidingLayoutData
|
||||
|
||||
@Composable
|
||||
fun TimelineItemAudioView(
|
||||
|
||||
+1
-1
@@ -12,7 +12,6 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContentPreviewParam
|
||||
import io.element.android.libraries.designsystem.icons.CompoundDrawables
|
||||
@@ -20,6 +19,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.UtdCause
|
||||
import io.element.android.libraries.ui.common.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
|
||||
+2
-2
@@ -24,8 +24,6 @@ import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.messages.impl.timeline.TimelineEvent
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayout
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
|
||||
import io.element.android.features.messages.impl.timeline.di.rememberPresenter
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAttachmentsContent
|
||||
@@ -60,6 +58,8 @@ import io.element.android.libraries.matrix.ui.media.contentvalidation.NotFoundCo
|
||||
import io.element.android.libraries.matrix.ui.media.contentvalidation.collectOverallState
|
||||
import io.element.android.libraries.matrix.ui.media.contentvalidation.rememberEventContentValidationState
|
||||
import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle
|
||||
import io.element.android.libraries.ui.common.layout.ContentAvoidingLayout
|
||||
import io.element.android.libraries.ui.common.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.libraries.voiceplayer.api.VoiceMessageState
|
||||
import io.element.android.wysiwyg.compose.EditorStyledText
|
||||
import io.element.android.wysiwyg.link.Link
|
||||
|
||||
+1
-1
@@ -13,11 +13,11 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContentPreviewParam
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationValue
|
||||
import io.element.android.libraries.ui.common.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -22,12 +22,12 @@ import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.libraries.designsystem.icons.CompoundDrawables
|
||||
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.Text
|
||||
import io.element.android.libraries.ui.common.layout.ContentAvoidingLayoutData
|
||||
|
||||
@Composable
|
||||
fun TimelineItemInformativeView(
|
||||
|
||||
+1
-1
@@ -11,11 +11,11 @@ package io.element.android.features.messages.impl.timeline.components.event
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
||||
import io.element.android.libraries.designsystem.icons.CompoundDrawables
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.ui.common.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
|
||||
+2
-2
@@ -23,8 +23,6 @@ import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayout
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.features.messages.impl.timeline.model.event.AN_EMOJI_ONLY_TEXT
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContentPreviewParam
|
||||
@@ -36,6 +34,8 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.utils.LocalUiTestMode
|
||||
import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle
|
||||
import io.element.android.libraries.textcomposer.mentions.LocalMentionSpanUpdater
|
||||
import io.element.android.libraries.ui.common.layout.ContentAvoidingLayout
|
||||
import io.element.android.libraries.ui.common.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.wysiwyg.compose.EditorStyledText
|
||||
import io.element.android.wysiwyg.link.Link
|
||||
|
||||
|
||||
+1
-1
@@ -11,11 +11,11 @@ package io.element.android.features.messages.impl.timeline.components.event
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
|
||||
import io.element.android.libraries.designsystem.icons.CompoundDrawables
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.ui.common.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
|
||||
+1
-1
@@ -40,7 +40,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContentPreviewParam
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.PlaybackSpeedButton
|
||||
@@ -52,6 +51,7 @@ 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.matrix.ui.media.contentvalidation.ContentValidationValue
|
||||
import io.element.android.libraries.ui.common.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
|
||||
import io.element.android.libraries.voiceplayer.api.VoiceMessageEvent
|
||||
|
||||
-209
@@ -1,209 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.impl.timeline.components.layout
|
||||
|
||||
import android.text.Layout
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.text.TextLayoutResult
|
||||
import androidx.compose.ui.text.style.ResolvedTextDirection
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.core.data.tryOrNull
|
||||
import io.element.android.libraries.designsystem.text.roundToPx
|
||||
import io.element.android.libraries.designsystem.utils.LocalUiTestMode
|
||||
import io.element.android.wysiwyg.compose.EditorStyledText
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* A layout with 2 children: the [content] and the [overlay].
|
||||
*
|
||||
* It will try to place the [overlay] on top of the [content] if possible, avoiding the area of it that is non-overlapping.
|
||||
* If the [overlay] can't be placed on top of the [content], it will be placed to the right of it, if it fits, otherwise, to its bottom in a new row.
|
||||
*
|
||||
* @param overlay The 'overlay' component of the layout, which will be positioned relative to the [content].
|
||||
* @param modifier The modifier for the layout.
|
||||
* @param spacing The spacing between the [content] and the [overlay]. Defaults to `0.dp`.
|
||||
* @param overlayOffset The offset of the [overlay] from the bottom right corner of the [content].
|
||||
* @param shrinkContent Whether the content should be shrunk to fit the available width or not. Defaults to `false`.
|
||||
* @param content The 'content' component of the layout.
|
||||
*/
|
||||
@Suppress("ContentSlotReused") // Since we added an exception for `LocalUiTestMode`, detekt thinks the layout can change in runtime: it won't
|
||||
@Composable
|
||||
fun ContentAvoidingLayout(
|
||||
overlay: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
spacing: Dp = 0.dp,
|
||||
overlayOffset: DpOffset = DpOffset.Zero,
|
||||
shrinkContent: Boolean = false,
|
||||
content: @Composable ContentAvoidingLayoutScope.() -> Unit,
|
||||
) {
|
||||
val scope = remember { ContentAvoidingLayoutScopeInstance() }
|
||||
|
||||
// Custom layouts don't seem to work well with Compose UI tests (they crash), so we use a Column instead when running in test mode.
|
||||
if (LocalUiTestMode.current) {
|
||||
Column {
|
||||
scope.content()
|
||||
overlay()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Layout(
|
||||
modifier = modifier,
|
||||
content = {
|
||||
scope.content()
|
||||
overlay()
|
||||
}
|
||||
) { measurables, constraints ->
|
||||
|
||||
// Measure the `overlay` view first, in case we need to shrink the `content`
|
||||
val overlayPlaceable = measurables.last().measure(Constraints(minWidth = 0, maxWidth = constraints.maxWidth))
|
||||
val contentConstraints = if (shrinkContent) {
|
||||
Constraints(minWidth = 0, maxWidth = constraints.maxWidth - overlayPlaceable.width)
|
||||
} else {
|
||||
Constraints(minWidth = 0, maxWidth = constraints.maxWidth)
|
||||
}
|
||||
val contentPlaceable = measurables.first().measure(contentConstraints)
|
||||
|
||||
var layoutWidth = contentPlaceable.width
|
||||
var layoutHeight = contentPlaceable.height
|
||||
|
||||
val data = scope.data.value
|
||||
|
||||
// Free space = width of the whole component - width of its non overlapping contents
|
||||
val freeSpace = max(contentPlaceable.width - data.nonOverlappingContentWidth, 0)
|
||||
|
||||
when {
|
||||
// When the content + the overlay don't fit in the available max width, we need to move the overlay to a new row
|
||||
!shrinkContent && data.nonOverlappingContentWidth + overlayPlaceable.width > constraints.maxWidth -> {
|
||||
layoutHeight += overlayPlaceable.height + overlayOffset.y.roundToPx()
|
||||
}
|
||||
// If the content is smaller than the available max width, we can move the overlay to the right of the content
|
||||
contentPlaceable.width < constraints.maxWidth -> {
|
||||
// If both the content and the overlay plus the padding can fit inside the current layoutWidth, there is no need to increase it
|
||||
if (freeSpace < overlayPlaceable.width + spacing.roundToPx()) {
|
||||
// Otherwise, we need to increase it by the width of the overlay + some padding adjustments
|
||||
val calculatedWidth = max(data.nonOverlappingContentWidth + overlayPlaceable.width + spacing.roundToPx(), contentPlaceable.width)
|
||||
layoutWidth = min(calculatedWidth, constraints.maxWidth)
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
layoutWidth = max(layoutWidth, constraints.minWidth)
|
||||
layoutHeight = max(layoutHeight, constraints.minHeight)
|
||||
|
||||
layout(layoutWidth, layoutHeight) {
|
||||
contentPlaceable.placeRelative(0, 0)
|
||||
overlayPlaceable.placeRelative(layoutWidth - overlayPlaceable.width, layoutHeight - overlayPlaceable.height + overlayOffset.y.roundToPx())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data class to hold the content layout data.
|
||||
* This is used to pass the data from the content to the [ContentAvoidingLayout].
|
||||
*
|
||||
* @param contentWidth The full width of the content in pixels.
|
||||
* @param contentHeight The full height of the content in pixels.
|
||||
* @param nonOverlappingContentWidth The width of the part of the content that can't overlap with the timestamp.
|
||||
* @param nonOverlappingContentHeight The height of the part of the content that can't overlap with the timestamp.
|
||||
*/
|
||||
data class ContentAvoidingLayoutData(
|
||||
val contentWidth: Int = 0,
|
||||
val contentHeight: Int = 0,
|
||||
val nonOverlappingContentWidth: Int = contentWidth,
|
||||
val nonOverlappingContentHeight: Int = contentHeight,
|
||||
)
|
||||
|
||||
/**
|
||||
* A scope for the [ContentAvoidingLayout].
|
||||
*/
|
||||
interface ContentAvoidingLayoutScope {
|
||||
/**
|
||||
* It should be called when the content layout changes, so it can update the [ContentAvoidingLayoutData] and measure and layout the content properly.
|
||||
*/
|
||||
fun onContentLayoutChange(data: ContentAvoidingLayoutData)
|
||||
}
|
||||
|
||||
private class ContentAvoidingLayoutScopeInstance(
|
||||
val data: MutableState<ContentAvoidingLayoutData> = mutableStateOf(ContentAvoidingLayoutData()),
|
||||
) : ContentAvoidingLayoutScope {
|
||||
override fun onContentLayoutChange(data: ContentAvoidingLayoutData) {
|
||||
this.data.value = data
|
||||
}
|
||||
}
|
||||
|
||||
object ContentAvoidingLayout {
|
||||
/**
|
||||
* Measures the last line of a [TextLayoutResult] and calls [onContentLayoutChange] with the [ContentAvoidingLayoutData].
|
||||
*
|
||||
* This is supposed to be used in the `onTextLayout` parameter of a Text based component.
|
||||
*/
|
||||
@Composable
|
||||
internal fun measureLastTextLine(
|
||||
onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit,
|
||||
extraWidth: Dp = 0.dp,
|
||||
): ((TextLayoutResult) -> Unit) {
|
||||
val extraWidthPx = extraWidth.roundToPx()
|
||||
return { textLayout: TextLayoutResult ->
|
||||
// We need to add the external extra width so it's not taken into account as 'free space'
|
||||
val textDirection = tryOrNull { textLayout.getParagraphDirection(0) }
|
||||
val lastLineWidth = when (textDirection) {
|
||||
ResolvedTextDirection.Rtl -> textLayout.getLineLeft(textLayout.lineCount - 1).roundToInt()
|
||||
else -> textLayout.getLineRight(textLayout.lineCount - 1).roundToInt()
|
||||
}
|
||||
val lastLineHeight = textLayout.getLineBottom(textLayout.lineCount - 1).roundToInt()
|
||||
onContentLayoutChange(
|
||||
ContentAvoidingLayoutData(
|
||||
contentWidth = textLayout.size.width + extraWidthPx,
|
||||
contentHeight = textLayout.size.height,
|
||||
nonOverlappingContentWidth = lastLineWidth + extraWidthPx,
|
||||
nonOverlappingContentHeight = lastLineHeight,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Measures the last line of a [Layout] and calls [onContentLayoutChange] with the [ContentAvoidingLayoutData].
|
||||
*
|
||||
* This is supposed to be used in the `onTextLayout` parameter of an [EditorStyledText] component.
|
||||
*/
|
||||
@Composable
|
||||
internal fun measureLegacyLastTextLine(
|
||||
onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit,
|
||||
extraWidth: Dp = 0.dp,
|
||||
): ((Layout) -> Unit) {
|
||||
val extraWidthPx = extraWidth.roundToPx()
|
||||
return { textLayout: Layout ->
|
||||
// We need to add the external extra width so it's not taken into account as 'free space'
|
||||
val lastLineWidth = textLayout.getLineWidth(textLayout.lineCount - 1).roundToInt()
|
||||
val lastLineHeight = textLayout.getLineBottom(textLayout.lineCount - 1)
|
||||
onContentLayoutChange(
|
||||
ContentAvoidingLayoutData(
|
||||
contentWidth = textLayout.width + extraWidthPx,
|
||||
contentHeight = textLayout.height,
|
||||
nonOverlappingContentWidth = lastLineWidth + extraWidthPx,
|
||||
nonOverlappingContentHeight = lastLineHeight,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user