fix(paginator): resolve page overflow and layout mismatches (#34)
- Enforce strict height and aspect ratio for Math and Image blocks. - Align Paragraph padding, width math, and text alignment with the UI. - Strip bottom margins on last blocks and use roundToInt for precision. - Add expectedHeight diagnostics to detect and log UI overflows.
This commit is contained in:
parent
7e28c1c2b3
commit
506f60473c
3 changed files with 392 additions and 200 deletions
|
|
@ -44,6 +44,7 @@ import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
|
@ -1295,6 +1296,25 @@ private fun TextWithEmphasis(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("BinaryOperationInTimber")
|
||||||
|
private fun checkLayoutMismatch(
|
||||||
|
blockIndex: Int,
|
||||||
|
blockType: String,
|
||||||
|
expectedHeight: Int,
|
||||||
|
actualHeight: Int,
|
||||||
|
tolerance: Int = 2
|
||||||
|
) {
|
||||||
|
if (actualHeight > expectedHeight + tolerance) {
|
||||||
|
val diff = actualHeight - expectedHeight
|
||||||
|
Timber.tag("PAGINATION_MISMATCH").e(
|
||||||
|
"OVERFLOW DETECTED! Block #$blockIndex ($blockType)\n" +
|
||||||
|
" -> Expected: ${expectedHeight}px\n" +
|
||||||
|
" -> Actual: ${actualHeight}px\n" +
|
||||||
|
" -> Diff: +${diff}px"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("UnusedBoxWithConstraintsScope")
|
@SuppressLint("UnusedBoxWithConstraintsScope")
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
|
@ -1516,18 +1536,16 @@ internal fun PaginatedReaderContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
val searchHighlightColor =
|
val searchHighlightColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.4f)
|
||||||
MaterialTheme.colorScheme.primary.copy(alpha = 0.4f)
|
val ttsHighlightColor = MaterialTheme.colorScheme.secondary.copy(alpha = 0.5f)
|
||||||
val ttsHighlightColor =
|
|
||||||
MaterialTheme.colorScheme.secondary.copy(alpha = 0.5f)
|
|
||||||
pageContent!!.content.forEach { block ->
|
pageContent!!.content.forEach { block ->
|
||||||
val marginModifier = Modifier.padding(
|
val marginModifier = Modifier.padding(
|
||||||
top = block.style.margin.top.coerceAtLeast(0.dp),
|
top = block.style.margin.top.coerceAtLeast(0.dp),
|
||||||
bottom = block.style.margin.bottom.coerceAtLeast(0.dp)
|
bottom = block.style.margin.bottom.coerceAtLeast(0.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
val alignModifier =
|
val alignModifier = if (block.style.horizontalAlign == "center") {
|
||||||
if (block.style.horizontalAlign == "center") {
|
|
||||||
Modifier.align(Alignment.CenterHorizontally)
|
Modifier.align(Alignment.CenterHorizontally)
|
||||||
} else {
|
} else {
|
||||||
Modifier.padding(
|
Modifier.padding(
|
||||||
|
|
@ -1536,8 +1554,7 @@ internal fun PaginatedReaderContent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val widthModifier =
|
val widthModifier = if (block.style.width != Dp.Unspecified) {
|
||||||
if (block.style.width != Dp.Unspecified) {
|
|
||||||
Modifier.width(block.style.width)
|
Modifier.width(block.style.width)
|
||||||
} else {
|
} else {
|
||||||
Modifier.fillMaxWidth()
|
Modifier.fillMaxWidth()
|
||||||
|
|
@ -1545,61 +1562,58 @@ internal fun PaginatedReaderContent(
|
||||||
|
|
||||||
val boxModifier = marginModifier
|
val boxModifier = marginModifier
|
||||||
.then(alignModifier)
|
.then(alignModifier)
|
||||||
|
.then(if (block.style.horizontalAlign == "center") widthModifier else Modifier)
|
||||||
.then(
|
.then(
|
||||||
if (block.style.horizontalAlign == "center") widthModifier
|
if (block.style.borderRadius > 0.dp) Modifier.clip(RoundedCornerShape(block.style.borderRadius))
|
||||||
else Modifier
|
else Modifier
|
||||||
)
|
)
|
||||||
.then(
|
|
||||||
if (block.style.borderRadius > 0.dp) {
|
|
||||||
Modifier.clip(RoundedCornerShape(block.style.borderRadius))
|
|
||||||
} else Modifier
|
|
||||||
)
|
|
||||||
.then(
|
.then(
|
||||||
if (block.style.backgroundColor.isSpecified) {
|
if (block.style.backgroundColor.isSpecified) {
|
||||||
Modifier.background(
|
Modifier.background(
|
||||||
block.style.backgroundColor,
|
block.style.backgroundColor,
|
||||||
shape = if (block.style.borderRadius > 0.dp) RoundedCornerShape(
|
shape = if (block.style.borderRadius > 0.dp) RoundedCornerShape(block.style.borderRadius) else androidx.compose.ui.graphics.RectangleShape
|
||||||
block.style.borderRadius
|
|
||||||
) else androidx.compose.ui.graphics.RectangleShape
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Modifier
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then(block.style.border?.let { border ->
|
|
||||||
Modifier.border(
|
|
||||||
BorderStroke(
|
|
||||||
border.width, border.color
|
|
||||||
),
|
|
||||||
shape = if (block.style.borderRadius > 0.dp) RoundedCornerShape(
|
|
||||||
block.style.borderRadius
|
|
||||||
) else androidx.compose.ui.graphics.RectangleShape
|
|
||||||
)
|
|
||||||
} ?: Modifier)
|
|
||||||
|
|
||||||
Box(modifier = boxModifier) {
|
|
||||||
val paddingModifier = Modifier
|
|
||||||
.padding(
|
|
||||||
start = block.style.padding.left.coerceAtLeast(
|
|
||||||
0.dp
|
|
||||||
),
|
|
||||||
top = block.style.padding.top.coerceAtLeast(
|
|
||||||
0.dp
|
|
||||||
),
|
|
||||||
end = block.style.padding.right.coerceAtLeast(
|
|
||||||
0.dp
|
|
||||||
),
|
|
||||||
bottom = block.style.padding.bottom.coerceAtLeast(
|
|
||||||
0.dp
|
|
||||||
)
|
)
|
||||||
|
} else Modifier
|
||||||
)
|
)
|
||||||
.then(
|
.then(
|
||||||
if (block.style.horizontalAlign != "center") widthModifier
|
block.style.border?.let { border ->
|
||||||
else Modifier.fillMaxWidth()
|
Modifier.border(
|
||||||
|
BorderStroke(border.width, border.color),
|
||||||
|
shape = if (block.style.borderRadius > 0.dp) RoundedCornerShape(block.style.borderRadius) else androidx.compose.ui.graphics.RectangleShape
|
||||||
|
)
|
||||||
|
} ?: Modifier
|
||||||
|
)
|
||||||
|
|
||||||
|
val diagnosticModifier = Modifier
|
||||||
|
.onGloballyPositioned { coordinates ->
|
||||||
|
val actualHeight = coordinates.size.height
|
||||||
|
if (block.expectedHeight > 0) {
|
||||||
|
checkLayoutMismatch(
|
||||||
|
blockIndex = block.blockIndex,
|
||||||
|
blockType = block::class.simpleName ?: "Block",
|
||||||
|
expectedHeight = block.expectedHeight,
|
||||||
|
actualHeight = actualHeight
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.then(boxModifier)
|
||||||
|
|
||||||
|
Box(modifier = diagnosticModifier) {
|
||||||
|
val borderWidth = block.style.border?.width ?: 0.dp
|
||||||
|
val paddingModifier = Modifier.padding(
|
||||||
|
start = block.style.padding.left.coerceAtLeast(0.dp) + borderWidth,
|
||||||
|
top = block.style.padding.top.coerceAtLeast(0.dp) + borderWidth,
|
||||||
|
end = block.style.padding.right.coerceAtLeast(0.dp) + borderWidth,
|
||||||
|
bottom = block.style.padding.bottom.coerceAtLeast(0.dp) + borderWidth
|
||||||
|
).then(
|
||||||
|
if (block.style.horizontalAlign != "center") widthModifier else Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
|
|
||||||
@Suppress("DEPRECATION") when (block) {
|
@Suppress("DEPRECATION") when (block) {
|
||||||
is ParagraphBlock -> {
|
is ParagraphBlock -> {
|
||||||
|
val paragraphStyle = textStyle.copy(
|
||||||
|
textAlign = block.textAlign ?: textStyle.textAlign
|
||||||
|
)
|
||||||
val searchHighlighted = highlightQueryInText(
|
val searchHighlighted = highlightQueryInText(
|
||||||
block.content,
|
block.content,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
|
|
@ -1648,7 +1662,7 @@ internal fun PaginatedReaderContent(
|
||||||
searchHighlighted
|
searchHighlighted
|
||||||
}
|
}
|
||||||
|
|
||||||
val diagnosticModifier =
|
@Suppress("UnusedVariable", "Unused") val diagnosticModifier =
|
||||||
if (block.textAlign == TextAlign.Justify) {
|
if (block.textAlign == TextAlign.Justify) {
|
||||||
Modifier.onGloballyPositioned { coordinates ->
|
Modifier.onGloballyPositioned { coordinates ->
|
||||||
val width = coordinates.size.width
|
val width = coordinates.size.width
|
||||||
|
|
@ -1672,10 +1686,8 @@ internal fun PaginatedReaderContent(
|
||||||
|
|
||||||
TextWithEmphasis(
|
TextWithEmphasis(
|
||||||
text = finalContent,
|
text = finalContent,
|
||||||
style = textStyle,
|
style = paragraphStyle,
|
||||||
modifier = paddingModifier.then(
|
modifier = paddingModifier,
|
||||||
diagnosticModifier
|
|
||||||
),
|
|
||||||
textMeasurer = textMeasurer,
|
textMeasurer = textMeasurer,
|
||||||
onLinkClick = onLinkClickCallback,
|
onLinkClick = onLinkClickCallback,
|
||||||
onGeneralTap = onGeneralTapCallback,
|
onGeneralTap = onGeneralTapCallback,
|
||||||
|
|
@ -1704,7 +1716,8 @@ internal fun PaginatedReaderContent(
|
||||||
|
|
||||||
is HeaderBlock -> {
|
is HeaderBlock -> {
|
||||||
val style = textStyle.copy(
|
val style = textStyle.copy(
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold,
|
||||||
|
textAlign = block.textAlign ?: textStyle.textAlign
|
||||||
)
|
)
|
||||||
val searchHighlighted = highlightQueryInText(
|
val searchHighlighted = highlightQueryInText(
|
||||||
block.content,
|
block.content,
|
||||||
|
|
@ -1780,6 +1793,9 @@ internal fun PaginatedReaderContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
is QuoteBlock -> {
|
is QuoteBlock -> {
|
||||||
|
val quoteStyle = textStyle.copy(
|
||||||
|
textAlign = block.textAlign ?: textStyle.textAlign
|
||||||
|
)
|
||||||
val quoteModifier =
|
val quoteModifier =
|
||||||
paddingModifier.padding(start = 16.dp)
|
paddingModifier.padding(start = 16.dp)
|
||||||
val searchHighlighted = highlightQueryInText(
|
val searchHighlighted = highlightQueryInText(
|
||||||
|
|
@ -1827,7 +1843,7 @@ internal fun PaginatedReaderContent(
|
||||||
}
|
}
|
||||||
TextWithEmphasis(
|
TextWithEmphasis(
|
||||||
text = finalContent,
|
text = finalContent,
|
||||||
style = textStyle,
|
style = quoteStyle,
|
||||||
modifier = quoteModifier,
|
modifier = quoteModifier,
|
||||||
textMeasurer = textMeasurer,
|
textMeasurer = textMeasurer,
|
||||||
onLinkClick = onLinkClickCallback,
|
onLinkClick = onLinkClickCallback,
|
||||||
|
|
@ -2112,21 +2128,38 @@ internal fun PaginatedReaderContent(
|
||||||
containerWidthPx,
|
containerWidthPx,
|
||||||
localDensity
|
localDensity
|
||||||
)
|
)
|
||||||
val imageModifier =
|
val heightPx = parseSvgDimension(
|
||||||
|
block.svgHeight,
|
||||||
|
fontSizePx,
|
||||||
|
containerWidthPx,
|
||||||
|
localDensity
|
||||||
|
)
|
||||||
|
|
||||||
|
var imageModifier: Modifier = Modifier
|
||||||
if (widthPx != null) {
|
if (widthPx != null) {
|
||||||
val finalWidthDp =
|
val finalWidthDp = with(localDensity) { widthPx.toDp() }
|
||||||
with(localDensity) {
|
Timber.d("Applying calculated width to MathBlock image: $finalWidthDp")
|
||||||
widthPx.toDp()
|
imageModifier = imageModifier.width(finalWidthDp)
|
||||||
}
|
|
||||||
Timber.d(
|
|
||||||
"Applying calculated width to MathBlock image: $finalWidthDp"
|
|
||||||
)
|
|
||||||
Modifier.width(finalWidthDp)
|
|
||||||
} else {
|
} else {
|
||||||
Timber.w(
|
Timber.w("Could not calculate a specific width for MathBlock. It will fill available space.")
|
||||||
"Could not calculate a specific width for MathBlock. It will fill available space."
|
imageModifier = imageModifier.fillMaxWidth()
|
||||||
)
|
}
|
||||||
Modifier
|
|
||||||
|
if (heightPx != null) {
|
||||||
|
val finalHeightDp = with(localDensity) { heightPx.toDp() }
|
||||||
|
Timber.d("Applying calculated height to MathBlock image: $finalHeightDp")
|
||||||
|
imageModifier = imageModifier.height(finalHeightDp)
|
||||||
|
} else {
|
||||||
|
val viewBoxParts = block.svgViewBox?.split(' ', ',')?.mapNotNull { it.toFloatOrNull() }
|
||||||
|
if (viewBoxParts != null && viewBoxParts.size == 4 && viewBoxParts[2] > 0) {
|
||||||
|
val aspectRatio = viewBoxParts[3] / viewBoxParts[2]
|
||||||
|
val effectiveWidth = widthPx ?: containerWidthPx.toFloat()
|
||||||
|
val finalHeightDp = with(localDensity) { (effectiveWidth * aspectRatio).toDp() }
|
||||||
|
imageModifier = imageModifier.height(finalHeightDp)
|
||||||
|
} else {
|
||||||
|
val fallbackHeightDp = with(localDensity) { (textStyle.fontSize.value * 3).sp.toDp() }
|
||||||
|
imageModifier = imageModifier.height(fallbackHeightDp)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val imageRequest =
|
val imageRequest =
|
||||||
|
|
@ -2186,6 +2219,15 @@ internal fun PaginatedReaderContent(
|
||||||
)
|
)
|
||||||
else Modifier
|
else Modifier
|
||||||
)
|
)
|
||||||
|
.then(
|
||||||
|
if (block.intrinsicWidth != null && block.intrinsicHeight != null && block.intrinsicWidth > 0f && block.intrinsicHeight > 0f) {
|
||||||
|
Modifier.aspectRatio(block.intrinsicWidth / block.intrinsicHeight, matchHeightConstraintsFirst = false)
|
||||||
|
} else if (style.height != Dp.Unspecified) {
|
||||||
|
Modifier.height(style.height)
|
||||||
|
} else {
|
||||||
|
Modifier.height(250.dp)
|
||||||
|
}
|
||||||
|
)
|
||||||
.then(paddingModifier)
|
.then(paddingModifier)
|
||||||
|
|
||||||
val colorFilter =
|
val colorFilter =
|
||||||
|
|
@ -2450,6 +2492,15 @@ internal fun PaginatedReaderContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
is ImageBlock -> {
|
is ImageBlock -> {
|
||||||
|
val imageModifier = Modifier.fillMaxWidth().then(
|
||||||
|
if (blockInCell.intrinsicWidth != null && blockInCell.intrinsicHeight != null && blockInCell.intrinsicWidth > 0f && blockInCell.intrinsicHeight > 0f) {
|
||||||
|
Modifier.aspectRatio(blockInCell.intrinsicWidth / blockInCell.intrinsicHeight, matchHeightConstraintsFirst = false)
|
||||||
|
} else if (blockInCell.style.height != Dp.Unspecified) {
|
||||||
|
Modifier.height(blockInCell.style.height)
|
||||||
|
} else {
|
||||||
|
Modifier.height(250.dp)
|
||||||
|
}
|
||||||
|
)
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = Builder(
|
model = Builder(
|
||||||
LocalContext.current
|
LocalContext.current
|
||||||
|
|
@ -2460,12 +2511,9 @@ internal fun PaginatedReaderContent(
|
||||||
).build(),
|
).build(),
|
||||||
contentDescription = blockInCell.altText,
|
contentDescription = blockInCell.altText,
|
||||||
contentScale = ContentScale.Fit,
|
contentScale = ContentScale.Fit,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = imageModifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Catch-all for any
|
|
||||||
// other text-based
|
|
||||||
// content
|
|
||||||
is TextContentBlock -> {
|
is TextContentBlock -> {
|
||||||
Text(
|
Text(
|
||||||
text = blockInCell.content,
|
text = blockInCell.content,
|
||||||
|
|
@ -3178,6 +3226,15 @@ private fun RenderFlexChildBlock(
|
||||||
if (style.maxWidth != Dp.Unspecified) Modifier.widthIn(max = style.maxWidth)
|
if (style.maxWidth != Dp.Unspecified) Modifier.widthIn(max = style.maxWidth)
|
||||||
else Modifier
|
else Modifier
|
||||||
)
|
)
|
||||||
|
.then(
|
||||||
|
if (childBlock.intrinsicWidth != null && childBlock.intrinsicHeight != null && childBlock.intrinsicWidth > 0f && childBlock.intrinsicHeight > 0f) {
|
||||||
|
Modifier.aspectRatio(childBlock.intrinsicWidth / childBlock.intrinsicHeight, matchHeightConstraintsFirst = false)
|
||||||
|
} else if (style.height != Dp.Unspecified) {
|
||||||
|
Modifier.height(style.height)
|
||||||
|
} else {
|
||||||
|
Modifier.height(250.dp)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
val colorFilter = if (childBlock.style.filter == "invert(100%)") {
|
val colorFilter = if (childBlock.style.filter == "invert(100%)") {
|
||||||
val matrix = floatArrayOf(
|
val matrix = floatArrayOf(
|
||||||
|
|
@ -3295,6 +3352,15 @@ private fun RenderFlexChildBlock(
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
} else if (blockInCell is ImageBlock) {
|
} else if (blockInCell is ImageBlock) {
|
||||||
|
val imageModifier = Modifier.fillMaxWidth().then(
|
||||||
|
if (blockInCell.intrinsicWidth != null && blockInCell.intrinsicHeight != null && blockInCell.intrinsicWidth > 0f && blockInCell.intrinsicHeight > 0f) {
|
||||||
|
Modifier.aspectRatio(blockInCell.intrinsicWidth / blockInCell.intrinsicHeight, matchHeightConstraintsFirst = false)
|
||||||
|
} else if (blockInCell.style.height != Dp.Unspecified) {
|
||||||
|
Modifier.height(blockInCell.style.height)
|
||||||
|
} else {
|
||||||
|
Modifier.height(250.dp)
|
||||||
|
}
|
||||||
|
)
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = Builder(LocalContext.current).data(
|
model = Builder(LocalContext.current).data(
|
||||||
File(
|
File(
|
||||||
|
|
@ -3303,7 +3369,7 @@ private fun RenderFlexChildBlock(
|
||||||
).build(),
|
).build(),
|
||||||
contentDescription = blockInCell.altText,
|
contentDescription = blockInCell.altText,
|
||||||
contentScale = ContentScale.Fit,
|
contentScale = ContentScale.Fit,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = imageModifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,7 @@ sealed interface ContentBlock {
|
||||||
val elementId: String?
|
val elementId: String?
|
||||||
val cfi: String?
|
val cfi: String?
|
||||||
val blockIndex: Int
|
val blockIndex: Int
|
||||||
|
val expectedHeight: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface TextContentBlock : ContentBlock {
|
sealed interface TextContentBlock : ContentBlock {
|
||||||
|
|
@ -156,7 +157,8 @@ data class ParagraphBlock(
|
||||||
@ProtoNumber(5) override val cfi: String? = null,
|
@ProtoNumber(5) override val cfi: String? = null,
|
||||||
@ProtoNumber(6) override val startCharOffsetInSource: Int = 0,
|
@ProtoNumber(6) override val startCharOffsetInSource: Int = 0,
|
||||||
@ProtoNumber(7) override val endCharOffsetInSource: Int = -1,
|
@ProtoNumber(7) override val endCharOffsetInSource: Int = -1,
|
||||||
@ProtoNumber(8) override val blockIndex: Int
|
@ProtoNumber(8) override val blockIndex: Int,
|
||||||
|
@ProtoNumber(9) override val expectedHeight: Int = 0
|
||||||
) : TextContentBlock
|
) : TextContentBlock
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
@ -169,7 +171,8 @@ data class ImageBlock(
|
||||||
@ProtoNumber(6) override val elementId: String? = null,
|
@ProtoNumber(6) override val elementId: String? = null,
|
||||||
@ProtoNumber(7) override val cfi: String? = null,
|
@ProtoNumber(7) override val cfi: String? = null,
|
||||||
@ProtoNumber(8) val invertOnDarkTheme: Boolean = false,
|
@ProtoNumber(8) val invertOnDarkTheme: Boolean = false,
|
||||||
@ProtoNumber(9) override val blockIndex: Int
|
@ProtoNumber(9) override val blockIndex: Int,
|
||||||
|
@ProtoNumber(10) override val expectedHeight: Int = 0
|
||||||
) : ContentBlock
|
) : ContentBlock
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
@ -183,6 +186,7 @@ data class HeaderBlock(
|
||||||
@ProtoNumber(7) override val startCharOffsetInSource: Int = 0,
|
@ProtoNumber(7) override val startCharOffsetInSource: Int = 0,
|
||||||
@ProtoNumber(8) override val endCharOffsetInSource: Int = -1,
|
@ProtoNumber(8) override val endCharOffsetInSource: Int = -1,
|
||||||
@ProtoNumber(9) override val blockIndex: Int,
|
@ProtoNumber(9) override val blockIndex: Int,
|
||||||
|
@ProtoNumber(10) override val expectedHeight: Int = 0
|
||||||
) : TextContentBlock
|
) : TextContentBlock
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
@ -191,7 +195,8 @@ data class SpacerBlock(
|
||||||
@ProtoNumber(2) override val style: BlockStyle = BlockStyle(),
|
@ProtoNumber(2) override val style: BlockStyle = BlockStyle(),
|
||||||
@ProtoNumber(3) override val elementId: String? = null,
|
@ProtoNumber(3) override val elementId: String? = null,
|
||||||
@ProtoNumber(4) override val cfi: String? = null,
|
@ProtoNumber(4) override val cfi: String? = null,
|
||||||
@ProtoNumber(5) override val blockIndex: Int
|
@ProtoNumber(5) override val blockIndex: Int,
|
||||||
|
@ProtoNumber(6) override val expectedHeight: Int = 0
|
||||||
) : ContentBlock
|
) : ContentBlock
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
@ -203,7 +208,8 @@ data class QuoteBlock(
|
||||||
@ProtoNumber(5) override val cfi: String? = null,
|
@ProtoNumber(5) override val cfi: String? = null,
|
||||||
@ProtoNumber(6) override val startCharOffsetInSource: Int = 0,
|
@ProtoNumber(6) override val startCharOffsetInSource: Int = 0,
|
||||||
@ProtoNumber(7) override val endCharOffsetInSource: Int = -1,
|
@ProtoNumber(7) override val endCharOffsetInSource: Int = -1,
|
||||||
@ProtoNumber(8) override val blockIndex: Int
|
@ProtoNumber(8) override val blockIndex: Int,
|
||||||
|
@ProtoNumber(9) override val expectedHeight: Int = 0
|
||||||
) : TextContentBlock
|
) : TextContentBlock
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
@ -216,7 +222,8 @@ data class ListItemBlock(
|
||||||
@ProtoNumber(6) override val cfi: String? = null,
|
@ProtoNumber(6) override val cfi: String? = null,
|
||||||
@ProtoNumber(7) override val startCharOffsetInSource: Int = 0,
|
@ProtoNumber(7) override val startCharOffsetInSource: Int = 0,
|
||||||
@ProtoNumber(8) override val endCharOffsetInSource: Int = -1,
|
@ProtoNumber(8) override val endCharOffsetInSource: Int = -1,
|
||||||
@ProtoNumber(9) override val blockIndex: Int
|
@ProtoNumber(9) override val blockIndex: Int,
|
||||||
|
@ProtoNumber(10) override val expectedHeight: Int = 0
|
||||||
) : TextContentBlock
|
) : TextContentBlock
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
@ -233,7 +240,8 @@ data class TableBlock(
|
||||||
@ProtoNumber(2) override val style: BlockStyle = BlockStyle(),
|
@ProtoNumber(2) override val style: BlockStyle = BlockStyle(),
|
||||||
@ProtoNumber(3) override val elementId: String? = null,
|
@ProtoNumber(3) override val elementId: String? = null,
|
||||||
@ProtoNumber(4) override val cfi: String? = null,
|
@ProtoNumber(4) override val cfi: String? = null,
|
||||||
@ProtoNumber(5) override val blockIndex: Int
|
@ProtoNumber(5) override val blockIndex: Int,
|
||||||
|
@ProtoNumber(6) override val expectedHeight: Int = 0
|
||||||
) : ContentBlock
|
) : ContentBlock
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
@ -247,7 +255,8 @@ data class MathBlock(
|
||||||
@ProtoNumber(7) val svgHeight: String? = null,
|
@ProtoNumber(7) val svgHeight: String? = null,
|
||||||
@ProtoNumber(8) val svgViewBox: String? = null,
|
@ProtoNumber(8) val svgViewBox: String? = null,
|
||||||
@ProtoNumber(9) val isFromMathJax: Boolean = false,
|
@ProtoNumber(9) val isFromMathJax: Boolean = false,
|
||||||
@ProtoNumber(10) override val blockIndex: Int
|
@ProtoNumber(10) override val blockIndex: Int,
|
||||||
|
@ProtoNumber(11) override val expectedHeight: Int = 0
|
||||||
) : ContentBlock
|
) : ContentBlock
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
@ -257,7 +266,8 @@ data class WrappingContentBlock(
|
||||||
@ProtoNumber(3) override val style: BlockStyle = BlockStyle(),
|
@ProtoNumber(3) override val style: BlockStyle = BlockStyle(),
|
||||||
@ProtoNumber(4) override val elementId: String? = null,
|
@ProtoNumber(4) override val elementId: String? = null,
|
||||||
@ProtoNumber(5) override val cfi: String? = null,
|
@ProtoNumber(5) override val cfi: String? = null,
|
||||||
@ProtoNumber(6) override val blockIndex: Int
|
@ProtoNumber(6) override val blockIndex: Int,
|
||||||
|
@ProtoNumber(7) override val expectedHeight: Int = 0
|
||||||
) : ContentBlock
|
) : ContentBlock
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
@ -332,7 +342,8 @@ data class FlexContainerBlock(
|
||||||
@ProtoNumber(2) override val style: BlockStyle = BlockStyle(),
|
@ProtoNumber(2) override val style: BlockStyle = BlockStyle(),
|
||||||
@ProtoNumber(3) override val elementId: String? = null,
|
@ProtoNumber(3) override val elementId: String? = null,
|
||||||
@ProtoNumber(4) override val cfi: String? = null,
|
@ProtoNumber(4) override val cfi: String? = null,
|
||||||
@ProtoNumber(5) override val blockIndex: Int
|
@ProtoNumber(5) override val blockIndex: Int,
|
||||||
|
@ProtoNumber(6) override val expectedHeight: Int = 0
|
||||||
) : ContentBlock
|
) : ContentBlock
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ import androidx.compose.ui.unit.isSpecified
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
interface BlockMeasurementProvider {
|
interface BlockMeasurementProvider {
|
||||||
suspend fun measure(block: ContentBlock): Int
|
suspend fun measure(block: ContentBlock): Int
|
||||||
|
|
@ -286,14 +287,13 @@ class SuspendingAndroidBlockMeasurementProvider(
|
||||||
var currentHeight = 0
|
var currentHeight = 0
|
||||||
var splitRowIndex = -1
|
var splitRowIndex = -1
|
||||||
|
|
||||||
// Account for top and bottom decorations (padding + border)
|
|
||||||
val decorationTop = with(density) {
|
val decorationTop = with(density) {
|
||||||
block.style.padding.top.toPx() + (block.style.border?.width?.toPx() ?: 0f)
|
block.style.padding.top.toPx() + (block.style.border?.width?.toPx() ?: 0f)
|
||||||
}.toInt()
|
}.roundToInt()
|
||||||
|
|
||||||
val decorationBottom = with(density) {
|
val decorationBottom = with(density) {
|
||||||
block.style.padding.bottom.toPx() + (block.style.border?.width?.toPx() ?: 0f)
|
block.style.padding.bottom.toPx() + (block.style.border?.width?.toPx() ?: 0f)
|
||||||
}.toInt()
|
}.roundToInt()
|
||||||
|
|
||||||
Timber.tag("PAGINATION_DEBUG").d("SplitTable: avail=$availableHeight, topDec=$decorationTop, botDec=$decorationBottom")
|
Timber.tag("PAGINATION_DEBUG").d("SplitTable: avail=$availableHeight, topDec=$decorationTop, botDec=$decorationBottom")
|
||||||
currentHeight += decorationTop
|
currentHeight += decorationTop
|
||||||
|
|
@ -304,8 +304,8 @@ class SuspendingAndroidBlockMeasurementProvider(
|
||||||
val totalColspan = row.sumOf { it.colspan }.toFloat().coerceAtLeast(1f)
|
val totalColspan = row.sumOf { it.colspan }.toFloat().coerceAtLeast(1f)
|
||||||
|
|
||||||
row.forEach { cell ->
|
row.forEach { cell ->
|
||||||
val cellMaxWidth = ((constraints.maxWidth) * (cell.colspan.toFloat() / totalColspan)).toInt()
|
val cellMaxWidth = ((constraints.maxWidth) * (cell.colspan.toFloat() / totalColspan)).roundToInt()
|
||||||
constraints.copy(maxWidth = cellMaxWidth.coerceAtLeast(0))
|
@Suppress("UnusedVariable", "Unused") val cellConstraints = constraints.copy(maxWidth = cellMaxWidth.coerceAtLeast(0))
|
||||||
|
|
||||||
var cellHeight = 0
|
var cellHeight = 0
|
||||||
cell.content.forEach { b ->
|
cell.content.forEach { b ->
|
||||||
|
|
@ -314,11 +314,10 @@ class SuspendingAndroidBlockMeasurementProvider(
|
||||||
val cellDecoration = with(density) {
|
val cellDecoration = with(density) {
|
||||||
cell.style.blockStyle.padding.top.toPx() + cell.style.blockStyle.padding.bottom.toPx() +
|
cell.style.blockStyle.padding.top.toPx() + cell.style.blockStyle.padding.bottom.toPx() +
|
||||||
(cell.style.blockStyle.border?.width?.toPx() ?: 0f) * 2
|
(cell.style.blockStyle.border?.width?.toPx() ?: 0f) * 2
|
||||||
}.toInt()
|
}.roundToInt()
|
||||||
maxRowHeight = maxOf(maxRowHeight, cellHeight + cellDecoration)
|
maxRowHeight = maxOf(maxRowHeight, cellHeight + cellDecoration)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CHECK: Must account for decorationBottom here
|
|
||||||
if (currentHeight + maxRowHeight + decorationBottom > availableHeight) {
|
if (currentHeight + maxRowHeight + decorationBottom > availableHeight) {
|
||||||
Timber.tag("PAGINATION_DEBUG").d("SplitTable: Breaking at row $i. currentH=$currentHeight, rowH=$maxRowHeight")
|
Timber.tag("PAGINATION_DEBUG").d("SplitTable: Breaking at row $i. currentH=$currentHeight, rowH=$maxRowHeight")
|
||||||
splitRowIndex = i
|
splitRowIndex = i
|
||||||
|
|
@ -327,7 +326,7 @@ class SuspendingAndroidBlockMeasurementProvider(
|
||||||
currentHeight += maxRowHeight
|
currentHeight += maxRowHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
if (splitRowIndex <= 0) return null // Can't even fit the first row
|
if (splitRowIndex <= 0) return null
|
||||||
|
|
||||||
val part1Rows = block.rows.subList(0, splitRowIndex)
|
val part1Rows = block.rows.subList(0, splitRowIndex)
|
||||||
val part2Rows = block.rows.subList(splitRowIndex, block.rows.size)
|
val part2Rows = block.rows.subList(splitRowIndex, block.rows.size)
|
||||||
|
|
@ -346,11 +345,11 @@ class SuspendingAndroidBlockMeasurementProvider(
|
||||||
|
|
||||||
val decorationTop = with(density) {
|
val decorationTop = with(density) {
|
||||||
block.style.padding.top.toPx() + (block.style.border?.width?.toPx() ?: 0f)
|
block.style.padding.top.toPx() + (block.style.border?.width?.toPx() ?: 0f)
|
||||||
}.toInt()
|
}.roundToInt()
|
||||||
|
|
||||||
val decorationBottom = with(density) {
|
val decorationBottom = with(density) {
|
||||||
block.style.padding.bottom.toPx() + (block.style.border?.width?.toPx() ?: 0f)
|
block.style.padding.bottom.toPx() + (block.style.border?.width?.toPx() ?: 0f)
|
||||||
}.toInt()
|
}.roundToInt()
|
||||||
|
|
||||||
currentHeight += decorationTop
|
currentHeight += decorationTop
|
||||||
|
|
||||||
|
|
@ -363,9 +362,8 @@ class SuspendingAndroidBlockMeasurementProvider(
|
||||||
val currMargin = child.style.margin.top.toPx()
|
val currMargin = child.style.margin.top.toPx()
|
||||||
maxOf(prevMargin, currMargin)
|
maxOf(prevMargin, currMargin)
|
||||||
} else child.style.margin.top.toPx()
|
} else child.style.margin.top.toPx()
|
||||||
}.toInt()
|
}.roundToInt()
|
||||||
|
|
||||||
// CHECK: Must account for decorationBottom here
|
|
||||||
if (currentHeight + childHeight + margin + decorationBottom > availableHeight) {
|
if (currentHeight + childHeight + margin + decorationBottom > availableHeight) {
|
||||||
splitChildIndex = i
|
splitChildIndex = i
|
||||||
break
|
break
|
||||||
|
|
@ -400,6 +398,22 @@ private fun copyBlockWithNewStyle(block: ContentBlock, newStyle: BlockStyle): Co
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
private fun <T : ContentBlock> setBlockExpectedHeight(block: T, height: Int): T {
|
||||||
|
return when (block) {
|
||||||
|
is ParagraphBlock -> block.copy(expectedHeight = height)
|
||||||
|
is HeaderBlock -> block.copy(expectedHeight = height)
|
||||||
|
is ImageBlock -> block.copy(expectedHeight = height)
|
||||||
|
is SpacerBlock -> block.copy(expectedHeight = height)
|
||||||
|
is QuoteBlock -> block.copy(expectedHeight = height)
|
||||||
|
is ListItemBlock -> block.copy(expectedHeight = height)
|
||||||
|
is WrappingContentBlock -> block.copy(expectedHeight = height)
|
||||||
|
is TableBlock -> block.copy(expectedHeight = height)
|
||||||
|
is FlexContainerBlock -> block.copy(expectedHeight = height)
|
||||||
|
is MathBlock -> block.copy(expectedHeight = height)
|
||||||
|
} as T
|
||||||
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
|
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
|
||||||
suspend fun paginate(
|
suspend fun paginate(
|
||||||
blocks: List<ContentBlock>,
|
blocks: List<ContentBlock>,
|
||||||
|
|
@ -417,10 +431,11 @@ suspend fun paginate(
|
||||||
var remainingHeight = pageHeight
|
var remainingHeight = pageHeight
|
||||||
val remainingBlocks = blocks.toMutableList()
|
val remainingBlocks = blocks.toMutableList()
|
||||||
var pageIndex = 0
|
var pageIndex = 0
|
||||||
val safetyMarginPerBlock = 2
|
val safetyMarginPerBlock = 0
|
||||||
|
|
||||||
while (remainingBlocks.isNotEmpty()) {
|
while (remainingBlocks.isNotEmpty()) {
|
||||||
val block = remainingBlocks.removeAt(0)
|
val block = remainingBlocks.removeAt(0)
|
||||||
|
|
||||||
val blockHeight = measurementProvider.measure(block)
|
val blockHeight = measurementProvider.measure(block)
|
||||||
val blockHeightWithSafetyMargin = blockHeight + safetyMarginPerBlock
|
val blockHeightWithSafetyMargin = blockHeight + safetyMarginPerBlock
|
||||||
|
|
||||||
|
|
@ -432,11 +447,12 @@ suspend fun paginate(
|
||||||
} else {
|
} else {
|
||||||
block.style.margin.top.toPx()
|
block.style.margin.top.toPx()
|
||||||
}
|
}
|
||||||
}
|
}.roundToInt()
|
||||||
|
|
||||||
val spaceRequired = (blockHeightWithSafetyMargin + spaceBetweenBlocks).toInt()
|
val spaceRequired = blockHeightWithSafetyMargin + spaceBetweenBlocks
|
||||||
|
|
||||||
Timber.tag("PAGINATION_DEBUG").d("Processing ${block::class.simpleName}: req=$spaceRequired, remaining=$remainingHeight, margin=$spaceBetweenBlocks, heightOnly=$blockHeight")
|
Timber.tag("PAGINATION_DEBUG")
|
||||||
|
.d("Processing ${block::class.simpleName}: req=$spaceRequired, remaining=$remainingHeight, margin=$spaceBetweenBlocks, heightOnly=$blockHeight")
|
||||||
|
|
||||||
if (spaceRequired <= remainingHeight) {
|
if (spaceRequired <= remainingHeight) {
|
||||||
var blockToAdd = block
|
var blockToAdd = block
|
||||||
|
|
@ -444,51 +460,94 @@ suspend fun paginate(
|
||||||
|
|
||||||
if (currentPageContent.isNotEmpty()) {
|
if (currentPageContent.isNotEmpty()) {
|
||||||
val prevBlock = currentPageContent.last()
|
val prevBlock = currentPageContent.last()
|
||||||
val newPrevStyle = prevBlock.style.copy(margin = prevBlock.style.margin.copy(bottom = 0.dp))
|
val newPrevStyle =
|
||||||
val newPrevBlock = copyBlockWithNewStyle(prevBlock, newPrevStyle)
|
prevBlock.style.copy(margin = prevBlock.style.margin.copy(bottom = 0.dp))
|
||||||
|
val newPrevBlock = setBlockExpectedHeight(
|
||||||
|
copyBlockWithNewStyle(prevBlock, newPrevStyle),
|
||||||
|
prevBlock.expectedHeight
|
||||||
|
)
|
||||||
currentPageContent[currentPageContent.size - 1] = newPrevBlock
|
currentPageContent[currentPageContent.size - 1] = newPrevBlock
|
||||||
}
|
}
|
||||||
val newCurrentStyle = block.style.copy(margin = block.style.margin.copy(top = collapsedMarginDp))
|
val newCurrentStyle =
|
||||||
|
block.style.copy(margin = block.style.margin.copy(top = collapsedMarginDp))
|
||||||
blockToAdd = copyBlockWithNewStyle(block, newCurrentStyle)
|
blockToAdd = copyBlockWithNewStyle(block, newCurrentStyle)
|
||||||
|
|
||||||
|
blockToAdd = setBlockExpectedHeight(blockToAdd, spaceRequired)
|
||||||
|
|
||||||
currentPageContent.add(blockToAdd)
|
currentPageContent.add(blockToAdd)
|
||||||
remainingHeight -= spaceRequired
|
remainingHeight -= spaceRequired
|
||||||
} else {
|
} else {
|
||||||
var wasSplit = false
|
var wasSplit = false
|
||||||
val heightForSplitting = remainingHeight - spaceBetweenBlocks.toInt()
|
val heightForSplitting = remainingHeight - spaceBetweenBlocks
|
||||||
|
|
||||||
if (heightForSplitting > 50) {
|
if (heightForSplitting > 50) {
|
||||||
when (block) {
|
when (block) {
|
||||||
is ParagraphBlock -> {
|
is ParagraphBlock -> {
|
||||||
if (!block.style.pageBreakInsideAvoid) {
|
if (!block.style.pageBreakInsideAvoid) {
|
||||||
measurementProvider.split(block, heightForSplitting)?.let { (part1, part2) ->
|
measurementProvider.split(block, heightForSplitting)
|
||||||
|
?.let { (part1, part2) ->
|
||||||
if (part1.content.isNotEmpty()) {
|
if (part1.content.isNotEmpty()) {
|
||||||
val collapsedMarginDp = with(density) { spaceBetweenBlocks.toDp() }
|
val collapsedMarginDp =
|
||||||
|
with(density) { spaceBetweenBlocks.toDp() }
|
||||||
if (currentPageContent.isNotEmpty()) {
|
if (currentPageContent.isNotEmpty()) {
|
||||||
val prevBlock = currentPageContent.last()
|
val prevBlock = currentPageContent.last()
|
||||||
val newPrevStyle = prevBlock.style.copy(margin = prevBlock.style.margin.copy(bottom = 0.dp))
|
val newPrevStyle = prevBlock.style.copy(
|
||||||
currentPageContent[currentPageContent.size - 1] = copyBlockWithNewStyle(prevBlock, newPrevStyle)
|
margin = prevBlock.style.margin.copy(bottom = 0.dp)
|
||||||
|
)
|
||||||
|
currentPageContent[currentPageContent.size - 1] =
|
||||||
|
setBlockExpectedHeight(
|
||||||
|
copyBlockWithNewStyle(
|
||||||
|
prevBlock,
|
||||||
|
newPrevStyle
|
||||||
|
), prevBlock.expectedHeight
|
||||||
|
)
|
||||||
}
|
}
|
||||||
val newPart1Style = part1.style.copy(margin = part1.style.margin.copy(top = collapsedMarginDp))
|
val newPart1Style =
|
||||||
val finalPart1 = part1.copy(style = newPart1Style)
|
part1.style.copy(margin = part1.style.margin.copy(top = collapsedMarginDp))
|
||||||
|
var finalPart1 = part1.copy(style = newPart1Style)
|
||||||
|
|
||||||
|
val part1Height = measurementProvider.measure(finalPart1)
|
||||||
|
val part1Total = part1Height + spaceBetweenBlocks
|
||||||
|
finalPart1 = setBlockExpectedHeight(finalPart1, part1Total)
|
||||||
|
|
||||||
currentPageContent.add(finalPart1)
|
currentPageContent.add(finalPart1)
|
||||||
if (part2.content.isNotEmpty()) remainingBlocks.add(0, part2)
|
if (part2.content.isNotEmpty()) remainingBlocks.add(
|
||||||
|
0,
|
||||||
|
part2
|
||||||
|
)
|
||||||
wasSplit = true
|
wasSplit = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is WrappingContentBlock -> {
|
is WrappingContentBlock -> {
|
||||||
measurementProvider.split(block, heightForSplitting)?.let { (part1, part2) ->
|
measurementProvider.split(block, heightForSplitting)
|
||||||
|
?.let { (part1, part2) ->
|
||||||
if (part1.paragraphsToWrap.any { it.content.isNotBlank() }) {
|
if (part1.paragraphsToWrap.any { it.content.isNotBlank() }) {
|
||||||
val collapsedMarginDp = with(density) { spaceBetweenBlocks.toDp() }
|
val collapsedMarginDp =
|
||||||
|
with(density) { spaceBetweenBlocks.toDp() }
|
||||||
if (currentPageContent.isNotEmpty()) {
|
if (currentPageContent.isNotEmpty()) {
|
||||||
val prevBlock = currentPageContent.last()
|
val prevBlock = currentPageContent.last()
|
||||||
val newPrevStyle = prevBlock.style.copy(margin = prevBlock.style.margin.copy(bottom = 0.dp))
|
val newPrevStyle = prevBlock.style.copy(
|
||||||
currentPageContent[currentPageContent.size - 1] = copyBlockWithNewStyle(prevBlock, newPrevStyle)
|
margin = prevBlock.style.margin.copy(bottom = 0.dp)
|
||||||
|
)
|
||||||
|
currentPageContent[currentPageContent.size - 1] =
|
||||||
|
setBlockExpectedHeight(
|
||||||
|
copyBlockWithNewStyle(
|
||||||
|
prevBlock,
|
||||||
|
newPrevStyle
|
||||||
|
), prevBlock.expectedHeight
|
||||||
|
)
|
||||||
}
|
}
|
||||||
val newPart1Style = part1.style.copy(margin = part1.style.margin.copy(top = collapsedMarginDp))
|
val newPart1Style =
|
||||||
val finalPart1 = part1.copy(style = newPart1Style)
|
part1.style.copy(margin = part1.style.margin.copy(top = collapsedMarginDp))
|
||||||
|
var finalPart1 = part1.copy(style = newPart1Style)
|
||||||
|
|
||||||
|
val part1Height = measurementProvider.measure(finalPart1)
|
||||||
|
val part1Total = part1Height + spaceBetweenBlocks
|
||||||
|
finalPart1 = setBlockExpectedHeight(finalPart1, part1Total)
|
||||||
|
|
||||||
currentPageContent.add(finalPart1)
|
currentPageContent.add(finalPart1)
|
||||||
if (part2.isNotEmpty()) {
|
if (part2.isNotEmpty()) {
|
||||||
remainingBlocks.addAll(0, part2)
|
remainingBlocks.addAll(0, part2)
|
||||||
|
|
@ -497,23 +556,74 @@ suspend fun paginate(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is TableBlock -> {
|
is TableBlock -> {
|
||||||
measurementProvider.split(block, heightForSplitting)?.let { (part1, part2) ->
|
measurementProvider.split(block, heightForSplitting)
|
||||||
|
?.let { (part1, part2) ->
|
||||||
val collapsedMarginDp = with(density) { spaceBetweenBlocks.toDp() }
|
val collapsedMarginDp = with(density) { spaceBetweenBlocks.toDp() }
|
||||||
currentPageContent.add(copyBlockWithNewStyle(part1, part1.style.copy(margin = part1.style.margin.copy(top = collapsedMarginDp))))
|
if (currentPageContent.isNotEmpty()) {
|
||||||
|
val prevBlock = currentPageContent.last()
|
||||||
|
val newPrevStyle = prevBlock.style.copy(
|
||||||
|
margin = prevBlock.style.margin.copy(bottom = 0.dp)
|
||||||
|
)
|
||||||
|
currentPageContent[currentPageContent.size - 1] =
|
||||||
|
setBlockExpectedHeight(
|
||||||
|
copyBlockWithNewStyle(
|
||||||
|
prevBlock,
|
||||||
|
newPrevStyle
|
||||||
|
), prevBlock.expectedHeight
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val newPart1Style =
|
||||||
|
part1.style.copy(margin = part1.style.margin.copy(top = collapsedMarginDp))
|
||||||
|
var finalPart1 = copyBlockWithNewStyle(part1, newPart1Style)
|
||||||
|
|
||||||
|
val part1Height = measurementProvider.measure(finalPart1)
|
||||||
|
val part1Total = part1Height + spaceBetweenBlocks
|
||||||
|
finalPart1 = setBlockExpectedHeight(finalPart1, part1Total)
|
||||||
|
|
||||||
|
currentPageContent.add(finalPart1)
|
||||||
remainingBlocks.add(0, part2)
|
remainingBlocks.add(0, part2)
|
||||||
wasSplit = true
|
wasSplit = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is FlexContainerBlock -> {
|
is FlexContainerBlock -> {
|
||||||
measurementProvider.split(block, heightForSplitting)?.let { (part1, part2) ->
|
measurementProvider.split(block, heightForSplitting)
|
||||||
|
?.let { (part1, part2) ->
|
||||||
val collapsedMarginDp = with(density) { spaceBetweenBlocks.toDp() }
|
val collapsedMarginDp = with(density) { spaceBetweenBlocks.toDp() }
|
||||||
currentPageContent.add(copyBlockWithNewStyle(part1, part1.style.copy(margin = part1.style.margin.copy(top = collapsedMarginDp))))
|
if (currentPageContent.isNotEmpty()) {
|
||||||
|
val prevBlock = currentPageContent.last()
|
||||||
|
val newPrevStyle = prevBlock.style.copy(
|
||||||
|
margin = prevBlock.style.margin.copy(bottom = 0.dp)
|
||||||
|
)
|
||||||
|
currentPageContent[currentPageContent.size - 1] =
|
||||||
|
setBlockExpectedHeight(
|
||||||
|
copyBlockWithNewStyle(
|
||||||
|
prevBlock,
|
||||||
|
newPrevStyle
|
||||||
|
), prevBlock.expectedHeight
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val newPart1Style =
|
||||||
|
part1.style.copy(margin = part1.style.margin.copy(top = collapsedMarginDp))
|
||||||
|
var finalPart1 = copyBlockWithNewStyle(part1, newPart1Style)
|
||||||
|
|
||||||
|
val part1Height = measurementProvider.measure(finalPart1)
|
||||||
|
val part1Total = part1Height + spaceBetweenBlocks
|
||||||
|
finalPart1 = setBlockExpectedHeight(finalPart1, part1Total)
|
||||||
|
|
||||||
|
currentPageContent.add(finalPart1)
|
||||||
remainingBlocks.add(0, part2)
|
remainingBlocks.add(0, part2)
|
||||||
wasSplit = true
|
wasSplit = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> { Timber.d("Page ${pageIndex + 1}: Block type is not splittable.") }
|
|
||||||
|
else -> {
|
||||||
|
Timber.d("Page ${pageIndex + 1}: Block type is not splittable.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Timber.d("Page ${pageIndex + 1}: Not enough height for splitting ($heightForSplitting <= 50).")
|
Timber.d("Page ${pageIndex + 1}: Not enough height for splitting ($heightForSplitting <= 50).")
|
||||||
|
|
@ -521,14 +631,20 @@ suspend fun paginate(
|
||||||
|
|
||||||
if (!wasSplit) {
|
if (!wasSplit) {
|
||||||
if (currentPageContent.isEmpty()) {
|
if (currentPageContent.isEmpty()) {
|
||||||
Timber.tag("PAGINATION_DEBUG").w("FORCING block ${block::class.simpleName} onto page because it is the first block, even though req($spaceRequired) > remaining($remainingHeight)")
|
Timber.tag("PAGINATION_DEBUG")
|
||||||
currentPageContent.add(block)
|
.w("FORCING block ${block::class.simpleName} onto page because it is the first block, even though req($spaceRequired) > remaining($remainingHeight)")
|
||||||
|
val forcedHeight = blockHeight + spaceBetweenBlocks
|
||||||
|
val blockToAdd = setBlockExpectedHeight(block, forcedHeight)
|
||||||
|
currentPageContent.add(blockToAdd)
|
||||||
} else {
|
} else {
|
||||||
Timber.tag("PAGINATION_DEBUG").d("Block ${block::class.simpleName} did not fit and was not split. Moving to next page.")
|
Timber.tag("PAGINATION_DEBUG")
|
||||||
|
.d("Block ${block::class.simpleName} did not fit and was not split. Moving to next page.")
|
||||||
remainingBlocks.add(0, block)
|
remainingBlocks.add(0, block)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
zeroOutBottomMargin(currentPageContent)
|
||||||
|
|
||||||
pages.add(Page(content = currentPageContent.toList()))
|
pages.add(Page(content = currentPageContent.toList()))
|
||||||
pageIndex++
|
pageIndex++
|
||||||
currentPageContent = mutableListOf()
|
currentPageContent = mutableListOf()
|
||||||
|
|
@ -536,6 +652,8 @@ suspend fun paginate(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (currentPageContent.isNotEmpty()) {
|
if (currentPageContent.isNotEmpty()) {
|
||||||
|
zeroOutBottomMargin(currentPageContent)
|
||||||
|
|
||||||
pages.add(Page(content = currentPageContent.toList()))
|
pages.add(Page(content = currentPageContent.toList()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -569,7 +687,6 @@ private suspend fun measureBlockHeight(
|
||||||
val specifiedWidthDp = block.style.width
|
val specifiedWidthDp = block.style.width
|
||||||
val specifiedMaxWidthDp = block.style.maxWidth
|
val specifiedMaxWidthDp = block.style.maxWidth
|
||||||
|
|
||||||
// 1. Determine the block's final outer width.
|
|
||||||
val blockOuterWidthPx = with(density) {
|
val blockOuterWidthPx = with(density) {
|
||||||
var effectiveWidthPx = constraints.maxWidth.toFloat()
|
var effectiveWidthPx = constraints.maxWidth.toFloat()
|
||||||
if (specifiedWidthDp != Dp.Unspecified) {
|
if (specifiedWidthDp != Dp.Unspecified) {
|
||||||
|
|
@ -584,18 +701,18 @@ private suspend fun measureBlockHeight(
|
||||||
effectiveWidthPx.coerceAtMost(constraints.maxWidth.toFloat())
|
effectiveWidthPx.coerceAtMost(constraints.maxWidth.toFloat())
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Determine the width available for the content itself.
|
val contentMaxWidth = if (specifiedWidthDp == Dp.Unspecified) {
|
||||||
val contentMaxWidth = if (isBorderBox) {
|
(blockOuterWidthPx - horizontalPaddingPx - horizontalBorderPx)
|
||||||
|
} else if (isBorderBox) {
|
||||||
(blockOuterWidthPx - horizontalPaddingPx - horizontalBorderPx)
|
(blockOuterWidthPx - horizontalPaddingPx - horizontalBorderPx)
|
||||||
} else {
|
} else {
|
||||||
blockOuterWidthPx
|
blockOuterWidthPx
|
||||||
}
|
}
|
||||||
|
|
||||||
val adjustedConstraints = constraints.copy(
|
val adjustedConstraints = constraints.copy(
|
||||||
maxWidth = contentMaxWidth.toInt().coerceAtLeast(0)
|
maxWidth = contentMaxWidth.roundToInt().coerceAtLeast(0)
|
||||||
)
|
)
|
||||||
|
|
||||||
// 3. Measure the height of the actual content using the calculated content width.
|
|
||||||
val contentHeight = when (block) {
|
val contentHeight = when (block) {
|
||||||
is ParagraphBlock -> {
|
is ParagraphBlock -> {
|
||||||
val height = withContext(Dispatchers.Main) {
|
val height = withContext(Dispatchers.Main) {
|
||||||
|
|
@ -634,20 +751,19 @@ private suspend fun measureBlockHeight(
|
||||||
contentMaxWidth
|
contentMaxWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
val height = (imageRenderWidthPx * aspectRatio).toInt()
|
val height = (imageRenderWidthPx * aspectRatio).roundToInt()
|
||||||
height
|
height
|
||||||
} else {
|
} else {
|
||||||
Timber.w("Image at '${block.path}' has no valid intrinsic dimensions, falling back to fixed height.")
|
Timber.w("Image at '${block.path}' has no valid intrinsic dimensions, falling back to fixed height.")
|
||||||
if (block.style.height != Dp.Unspecified) {
|
if (block.style.height != Dp.Unspecified) {
|
||||||
with(density) { block.style.height.toPx() }.toInt()
|
with(density) { block.style.height.toPx().roundToInt() }
|
||||||
} else {
|
} else {
|
||||||
with(density) { 250.dp.toPx() }.toInt()
|
with(density) { 250.dp.toPx().roundToInt() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is SpacerBlock -> {
|
is SpacerBlock -> {
|
||||||
// A spacer's height property defines its content height.
|
val height = with(density) { block.height.toPx().roundToInt() }
|
||||||
val height = with(density) { block.height.toPx() }.toInt()
|
|
||||||
height
|
height
|
||||||
}
|
}
|
||||||
is QuoteBlock -> {
|
is QuoteBlock -> {
|
||||||
|
|
@ -673,7 +789,7 @@ private suspend fun measureBlockHeight(
|
||||||
).size.height
|
).size.height
|
||||||
}
|
}
|
||||||
val markerImageHeight = if (block.itemMarkerImage != null) {
|
val markerImageHeight = if (block.itemMarkerImage != null) {
|
||||||
with(density) { (defaultStyle.fontSize.value * 0.8f).sp.toPx() }.toInt()
|
with(density) { (defaultStyle.fontSize.value * 0.8f).sp.toPx().roundToInt() }
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
@ -689,8 +805,8 @@ private suspend fun measureBlockHeight(
|
||||||
row.forEach { cell ->
|
row.forEach { cell ->
|
||||||
val cellBlockStyle = cell.style.blockStyle
|
val cellBlockStyle = cell.style.blockStyle
|
||||||
val cellMaxWidth = when {
|
val cellMaxWidth = when {
|
||||||
cellBlockStyle.width.isSpecified -> with(density) { cellBlockStyle.width.toPx() }.toInt()
|
cellBlockStyle.width.isSpecified -> with(density) { cellBlockStyle.width.toPx().roundToInt() }
|
||||||
else -> (adjustedConstraints.maxWidth * (cell.colspan.toFloat() / totalColspan)).toInt()
|
else -> (adjustedConstraints.maxWidth * (cell.colspan.toFloat() / totalColspan)).roundToInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
val cellConstraints = adjustedConstraints.copy(maxWidth = cellMaxWidth.coerceAtLeast(0))
|
val cellConstraints = adjustedConstraints.copy(maxWidth = cellMaxWidth.coerceAtLeast(0))
|
||||||
|
|
@ -702,7 +818,7 @@ private suspend fun measureBlockHeight(
|
||||||
cellDecorationHeight = cellBlockStyle.padding.top.toPx() + cellBlockStyle.padding.bottom.toPx()
|
cellDecorationHeight = cellBlockStyle.padding.top.toPx() + cellBlockStyle.padding.bottom.toPx()
|
||||||
cellBlockStyle.border?.let { cellDecorationHeight += it.width.toPx() * 2 }
|
cellBlockStyle.border?.let { cellDecorationHeight += it.width.toPx() * 2 }
|
||||||
}
|
}
|
||||||
maxRowHeight = maxOf(maxRowHeight, (cellContentHeight + cellDecorationHeight).toInt())
|
maxRowHeight = maxOf(maxRowHeight, (cellContentHeight + cellDecorationHeight).roundToInt())
|
||||||
}
|
}
|
||||||
totalHeight += maxRowHeight
|
totalHeight += maxRowHeight
|
||||||
}
|
}
|
||||||
|
|
@ -820,13 +936,12 @@ private suspend fun measureBlockHeight(
|
||||||
|
|
||||||
textOffset += firstLineEndOffset
|
textOffset += firstLineEndOffset
|
||||||
|
|
||||||
// Skip leading whitespace for the next iteration.
|
|
||||||
while (textOffset < fullText.length && fullText[textOffset].isWhitespace()) {
|
while (textOffset < fullText.length && fullText[textOffset].isWhitespace()) {
|
||||||
textOffset++
|
textOffset++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val height = maxOf(currentY, imageHeightPx).toInt()
|
val height = maxOf(currentY, imageHeightPx).roundToInt()
|
||||||
height
|
height
|
||||||
}
|
}
|
||||||
is FlexContainerBlock -> {
|
is FlexContainerBlock -> {
|
||||||
|
|
@ -848,36 +963,26 @@ private suspend fun measureBlockHeight(
|
||||||
val heightPx = parseSvgDimension(block.svgHeight, fontSizePx, containerWidthPx, density)
|
val heightPx = parseSvgDimension(block.svgHeight, fontSizePx, containerWidthPx, density)
|
||||||
|
|
||||||
val finalHeight = if (heightPx != null) {
|
val finalHeight = if (heightPx != null) {
|
||||||
Timber.d("Paginator measuring MathBlock '${block.elementId}': Using explicit height '${block.svgHeight}' -> ${heightPx.toInt()}px")
|
heightPx.roundToInt()
|
||||||
heightPx.toInt()
|
|
||||||
} else if (widthPx != null && block.svgViewBox != null) {
|
|
||||||
val viewBoxParts = block.svgViewBox.split(' ', ',').mapNotNull { it.toFloatOrNull() }
|
|
||||||
if (viewBoxParts.size == 4 && viewBoxParts[2] > 0) {
|
|
||||||
val viewBoxWidth = viewBoxParts[2]
|
|
||||||
val viewBoxHeight = viewBoxParts[3]
|
|
||||||
val aspectRatio = viewBoxHeight / viewBoxWidth
|
|
||||||
val calculatedHeight = (widthPx * aspectRatio).toInt()
|
|
||||||
Timber.d("Paginator measuring MathBlock '${block.elementId}': Using width '${block.svgWidth}' -> ${widthPx}px and viewBox ratio $aspectRatio -> calculated height ${calculatedHeight}px")
|
|
||||||
calculatedHeight
|
|
||||||
} else {
|
} else {
|
||||||
val fallbackHeight = with(density) { (defaultStyle.fontSize.value * 3).sp.toPx() }.toInt()
|
val viewBoxParts = block.svgViewBox?.split(' ', ',')?.mapNotNull { it.toFloatOrNull() }
|
||||||
Timber.w("Paginator measuring MathBlock '${block.elementId}': Invalid viewBox '${block.svgViewBox}'. Using fallback height ${fallbackHeight}px")
|
if (viewBoxParts != null && viewBoxParts.size == 4 && viewBoxParts[2] > 0) {
|
||||||
fallbackHeight
|
val aspectRatio = viewBoxParts[3] / viewBoxParts[2]
|
||||||
|
|
||||||
|
val effectiveWidth = widthPx ?: containerWidthPx.toFloat()
|
||||||
|
(effectiveWidth * aspectRatio).roundToInt()
|
||||||
|
} else {
|
||||||
|
with(density) { (defaultStyle.fontSize.value * 3).sp.toPx().roundToInt() }
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
val fallbackHeight = with(density) { (defaultStyle.fontSize.value * 3).sp.toPx() }.toInt()
|
|
||||||
Timber.w("Paginator measuring MathBlock '${block.elementId}': No usable dimensions (width='${block.svgWidth}', height='${block.svgHeight}'). Using fallback height ${fallbackHeight}px")
|
|
||||||
fallbackHeight
|
|
||||||
}
|
}
|
||||||
finalHeight
|
finalHeight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 4. Calculate the final total height based on box-sizing.
|
|
||||||
val specifiedHeightDp = block.style.height
|
val specifiedHeightDp = block.style.height
|
||||||
val finalHeight = if (isBorderBox && specifiedHeightDp != Dp.Unspecified) {
|
val finalHeight = if (isBorderBox && specifiedHeightDp != Dp.Unspecified) {
|
||||||
with(density) { specifiedHeightDp.toPx() }.toInt()
|
with(density) { specifiedHeightDp.toPx().roundToInt() }
|
||||||
} else {
|
} else {
|
||||||
(contentHeight + verticalPaddingPx + verticalBorderPx).toInt()
|
(contentHeight + verticalPaddingPx + verticalBorderPx).roundToInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
Timber.tag("PAGINATION_DEBUG").v("Measure result for ${block::class.simpleName}: content=$contentHeight, paddingV=$verticalPaddingPx, borderV=$verticalBorderPx, total=$finalHeight")
|
Timber.tag("PAGINATION_DEBUG").v("Measure result for ${block::class.simpleName}: content=$contentHeight, paddingV=$verticalPaddingPx, borderV=$verticalBorderPx, total=$finalHeight")
|
||||||
|
|
@ -897,11 +1002,11 @@ private suspend fun splitParagraphBlock(
|
||||||
|
|
||||||
val decorationTop = with(density) {
|
val decorationTop = with(density) {
|
||||||
block.style.padding.top.toPx() + (block.style.border?.width?.toPx() ?: 0f)
|
block.style.padding.top.toPx() + (block.style.border?.width?.toPx() ?: 0f)
|
||||||
}
|
}.roundToInt()
|
||||||
|
|
||||||
val decorationBottom = with(density) {
|
val decorationBottom = with(density) {
|
||||||
block.style.padding.bottom.toPx() + (block.style.border?.width?.toPx() ?: 0f)
|
block.style.padding.bottom.toPx() + (block.style.border?.width?.toPx() ?: 0f)
|
||||||
}
|
}.roundToInt()
|
||||||
|
|
||||||
val availableTextHeight = availableHeight - decorationTop - decorationBottom
|
val availableTextHeight = availableHeight - decorationTop - decorationBottom
|
||||||
|
|
||||||
|
|
@ -928,13 +1033,13 @@ private suspend fun splitParagraphBlock(
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
var lastVisibleLine = layoutResult.getLineForVerticalPosition(availableTextHeight)
|
var lastVisibleLine = layoutResult.getLineForVerticalPosition(availableTextHeight.toFloat())
|
||||||
|
|
||||||
if (layoutResult.getLineBottom(lastVisibleLine) > availableHeight.toFloat()) {
|
if (layoutResult.getLineBottom(lastVisibleLine) > availableHeight.toFloat()) {
|
||||||
lastVisibleLine--
|
lastVisibleLine--
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastVisibleLine < 0) { // Safety check after decrementing
|
if (lastVisibleLine < 0) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1031,7 +1136,7 @@ internal fun parseSvgDimension(
|
||||||
dimension.endsWith("ex") -> dimension.removeSuffix("ex").toFloatOrNull()?.let { it * 0.5f * fontSizePx }
|
dimension.endsWith("ex") -> dimension.removeSuffix("ex").toFloatOrNull()?.let { it * 0.5f * fontSizePx }
|
||||||
dimension.endsWith("em") -> dimension.removeSuffix("em").toFloatOrNull()?.let { it * fontSizePx }
|
dimension.endsWith("em") -> dimension.removeSuffix("em").toFloatOrNull()?.let { it * fontSizePx }
|
||||||
dimension.endsWith("px") -> dimension.removeSuffix("px").toFloatOrNull()
|
dimension.endsWith("px") -> dimension.removeSuffix("px").toFloatOrNull()
|
||||||
dimension.endsWith("pt") -> dimension.removeSuffix("pt").toFloatOrNull()?.let { it * density.density * 160f / 72f }
|
dimension.endsWith("pt") -> dimension.removeSuffix("pt").toFloatOrNull()?.let { it * 1.333f * density.density }
|
||||||
dimension.endsWith("%") -> dimension.removeSuffix("%").toFloatOrNull()?.let { (it / 100f) * containerWidthPx }
|
dimension.endsWith("%") -> dimension.removeSuffix("%").toFloatOrNull()?.let { (it / 100f) * containerWidthPx }
|
||||||
else -> dimension.toFloatOrNull()
|
else -> dimension.toFloatOrNull()
|
||||||
}
|
}
|
||||||
|
|
@ -1056,12 +1161,22 @@ private suspend fun calculateContentHeightWithMargins(
|
||||||
} else {
|
} else {
|
||||||
child.style.margin.top.toPx()
|
child.style.margin.top.toPx()
|
||||||
}
|
}
|
||||||
}
|
}.roundToInt()
|
||||||
totalHeight += (childHeight + margin).toInt()
|
totalHeight += (childHeight + margin)
|
||||||
Timber.tag("PAGINATION_DEBUG").v(" Internal Child ${child::class.simpleName}: h=$childHeight, margin=$margin, runningTotal=$totalHeight")
|
Timber.tag("PAGINATION_DEBUG").v(" Internal Child ${child::class.simpleName}: h=$childHeight, margin=$margin, runningTotal=$totalHeight")
|
||||||
}
|
}
|
||||||
if (children.isNotEmpty()) {
|
if (children.isNotEmpty()) {
|
||||||
totalHeight += with(density) { children.last().style.margin.bottom.toPx() }.toInt()
|
totalHeight += with(density) { children.last().style.margin.bottom.toPx().roundToInt() }
|
||||||
}
|
}
|
||||||
return totalHeight
|
return totalHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun zeroOutBottomMargin(blocks: MutableList<ContentBlock>) {
|
||||||
|
if (blocks.isNotEmpty()) {
|
||||||
|
val lastBlock = blocks.last()
|
||||||
|
val newLastStyle = lastBlock.style.copy(margin = lastBlock.style.margin.copy(bottom = 0.dp))
|
||||||
|
val newLastBlock =
|
||||||
|
setBlockExpectedHeight(copyBlockWithNewStyle(lastBlock, newLastStyle), lastBlock.expectedHeight)
|
||||||
|
blocks[blocks.size - 1] = newLastBlock
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue