feat(epub): overhaul border system to support individual sides and radii (#44)
- Refactored `BlockStyle` to replace unified border/radius fields with explicit properties for all four sides and corners. - Overhauled `CssParser` to correctly handle CSS border shorthands (`border-bottom`, `border-width`, etc.) and multi-value `border-radius`. - Implemented `drawCssBorders` custom modifier to render side-specific borders, styles (dashed/dotted), and background clipping. - Updated `Paginator` measurement logic to calculate block height and splitting points based on individual side widths.
This commit is contained in:
parent
68c4611187
commit
dff236fa7f
6 changed files with 434 additions and 206 deletions
|
|
@ -221,11 +221,20 @@ class ContentStyler(
|
||||||
} else {
|
} else {
|
||||||
original.backgroundColor
|
original.backgroundColor
|
||||||
}
|
}
|
||||||
val newBorder = original.border?.let {
|
|
||||||
val newBorderColor = CssParser.adaptColorForTheme(it.color, isDarkTheme, isBackground = false)
|
fun themeBorder(b: BorderStyle?): BorderStyle? {
|
||||||
it.copy(color = newBorderColor)
|
if (b == null) return null
|
||||||
|
val newColor = CssParser.adaptColorForTheme(b.color, isDarkTheme, isBackground = false)
|
||||||
|
return b.copy(color = newColor)
|
||||||
}
|
}
|
||||||
original.copy(backgroundColor = newBgColor, border = newBorder)
|
|
||||||
|
original.copy(
|
||||||
|
backgroundColor = newBgColor,
|
||||||
|
borderTop = themeBorder(original.borderTop),
|
||||||
|
borderRight = themeBorder(original.borderRight),
|
||||||
|
borderBottom = themeBorder(original.borderBottom),
|
||||||
|
borderLeft = themeBorder(original.borderLeft)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return style.copy(spanStyle = newSpanStyle, blockStyle = newBlockStyle)
|
return style.copy(spanStyle = newSpanStyle, blockStyle = newBlockStyle)
|
||||||
|
|
|
||||||
|
|
@ -385,6 +385,26 @@ object CssParser {
|
||||||
var marginBottomStr: String? = null
|
var marginBottomStr: String? = null
|
||||||
var marginLeftStr: String? = null
|
var marginLeftStr: String? = null
|
||||||
|
|
||||||
|
var borderTopWidth: Dp? = null
|
||||||
|
var borderRightWidth: Dp? = null
|
||||||
|
var borderBottomWidth: Dp? = null
|
||||||
|
var borderLeftWidth: Dp? = null
|
||||||
|
|
||||||
|
var borderTopStyle: String? = null
|
||||||
|
var borderRightStyle: String? = null
|
||||||
|
var borderBottomStyle: String? = null
|
||||||
|
var borderLeftStyle: String? = null
|
||||||
|
|
||||||
|
var borderTopColor: Color? = null
|
||||||
|
var borderRightColor: Color? = null
|
||||||
|
var borderBottomColor: Color? = null
|
||||||
|
var borderLeftColor: Color? = null
|
||||||
|
|
||||||
|
var borderTopLeftRadius: Dp = 0.dp
|
||||||
|
var borderTopRightRadius: Dp = 0.dp
|
||||||
|
var borderBottomRightRadius: Dp = 0.dp
|
||||||
|
var borderBottomLeftRadius: Dp = 0.dp
|
||||||
|
|
||||||
splitDeclarations(properties).filter { it.isNotBlank() }.forEach { prop ->
|
splitDeclarations(properties).filter { it.isNotBlank() }.forEach { prop ->
|
||||||
val parts = prop.split(':', limit = 2).map { it.trim() }
|
val parts = prop.split(':', limit = 2).map { it.trim() }
|
||||||
if (parts.size == 2) {
|
if (parts.size == 2) {
|
||||||
|
|
@ -401,7 +421,6 @@ object CssParser {
|
||||||
valueWithImportant
|
valueWithImportant
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to update border props ONLY if this border is significant
|
|
||||||
fun updateUnifiedBorder(
|
fun updateUnifiedBorder(
|
||||||
widthStr: String?,
|
widthStr: String?,
|
||||||
colorStr: String?,
|
colorStr: String?,
|
||||||
|
|
@ -410,12 +429,6 @@ object CssParser {
|
||||||
val parsedWidth = widthStr?.let { parseCssSizeToDp(it, baseFontSizeSp, density, containerWidthPx) } ?: 0.dp
|
val parsedWidth = widthStr?.let { parseCssSizeToDp(it, baseFontSizeSp, density, containerWidthPx) } ?: 0.dp
|
||||||
val parsedColor = colorStr?.let { parseColor(it) }?.let { this@CssParser.adaptColorForTheme(it, isDarkTheme, isBackground = false) }
|
val parsedColor = colorStr?.let { parseColor(it) }?.let { this@CssParser.adaptColorForTheme(it, isDarkTheme, isBackground = false) }
|
||||||
|
|
||||||
// We update the unified style if:
|
|
||||||
// 1. We found a width larger than what we've seen (prioritize visible borders)
|
|
||||||
// 2. Or we haven't seen any width yet and this is the first definition
|
|
||||||
// 3. Or the specific property is just setting style/color and we want to take the last one defined (standard CSS cascade behavior for same-specificity)
|
|
||||||
// However, for separate sides (left vs bottom), we strictly prioritize the one with width.
|
|
||||||
|
|
||||||
val isExplicitWidth = widthStr != null
|
val isExplicitWidth = widthStr != null
|
||||||
|
|
||||||
if (parsedWidth > maxBorderWidthFound) {
|
if (parsedWidth > maxBorderWidthFound) {
|
||||||
|
|
@ -423,19 +436,15 @@ object CssParser {
|
||||||
if (parsedColor != null) finalBorderColor = parsedColor
|
if (parsedColor != null) finalBorderColor = parsedColor
|
||||||
if (styleStr != null) finalBorderStyle = styleStr
|
if (styleStr != null) finalBorderStyle = styleStr
|
||||||
} else if (parsedWidth == maxBorderWidthFound && maxBorderWidthFound > 0.dp) {
|
} else if (parsedWidth == maxBorderWidthFound && maxBorderWidthFound > 0.dp) {
|
||||||
// If equal non-zero width, let last defined win (cascade)
|
|
||||||
if (parsedColor != null) finalBorderColor = parsedColor
|
if (parsedColor != null) finalBorderColor = parsedColor
|
||||||
if (styleStr != null) finalBorderStyle = styleStr
|
if (styleStr != null) finalBorderStyle = styleStr
|
||||||
} else if (!isExplicitWidth) {
|
} else if (!isExplicitWidth) {
|
||||||
// Just updating color or style without width
|
|
||||||
if (parsedColor != null) finalBorderColor = parsedColor
|
if (parsedColor != null) finalBorderColor = parsedColor
|
||||||
if (styleStr != null) finalBorderStyle = styleStr
|
if (styleStr != null) finalBorderStyle = styleStr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
when (key) {
|
when (key) {
|
||||||
// ... [Keep existing cases for font-family, font-size, font-weight, font-style, color, text-align, line-height, text-indent, text-decoration, letter-spacing, text-transform, font-variant, margin, margin-*, padding, padding-*, width, max-width, height, background-color] ...
|
|
||||||
|
|
||||||
"font-family" -> {
|
"font-family" -> {
|
||||||
fontFamilies = value.split(',')
|
fontFamilies = value.split(',')
|
||||||
.map { it.trim().removeSurrounding("\"").removeSurrounding("'").lowercase() }
|
.map { it.trim().removeSurrounding("\"").removeSurrounding("'").lowercase() }
|
||||||
|
|
@ -581,40 +590,78 @@ object CssParser {
|
||||||
backgroundColor = this@CssParser.adaptColorForTheme(originalColor, isDarkTheme, isBackground = true)
|
backgroundColor = this@CssParser.adaptColorForTheme(originalColor, isDarkTheme, isBackground = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Border Properties - Logic Updated
|
// Border Properties
|
||||||
"border-width" -> updateUnifiedBorder(value, null, null)
|
"border-width" -> {
|
||||||
"border-color" -> updateUnifiedBorder(null, value, null)
|
val widths = parseShorthand4(value, baseFontSizeSp, density, containerWidthPx)
|
||||||
"border-style" -> updateUnifiedBorder(null, null, value)
|
borderTopWidth = widths[0]; borderRightWidth = widths[1]; borderBottomWidth = widths[2]; borderLeftWidth = widths[3]
|
||||||
|
|
||||||
"border-top-width", "border-bottom-width", "border-left-width", "border-right-width" -> {
|
|
||||||
updateUnifiedBorder(value, null, null)
|
|
||||||
}
|
}
|
||||||
"border-top-color", "border-bottom-color", "border-left-color", "border-right-color" -> {
|
"border-style" -> {
|
||||||
updateUnifiedBorder(null, value, null)
|
val styles = parseShorthand4Strings(value)
|
||||||
|
borderTopStyle = styles[0]; borderRightStyle = styles[1]; borderBottomStyle = styles[2]; borderLeftStyle = styles[3]
|
||||||
}
|
}
|
||||||
"border-top-style", "border-bottom-style", "border-left-style", "border-right-style" -> {
|
"border-color" -> {
|
||||||
updateUnifiedBorder(null, null, value)
|
val colors = parseShorthand4Colors(value)
|
||||||
|
borderTopColor = colors[0]; borderRightColor = colors[1]; borderBottomColor = colors[2]; borderLeftColor = colors[3]
|
||||||
}
|
}
|
||||||
|
|
||||||
"border-bottom", "border-top", "border-left", "border-right", "border" -> {
|
"border-top-width" -> borderTopWidth = parseCssSizeToDp(value, baseFontSizeSp, density, containerWidthPx)
|
||||||
val borderParts = value.split(" ").filter { it.isNotBlank() }
|
"border-right-width" -> borderRightWidth = parseCssSizeToDp(value, baseFontSizeSp, density, containerWidthPx)
|
||||||
var widthVal: String? = null
|
"border-bottom-width" -> borderBottomWidth = parseCssSizeToDp(value, baseFontSizeSp, density, containerWidthPx)
|
||||||
var colorVal: String? = null
|
"border-left-width" -> borderLeftWidth = parseCssSizeToDp(value, baseFontSizeSp, density, containerWidthPx)
|
||||||
var styleVal: String? = null
|
|
||||||
|
|
||||||
borderParts.forEach { part ->
|
"border-top-style" -> borderTopStyle = value
|
||||||
val parsedWidth = parseCssSizeToDp(part, baseFontSizeSp, density, containerWidthPx)
|
"border-right-style" -> borderRightStyle = value
|
||||||
if (parsedWidth > 0.dp || part == "0" || part == "0px" || BORDER_WIDTH_KEYWORDS.containsKey(part)) {
|
"border-bottom-style" -> borderBottomStyle = value
|
||||||
widthVal = part
|
"border-left-style" -> borderLeftStyle = value
|
||||||
} else if (part in listOf("solid", "dotted", "dashed", "double", "groove", "ridge", "inset", "outset")) {
|
|
||||||
styleVal = part
|
"border-top-color" -> borderTopColor = parseColor(value)
|
||||||
} else if (parseColor(part) != null) {
|
"border-right-color" -> borderRightColor = parseColor(value)
|
||||||
colorVal = part
|
"border-bottom-color" -> borderBottomColor = parseColor(value)
|
||||||
}
|
"border-left-color" -> borderLeftColor = parseColor(value)
|
||||||
}
|
|
||||||
updateUnifiedBorder(widthVal, colorVal, styleVal)
|
"border-top" -> {
|
||||||
|
val (w, s, c) = parseBorderShorthand(value, baseFontSizeSp, density, containerWidthPx)
|
||||||
|
if (w != null) borderTopWidth = w
|
||||||
|
if (s != null) borderTopStyle = s
|
||||||
|
if (c != null) borderTopColor = c
|
||||||
}
|
}
|
||||||
// End Border Properties
|
"border-right" -> {
|
||||||
|
val (w, s, c) = parseBorderShorthand(value, baseFontSizeSp, density, containerWidthPx)
|
||||||
|
if (w != null) borderRightWidth = w
|
||||||
|
if (s != null) borderRightStyle = s
|
||||||
|
if (c != null) borderRightColor = c
|
||||||
|
}
|
||||||
|
"border-bottom" -> {
|
||||||
|
val (w, s, c) = parseBorderShorthand(value, baseFontSizeSp, density, containerWidthPx)
|
||||||
|
if (w != null) borderBottomWidth = w
|
||||||
|
if (s != null) borderBottomStyle = s
|
||||||
|
if (c != null) borderBottomColor = c
|
||||||
|
}
|
||||||
|
"border-left" -> {
|
||||||
|
val (w, s, c) = parseBorderShorthand(value, baseFontSizeSp, density, containerWidthPx)
|
||||||
|
if (w != null) borderLeftWidth = w
|
||||||
|
if (s != null) borderLeftStyle = s
|
||||||
|
if (c != null) borderLeftColor = c
|
||||||
|
}
|
||||||
|
|
||||||
|
"border" -> {
|
||||||
|
val (w, s, c) = parseBorderShorthand(value, baseFontSizeSp, density, containerWidthPx)
|
||||||
|
if (w != null) { borderTopWidth = w; borderRightWidth = w; borderBottomWidth = w; borderLeftWidth = w }
|
||||||
|
if (s != null) { borderTopStyle = s; borderRightStyle = s; borderBottomStyle = s; borderLeftStyle = s }
|
||||||
|
if (c != null) { borderTopColor = c; borderRightColor = c; borderBottomColor = c; borderLeftColor = c }
|
||||||
|
}
|
||||||
|
|
||||||
|
"border-radius" -> {
|
||||||
|
val radii = parseShorthand4(value, baseFontSizeSp, density, containerWidthPx)
|
||||||
|
borderTopLeftRadius = radii[0]
|
||||||
|
borderTopRightRadius = radii[1]
|
||||||
|
borderBottomRightRadius = radii[2]
|
||||||
|
borderBottomLeftRadius = radii[3]
|
||||||
|
}
|
||||||
|
"border-top-left-radius" -> borderTopLeftRadius = parseCssSizeToDp(value, baseFontSizeSp, density, containerWidthPx)
|
||||||
|
"border-top-right-radius" -> borderTopRightRadius = parseCssSizeToDp(value, baseFontSizeSp, density, containerWidthPx)
|
||||||
|
"border-bottom-right-radius" -> borderBottomRightRadius = parseCssSizeToDp(value, baseFontSizeSp, density, containerWidthPx)
|
||||||
|
"border-bottom-left-radius" -> borderBottomLeftRadius = parseCssSizeToDp(value, baseFontSizeSp, density, containerWidthPx)
|
||||||
|
|
||||||
"border-collapse" -> {
|
"border-collapse" -> {
|
||||||
if (value in listOf("collapse", "separate")) {
|
if (value in listOf("collapse", "separate")) {
|
||||||
|
|
@ -624,9 +671,6 @@ object CssParser {
|
||||||
"border-spacing" -> {
|
"border-spacing" -> {
|
||||||
borderSpacing = parseCssSizeToDp(value, baseFontSizeSp, density, containerWidthPx)
|
borderSpacing = parseCssSizeToDp(value, baseFontSizeSp, density, containerWidthPx)
|
||||||
}
|
}
|
||||||
"border-radius" -> borderRadius = parseCssSizeToDp(value, baseFontSizeSp, density, containerWidthPx)
|
|
||||||
|
|
||||||
// ... [Keep existing cases for list-style-*, page-break-*, display, flex-*, filter, box-sizing, content, position, top/left/etc, float, hyphens, etc] ...
|
|
||||||
|
|
||||||
"list-style-type" -> {
|
"list-style-type" -> {
|
||||||
listStyleType = value
|
listStyleType = value
|
||||||
|
|
@ -727,7 +771,6 @@ object CssParser {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updated: Use the maxBorderWidthFound and corresponding colors
|
|
||||||
val finalBorder = if (maxBorderWidthFound > 0.dp && finalBorderStyle != null) {
|
val finalBorder = if (maxBorderWidthFound > 0.dp && finalBorderStyle != null) {
|
||||||
val borderColor = finalBorderColor ?: spanStyle.color.takeIf { it.isSpecified } ?: Color.Black
|
val borderColor = finalBorderColor ?: spanStyle.color.takeIf { it.isSpecified } ?: Color.Black
|
||||||
BorderStyle(
|
BorderStyle(
|
||||||
|
|
@ -737,9 +780,35 @@ object CssParser {
|
||||||
)
|
)
|
||||||
} else null
|
} else null
|
||||||
|
|
||||||
|
fun makeBorder(width: Dp?, style: String?, color: Color?): BorderStyle? {
|
||||||
|
val finalWidth = width ?: if (style != null && style != "none" && style != "hidden") 3.dp else 0.dp
|
||||||
|
val finalStyle = style ?: "none"
|
||||||
|
val finalColor = color ?: spanStyle.color.takeIf { it.isSpecified } ?: Color.Black
|
||||||
|
|
||||||
|
val adaptedColor = this@CssParser.adaptColorForTheme(finalColor, isDarkTheme, isBackground = false)
|
||||||
|
|
||||||
|
if (finalWidth > 0.dp && finalStyle != "none" && finalStyle != "hidden") {
|
||||||
|
return BorderStyle(finalWidth, adaptedColor, finalStyle)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val finalBorderTop = makeBorder(borderTopWidth, borderTopStyle, borderTopColor)
|
||||||
|
val finalBorderRight = makeBorder(borderRightWidth, borderRightStyle, borderRightColor)
|
||||||
|
val finalBorderBottom = makeBorder(borderBottomWidth, borderBottomStyle, borderBottomColor)
|
||||||
|
val finalBorderLeft = makeBorder(borderLeftWidth, borderLeftStyle, borderLeftColor)
|
||||||
|
|
||||||
val blockStyle = BlockStyle(
|
val blockStyle = BlockStyle(
|
||||||
margin = margin, padding = padding, width = width, maxWidth = maxWidth, height = height,
|
margin = margin, padding = padding, width = width, maxWidth = maxWidth, height = height,
|
||||||
backgroundColor = backgroundColor, border = finalBorder,
|
backgroundColor = backgroundColor,
|
||||||
|
borderTop = finalBorderTop,
|
||||||
|
borderRight = finalBorderRight,
|
||||||
|
borderBottom = finalBorderBottom,
|
||||||
|
borderLeft = finalBorderLeft,
|
||||||
|
borderTopLeftRadius = borderTopLeftRadius,
|
||||||
|
borderTopRightRadius = borderTopRightRadius,
|
||||||
|
borderBottomRightRadius = borderBottomRightRadius,
|
||||||
|
borderBottomLeftRadius = borderBottomLeftRadius,
|
||||||
listStyleType = listStyleType,
|
listStyleType = listStyleType,
|
||||||
listStyleImage = listStyleImage,
|
listStyleImage = listStyleImage,
|
||||||
pageBreakInsideAvoid = pageBreakInsideAvoid,
|
pageBreakInsideAvoid = pageBreakInsideAvoid,
|
||||||
|
|
@ -759,12 +828,75 @@ object CssParser {
|
||||||
horizontalAlign = finalHorizontalAlign,
|
horizontalAlign = finalHorizontalAlign,
|
||||||
filter = filter,
|
filter = filter,
|
||||||
borderCollapse = borderCollapse,
|
borderCollapse = borderCollapse,
|
||||||
borderSpacing = borderSpacing,
|
borderSpacing = borderSpacing
|
||||||
borderRadius = borderRadius
|
|
||||||
)
|
)
|
||||||
return CssStyle(spanStyle, paragraphStyle, blockStyle, fontFamilies, display, fontSize, textTransform, boxSizing, content, hyphens, fontVariantNumeric, textEmphasis)
|
return CssStyle(spanStyle, paragraphStyle, blockStyle, fontFamilies, display, fontSize, textTransform, boxSizing, content, hyphens, fontVariantNumeric, textEmphasis)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun parseShorthand4(value: String, baseFontSize: Float, density: Float, containerWidth: Int): List<Dp> {
|
||||||
|
val parts = value.split(' ').filter { it.isNotBlank() }
|
||||||
|
val dps = parts.map { parseCssSizeToDp(it, baseFontSize, density, containerWidth) }
|
||||||
|
return when (dps.size) {
|
||||||
|
1 -> listOf(dps[0], dps[0], dps[0], dps[0])
|
||||||
|
2 -> listOf(dps[0], dps[1], dps[0], dps[1]) // Top/Bottom, Left/Right
|
||||||
|
3 -> listOf(dps[0], dps[1], dps[2], dps[1]) // Top, Left/Right, Bottom
|
||||||
|
4 -> listOf(dps[0], dps[1], dps[2], dps[3]) // Top, Right, Bottom, Left
|
||||||
|
else -> listOf(0.dp, 0.dp, 0.dp, 0.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseShorthand4Strings(value: String): List<String?> {
|
||||||
|
val parts = value.split(' ').filter { it.isNotBlank() }
|
||||||
|
return when (parts.size) {
|
||||||
|
1 -> listOf(parts[0], parts[0], parts[0], parts[0])
|
||||||
|
2 -> listOf(parts[0], parts[1], parts[0], parts[1])
|
||||||
|
3 -> listOf(parts[0], parts[1], parts[2], parts[1])
|
||||||
|
4 -> listOf(parts[0], parts[1], parts[2], parts[3])
|
||||||
|
else -> listOf(null, null, null, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseShorthand4Colors(value: String): List<Color?> {
|
||||||
|
if (value.contains("(") || value.contains(",")) {
|
||||||
|
val c = parseColor(value)
|
||||||
|
return listOf(c, c, c, c)
|
||||||
|
}
|
||||||
|
val parts = value.split(' ').filter { it.isNotBlank() }
|
||||||
|
val colors = parts.map { parseColor(it) }
|
||||||
|
return when (colors.size) {
|
||||||
|
1 -> listOf(colors[0], colors[0], colors[0], colors[0])
|
||||||
|
2 -> listOf(colors[0], colors[1], colors[0], colors[1])
|
||||||
|
3 -> listOf(colors[0], colors[1], colors[2], colors[1])
|
||||||
|
4 -> listOf(colors[0], colors[1], colors[2], colors[3])
|
||||||
|
else -> listOf(null, null, null, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseBorderShorthand(value: String, baseFontSize: Float, density: Float, containerWidth: Int): Triple<Dp?, String?, Color?> {
|
||||||
|
val parts = value.split(" ").filter { it.isNotBlank() }
|
||||||
|
var w: Dp? = null
|
||||||
|
var s: String? = null
|
||||||
|
var c: Color? = null
|
||||||
|
|
||||||
|
val keywords = mapOf("thin" to 1.dp, "medium" to 3.dp, "thick" to 5.dp)
|
||||||
|
|
||||||
|
parts.forEach { part ->
|
||||||
|
val lower = part.lowercase()
|
||||||
|
if (keywords.containsKey(lower)) {
|
||||||
|
w = keywords[lower]
|
||||||
|
} else if (lower.endsWith("px") || lower.endsWith("em") || lower.endsWith("rem") || lower.endsWith("%") || lower.first().isDigit()) {
|
||||||
|
val parsed = parseCssSizeToDp(part, baseFontSize, density, containerWidth)
|
||||||
|
if (parsed > 0.dp || part == "0") w = parsed
|
||||||
|
} else if (lower in listOf("solid", "dashed", "dotted", "double", "none", "hidden", "groove", "ridge", "inset", "outset")) {
|
||||||
|
s = lower
|
||||||
|
} else {
|
||||||
|
val parsedColor = parseColor(part)
|
||||||
|
if (parsedColor != null) c = parsedColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Triple(w, s, c)
|
||||||
|
}
|
||||||
|
|
||||||
// ADD the parseCssSizeToDp function here at the bottom of the object or file
|
// ADD the parseCssSizeToDp function here at the bottom of the object or file
|
||||||
internal fun parseCssSizeToDp(
|
internal fun parseCssSizeToDp(
|
||||||
size: String,
|
size: String,
|
||||||
|
|
|
||||||
|
|
@ -256,9 +256,15 @@ private class SemanticHtmlParser(
|
||||||
val result = when (val tagName = element.tagName().lowercase()) {
|
val result = when (val tagName = element.tagName().lowercase()) {
|
||||||
"div", "header", "section", "article", "aside", "main", "footer", "nav", "figure" -> {
|
"div", "header", "section", "article", "aside", "main", "footer", "nav", "figure" -> {
|
||||||
val hasBoxStyles = elementStyle.blockStyle.backgroundColor.isSpecified ||
|
val hasBoxStyles = elementStyle.blockStyle.backgroundColor.isSpecified ||
|
||||||
elementStyle.blockStyle.border != null ||
|
elementStyle.blockStyle.borderTop != null ||
|
||||||
|
elementStyle.blockStyle.borderRight != null ||
|
||||||
|
elementStyle.blockStyle.borderBottom != null ||
|
||||||
|
elementStyle.blockStyle.borderLeft != null ||
|
||||||
elementStyle.blockStyle.padding != BoxBorders() ||
|
elementStyle.blockStyle.padding != BoxBorders() ||
|
||||||
elementStyle.blockStyle.borderRadius > 0.dp
|
elementStyle.blockStyle.borderTopLeftRadius > 0.dp ||
|
||||||
|
elementStyle.blockStyle.borderTopRightRadius > 0.dp ||
|
||||||
|
elementStyle.blockStyle.borderBottomRightRadius > 0.dp ||
|
||||||
|
elementStyle.blockStyle.borderBottomLeftRadius > 0.dp
|
||||||
|
|
||||||
if (hasBoxStyles) {
|
if (hasBoxStyles) {
|
||||||
val children = element.children().flatMap { child ->
|
val children = element.children().flatMap { child ->
|
||||||
|
|
|
||||||
|
|
@ -88,11 +88,11 @@ import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.composed
|
import androidx.compose.ui.composed
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.draw.drawBehind
|
import androidx.compose.ui.draw.drawBehind
|
||||||
import androidx.compose.ui.draw.drawWithContent
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.geometry.Rect
|
import androidx.compose.ui.geometry.Rect
|
||||||
|
import androidx.compose.ui.geometry.toRect
|
||||||
import androidx.compose.ui.graphics.BlendMode
|
import androidx.compose.ui.graphics.BlendMode
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
@ -100,6 +100,7 @@ import androidx.compose.ui.graphics.ColorFilter
|
||||||
import androidx.compose.ui.graphics.ColorMatrix
|
import androidx.compose.ui.graphics.ColorMatrix
|
||||||
import androidx.compose.ui.graphics.Path
|
import androidx.compose.ui.graphics.Path
|
||||||
import androidx.compose.ui.graphics.PathEffect
|
import androidx.compose.ui.graphics.PathEffect
|
||||||
|
import androidx.compose.ui.graphics.drawscope.Fill
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
import androidx.compose.ui.graphics.drawscope.clipPath
|
import androidx.compose.ui.graphics.drawscope.clipPath
|
||||||
import androidx.compose.ui.graphics.drawscope.clipRect
|
import androidx.compose.ui.graphics.drawscope.clipRect
|
||||||
|
|
@ -1588,25 +1589,9 @@ internal fun PaginatedReaderContent(
|
||||||
|
|
||||||
val styleModifier = alignModifier
|
val styleModifier = alignModifier
|
||||||
.then(if (block.style.horizontalAlign == "center") widthModifier else Modifier)
|
.then(if (block.style.horizontalAlign == "center") widthModifier else Modifier)
|
||||||
.then(
|
.drawCssBorders(
|
||||||
if (block.style.borderRadius > 0.dp) Modifier.clip(RoundedCornerShape(block.style.borderRadius))
|
blockStyle = block.style,
|
||||||
else Modifier
|
density = density
|
||||||
)
|
|
||||||
.then(
|
|
||||||
if (block.style.backgroundColor.isSpecified) {
|
|
||||||
Modifier.background(
|
|
||||||
block.style.backgroundColor,
|
|
||||||
shape = if (block.style.borderRadius > 0.dp) RoundedCornerShape(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
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val diagnosticModifier = Modifier
|
val diagnosticModifier = Modifier
|
||||||
|
|
@ -1636,12 +1621,11 @@ internal fun PaginatedReaderContent(
|
||||||
.then(styleModifier)
|
.then(styleModifier)
|
||||||
|
|
||||||
Box(modifier = diagnosticModifier) {
|
Box(modifier = diagnosticModifier) {
|
||||||
val borderWidth = block.style.border?.width ?: 0.dp
|
|
||||||
val paddingModifier = Modifier.padding(
|
val paddingModifier = Modifier.padding(
|
||||||
start = block.style.padding.left.coerceAtLeast(0.dp) + borderWidth,
|
start = block.style.padding.left.coerceAtLeast(0.dp) + (block.style.borderLeft?.width ?: 0.dp),
|
||||||
top = block.style.padding.top.coerceAtLeast(0.dp) + borderWidth,
|
top = block.style.padding.top.coerceAtLeast(0.dp) + (block.style.borderTop?.width ?: 0.dp),
|
||||||
end = block.style.padding.right.coerceAtLeast(0.dp) + borderWidth,
|
end = block.style.padding.right.coerceAtLeast(0.dp) + (block.style.borderRight?.width ?: 0.dp),
|
||||||
bottom = block.style.padding.bottom.coerceAtLeast(0.dp) + borderWidth
|
bottom = block.style.padding.bottom.coerceAtLeast(0.dp) + (block.style.borderBottom?.width ?: 0.dp)
|
||||||
).then(
|
).then(
|
||||||
if (block.style.horizontalAlign != "center") widthModifier else Modifier.fillMaxWidth()
|
if (block.style.horizontalAlign != "center") widthModifier else Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
|
|
@ -2322,61 +2306,16 @@ internal fun PaginatedReaderContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
is SpacerBlock -> {
|
is SpacerBlock -> {
|
||||||
val border = block.style.border
|
Box(
|
||||||
if (border != null && border.width > 0.dp) {
|
modifier = Modifier
|
||||||
Box(
|
.fillMaxWidth()
|
||||||
modifier = Modifier
|
.height(block.height)
|
||||||
.fillMaxWidth()
|
.drawCssBorders(block.style, density)
|
||||||
.height(
|
)
|
||||||
border.width
|
|
||||||
)
|
|
||||||
.drawBehind {
|
|
||||||
val strokeWidth =
|
|
||||||
border.width.toPx()
|
|
||||||
val pathEffect =
|
|
||||||
when (border.style) {
|
|
||||||
"dotted" -> PathEffect.dashPathEffect(
|
|
||||||
floatArrayOf(
|
|
||||||
strokeWidth,
|
|
||||||
strokeWidth * 2f
|
|
||||||
), 0f
|
|
||||||
)
|
|
||||||
|
|
||||||
"dashed" -> PathEffect.dashPathEffect(
|
|
||||||
floatArrayOf(
|
|
||||||
strokeWidth * 3f,
|
|
||||||
strokeWidth * 2f
|
|
||||||
), 0f
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
drawLine(
|
|
||||||
color = border.color,
|
|
||||||
start = Offset(
|
|
||||||
0f, strokeWidth / 2f
|
|
||||||
),
|
|
||||||
end = Offset(
|
|
||||||
size.width,
|
|
||||||
strokeWidth / 2f
|
|
||||||
),
|
|
||||||
strokeWidth = strokeWidth,
|
|
||||||
pathEffect = pathEffect
|
|
||||||
)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Spacer(Modifier.height(block.height))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is TableBlock -> {
|
is TableBlock -> {
|
||||||
// Table-level margin/background/border/padding
|
Column(modifier = paddingModifier) {
|
||||||
// are already
|
|
||||||
// applied by the outer Box wrapper. Only set
|
|
||||||
// width here.
|
|
||||||
val tableModifier = paddingModifier
|
|
||||||
|
|
||||||
Column(modifier = tableModifier) {
|
|
||||||
block.rows.forEach { tableRow ->
|
block.rows.forEach { tableRow ->
|
||||||
Row(
|
Row(
|
||||||
Modifier
|
Modifier
|
||||||
|
|
@ -2428,14 +2367,7 @@ internal fun PaginatedReaderContent(
|
||||||
Modifier
|
Modifier
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.then(cellStyle.border?.let { border ->
|
.drawCssBorders(cellStyle, density)
|
||||||
Modifier.border(
|
|
||||||
BorderStroke(
|
|
||||||
border.width,
|
|
||||||
border.color
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} ?: Modifier)
|
|
||||||
.padding(
|
.padding(
|
||||||
start = cellStyle.padding.left.coerceAtLeast(
|
start = cellStyle.padding.left.coerceAtLeast(
|
||||||
0.dp
|
0.dp
|
||||||
|
|
@ -2508,23 +2440,11 @@ internal fun PaginatedReaderContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
is SpacerBlock -> {
|
is SpacerBlock -> {
|
||||||
val spacerModifier =
|
|
||||||
if (blockInCell.style.border != null) {
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(
|
|
||||||
blockInCell.style.border.width
|
|
||||||
)
|
|
||||||
.background(
|
|
||||||
blockInCell.style.border.color
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Modifier.height(
|
|
||||||
blockInCell.height
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(
|
Spacer(
|
||||||
modifier = spacerModifier
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(blockInCell.height)
|
||||||
|
.drawCssBorders(blockInCell.style, density)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3311,17 +3231,12 @@ private fun RenderFlexChildBlock(
|
||||||
}
|
}
|
||||||
|
|
||||||
is SpacerBlock -> {
|
is SpacerBlock -> {
|
||||||
val border = childBlock.style.border
|
Box(
|
||||||
if (border != null && border.width > 0.dp) {
|
modifier = Modifier
|
||||||
Box(
|
.fillMaxWidth()
|
||||||
modifier = Modifier
|
.height(childBlock.height)
|
||||||
.fillMaxWidth()
|
.drawCssBorders(childBlock.style, density)
|
||||||
.height(border.width)
|
)
|
||||||
.background(border.color)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Spacer(Modifier.height(childBlock.height))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is TableBlock -> {
|
is TableBlock -> {
|
||||||
|
|
@ -3353,11 +3268,7 @@ private fun RenderFlexChildBlock(
|
||||||
)
|
)
|
||||||
else Modifier
|
else Modifier
|
||||||
)
|
)
|
||||||
.then(cellStyle.border?.let {
|
.drawCssBorders(cellStyle, density)
|
||||||
Modifier.border(
|
|
||||||
BorderStroke(it.width, it.color)
|
|
||||||
)
|
|
||||||
} ?: Modifier)
|
|
||||||
.padding(
|
.padding(
|
||||||
start = cellStyle.padding.left.coerceAtLeast(
|
start = cellStyle.padding.left.coerceAtLeast(
|
||||||
0.dp
|
0.dp
|
||||||
|
|
@ -3586,4 +3497,161 @@ private fun Modifier.realisticBookPage(
|
||||||
drawContent()
|
drawContent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("KotlinConstantConditions")
|
||||||
|
@Composable
|
||||||
|
fun Modifier.drawCssBorders(
|
||||||
|
blockStyle: BlockStyle,
|
||||||
|
@Suppress("unused") density: Density
|
||||||
|
): Modifier = this.drawBehind {
|
||||||
|
val topWidth = blockStyle.borderTop?.width?.toPx() ?: 0f
|
||||||
|
val rightWidth = blockStyle.borderRight?.width?.toPx() ?: 0f
|
||||||
|
val bottomWidth = blockStyle.borderBottom?.width?.toPx() ?: 0f
|
||||||
|
val leftWidth = blockStyle.borderLeft?.width?.toPx() ?: 0f
|
||||||
|
|
||||||
|
val tlRadius = blockStyle.borderTopLeftRadius.toPx()
|
||||||
|
val trRadius = blockStyle.borderTopRightRadius.toPx()
|
||||||
|
val brRadius = blockStyle.borderBottomRightRadius.toPx()
|
||||||
|
val blRadius = blockStyle.borderBottomLeftRadius.toPx()
|
||||||
|
|
||||||
|
if (blockStyle.backgroundColor.isSpecified && blockStyle.backgroundColor != Color.Transparent) {
|
||||||
|
val bgPath = Path().apply {
|
||||||
|
addRoundRect(
|
||||||
|
androidx.compose.ui.geometry.RoundRect(
|
||||||
|
rect = size.toRect(),
|
||||||
|
topLeft = androidx.compose.ui.geometry.CornerRadius(tlRadius, tlRadius),
|
||||||
|
topRight = androidx.compose.ui.geometry.CornerRadius(trRadius, trRadius),
|
||||||
|
bottomRight = androidx.compose.ui.geometry.CornerRadius(brRadius, brRadius),
|
||||||
|
bottomLeft = androidx.compose.ui.geometry.CornerRadius(blRadius, blRadius)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
drawPath(bgPath, color = blockStyle.backgroundColor, style = Fill)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Helper for PathEffects
|
||||||
|
fun getPathEffect(style: String?, width: Float): PathEffect? {
|
||||||
|
return when (style) {
|
||||||
|
"dashed" -> PathEffect.dashPathEffect(floatArrayOf(width * 3f, width * 2f), 0f)
|
||||||
|
"dotted" -> PathEffect.dashPathEffect(floatArrayOf(width, width), 0f)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TOP
|
||||||
|
if (topWidth > 0f && blockStyle.borderTop != null) {
|
||||||
|
val color = blockStyle.borderTop.color
|
||||||
|
val effect = getPathEffect(blockStyle.borderTop.style, topWidth)
|
||||||
|
val offset = topWidth / 2f
|
||||||
|
|
||||||
|
val startX = if (tlRadius > 0) tlRadius else 0f
|
||||||
|
val endX = if (trRadius > 0) size.width - trRadius else size.width
|
||||||
|
|
||||||
|
drawLine(
|
||||||
|
color = color,
|
||||||
|
start = Offset(startX, offset),
|
||||||
|
end = Offset(endX, offset),
|
||||||
|
strokeWidth = topWidth,
|
||||||
|
pathEffect = effect
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BOTTOM
|
||||||
|
if (bottomWidth > 0f && blockStyle.borderBottom != null) {
|
||||||
|
val color = blockStyle.borderBottom.color
|
||||||
|
val effect = getPathEffect(blockStyle.borderBottom.style, bottomWidth)
|
||||||
|
val offset = size.height - (bottomWidth / 2f)
|
||||||
|
|
||||||
|
val startX = if (blRadius > 0) blRadius else 0f
|
||||||
|
val endX = if (brRadius > 0) size.width - brRadius else size.width
|
||||||
|
|
||||||
|
drawLine(
|
||||||
|
color = color,
|
||||||
|
start = Offset(startX, offset),
|
||||||
|
end = Offset(endX, offset),
|
||||||
|
strokeWidth = bottomWidth,
|
||||||
|
pathEffect = effect
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LEFT
|
||||||
|
if (leftWidth > 0f && blockStyle.borderLeft != null) {
|
||||||
|
val color = blockStyle.borderLeft.color
|
||||||
|
val effect = getPathEffect(blockStyle.borderLeft.style, leftWidth)
|
||||||
|
val offset = leftWidth / 2f
|
||||||
|
|
||||||
|
val startY = if (tlRadius > 0) tlRadius else 0f
|
||||||
|
val endY = if (blRadius > 0) size.height - blRadius else size.height
|
||||||
|
|
||||||
|
drawLine(
|
||||||
|
color = color,
|
||||||
|
start = Offset(offset, startY),
|
||||||
|
end = Offset(offset, endY),
|
||||||
|
strokeWidth = leftWidth,
|
||||||
|
pathEffect = effect
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RIGHT
|
||||||
|
if (rightWidth > 0f && blockStyle.borderRight != null) {
|
||||||
|
val color = blockStyle.borderRight.color
|
||||||
|
val effect = getPathEffect(blockStyle.borderRight.style, rightWidth)
|
||||||
|
val offset = size.width - (rightWidth / 2f)
|
||||||
|
|
||||||
|
val startY = if (trRadius > 0) trRadius else 0f
|
||||||
|
val endY = if (brRadius > 0) size.height - brRadius else size.height
|
||||||
|
|
||||||
|
drawLine(
|
||||||
|
color = color,
|
||||||
|
start = Offset(offset, startY),
|
||||||
|
end = Offset(offset, endY),
|
||||||
|
strokeWidth = rightWidth,
|
||||||
|
pathEffect = effect
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tlRadius > 0f && topWidth > 0f && leftWidth > 0f && blockStyle.borderTop != null) {
|
||||||
|
drawArc(
|
||||||
|
color = blockStyle.borderTop.color,
|
||||||
|
startAngle = 180f, sweepAngle = 90f,
|
||||||
|
useCenter = false,
|
||||||
|
topLeft = Offset(leftWidth/2f, topWidth/2f),
|
||||||
|
size = androidx.compose.ui.geometry.Size(tlRadius * 2 - leftWidth, tlRadius * 2 - topWidth),
|
||||||
|
style = Stroke(width = topWidth)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trRadius > 0f && topWidth > 0f && rightWidth > 0f && blockStyle.borderTop != null) {
|
||||||
|
drawArc(
|
||||||
|
color = blockStyle.borderTop.color,
|
||||||
|
startAngle = 270f, sweepAngle = 90f,
|
||||||
|
useCenter = false,
|
||||||
|
topLeft = Offset(size.width - (trRadius * 2) + (rightWidth/2f), topWidth/2f),
|
||||||
|
size = androidx.compose.ui.geometry.Size(trRadius * 2 - rightWidth, trRadius * 2 - topWidth),
|
||||||
|
style = Stroke(width = topWidth)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (brRadius > 0f && bottomWidth > 0f && rightWidth > 0f && blockStyle.borderBottom != null) {
|
||||||
|
drawArc(
|
||||||
|
color = blockStyle.borderBottom.color,
|
||||||
|
startAngle = 0f, sweepAngle = 90f,
|
||||||
|
useCenter = false,
|
||||||
|
topLeft = Offset(size.width - (brRadius * 2) + (rightWidth/2f), size.height - (brRadius * 2) + (bottomWidth/2f)),
|
||||||
|
size = androidx.compose.ui.geometry.Size(brRadius * 2 - rightWidth, brRadius * 2 - bottomWidth),
|
||||||
|
style = Stroke(width = bottomWidth)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blRadius > 0f && bottomWidth > 0f && leftWidth > 0f && blockStyle.borderBottom != null) {
|
||||||
|
drawArc(
|
||||||
|
color = blockStyle.borderBottom.color,
|
||||||
|
startAngle = 90f, sweepAngle = 90f,
|
||||||
|
useCenter = false,
|
||||||
|
topLeft = Offset(leftWidth/2f, size.height - (blRadius * 2) + (bottomWidth/2f)),
|
||||||
|
size = androidx.compose.ui.geometry.Size(blRadius * 2 - leftWidth, blRadius * 2 - bottomWidth),
|
||||||
|
style = Stroke(width = bottomWidth)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -51,28 +51,34 @@ data class BlockStyle(
|
||||||
@ProtoNumber(4) @Serializable(with = DpSerializer::class) val maxWidth: Dp = Dp.Unspecified,
|
@ProtoNumber(4) @Serializable(with = DpSerializer::class) val maxWidth: Dp = Dp.Unspecified,
|
||||||
@ProtoNumber(5) @Serializable(with = DpSerializer::class) val height: Dp = Dp.Unspecified,
|
@ProtoNumber(5) @Serializable(with = DpSerializer::class) val height: Dp = Dp.Unspecified,
|
||||||
@ProtoNumber(6) @Serializable(with = ColorSerializer::class) val backgroundColor: Color = Color.Unspecified,
|
@ProtoNumber(6) @Serializable(with = ColorSerializer::class) val backgroundColor: Color = Color.Unspecified,
|
||||||
@ProtoNumber(7) val border: BorderStyle? = null,
|
@ProtoNumber(7) val borderTop: BorderStyle? = null,
|
||||||
@ProtoNumber(8) val listStyleType: String? = null,
|
@ProtoNumber(8) val borderRight: BorderStyle? = null,
|
||||||
@ProtoNumber(9) val listStyleImage: String? = null,
|
@ProtoNumber(9) val borderBottom: BorderStyle? = null,
|
||||||
@ProtoNumber(10) val pageBreakInsideAvoid: Boolean = false,
|
@ProtoNumber(10) val borderLeft: BorderStyle? = null,
|
||||||
@ProtoNumber(11) val pageBreakAfterAvoid: Boolean = false,
|
@ProtoNumber(11) val listStyleType: String? = null,
|
||||||
@ProtoNumber(12) val boxSizing: String? = null,
|
@ProtoNumber(12) val listStyleImage: String? = null,
|
||||||
@ProtoNumber(13) val float: String? = null,
|
@ProtoNumber(13) val pageBreakInsideAvoid: Boolean = false,
|
||||||
@ProtoNumber(14) val clear: String? = null,
|
@ProtoNumber(14) val pageBreakAfterAvoid: Boolean = false,
|
||||||
@ProtoNumber(15) val position: String? = null,
|
@ProtoNumber(15) val boxSizing: String? = null,
|
||||||
@ProtoNumber(16) @Serializable(with = DpSerializer::class) val top: Dp = Dp.Unspecified,
|
@ProtoNumber(16) val float: String? = null,
|
||||||
@ProtoNumber(17) @Serializable(with = DpSerializer::class) val right: Dp = Dp.Unspecified,
|
@ProtoNumber(17) val clear: String? = null,
|
||||||
@ProtoNumber(18) @Serializable(with = DpSerializer::class) val bottom: Dp = Dp.Unspecified,
|
@ProtoNumber(18) val position: String? = null,
|
||||||
@ProtoNumber(19) @Serializable(with = DpSerializer::class) val left: Dp = Dp.Unspecified,
|
@ProtoNumber(19) @Serializable(with = DpSerializer::class) val top: Dp = Dp.Unspecified,
|
||||||
@ProtoNumber(20) val display: String? = null,
|
@ProtoNumber(20) @Serializable(with = DpSerializer::class) val right: Dp = Dp.Unspecified,
|
||||||
@ProtoNumber(21) val flexDirection: String? = null,
|
@ProtoNumber(21) @Serializable(with = DpSerializer::class) val bottom: Dp = Dp.Unspecified,
|
||||||
@ProtoNumber(22) val justifyContent: String? = null,
|
@ProtoNumber(22) @Serializable(with = DpSerializer::class) val left: Dp = Dp.Unspecified,
|
||||||
@ProtoNumber(23) val alignItems: String? = null,
|
@ProtoNumber(23) val display: String? = null,
|
||||||
@ProtoNumber(24) val horizontalAlign: String? = null,
|
@ProtoNumber(24) val flexDirection: String? = null,
|
||||||
@ProtoNumber(25) val filter: String? = null,
|
@ProtoNumber(25) val justifyContent: String? = null,
|
||||||
@ProtoNumber(26) val borderCollapse: String? = null,
|
@ProtoNumber(26) val alignItems: String? = null,
|
||||||
@ProtoNumber(27) @Serializable(with = DpSerializer::class) val borderSpacing: Dp = 0.dp,
|
@ProtoNumber(27) val horizontalAlign: String? = null,
|
||||||
@ProtoNumber(28) @Serializable(with = DpSerializer::class) val borderRadius: Dp = 0.dp
|
@ProtoNumber(28) val filter: String? = null,
|
||||||
|
@ProtoNumber(29) val borderCollapse: String? = null,
|
||||||
|
@ProtoNumber(30) @Serializable(with = DpSerializer::class) val borderTopLeftRadius: Dp = 0.dp,
|
||||||
|
@ProtoNumber(31) @Serializable(with = DpSerializer::class) val borderTopRightRadius: Dp = 0.dp,
|
||||||
|
@ProtoNumber(32) @Serializable(with = DpSerializer::class) val borderBottomRightRadius: Dp = 0.dp,
|
||||||
|
@ProtoNumber(33) @Serializable(with = DpSerializer::class) val borderBottomLeftRadius: Dp = 0.dp,
|
||||||
|
@ProtoNumber(34) @Serializable(with = DpSerializer::class) val borderSpacing: Dp = 0.dp
|
||||||
) {
|
) {
|
||||||
fun merge(other: BlockStyle): BlockStyle {
|
fun merge(other: BlockStyle): BlockStyle {
|
||||||
return BlockStyle(
|
return BlockStyle(
|
||||||
|
|
@ -92,7 +98,14 @@ data class BlockStyle(
|
||||||
maxWidth = if (other.maxWidth != Dp.Unspecified) other.maxWidth else this.maxWidth,
|
maxWidth = if (other.maxWidth != Dp.Unspecified) other.maxWidth else this.maxWidth,
|
||||||
height = if (other.height != Dp.Unspecified) other.height else this.height,
|
height = if (other.height != Dp.Unspecified) other.height else this.height,
|
||||||
backgroundColor = if (other.backgroundColor.isSpecified) other.backgroundColor else this.backgroundColor,
|
backgroundColor = if (other.backgroundColor.isSpecified) other.backgroundColor else this.backgroundColor,
|
||||||
border = other.border ?: this.border,
|
borderTop = other.borderTop ?: this.borderTop,
|
||||||
|
borderRight = other.borderRight ?: this.borderRight,
|
||||||
|
borderBottom = other.borderBottom ?: this.borderBottom,
|
||||||
|
borderLeft = other.borderLeft ?: this.borderLeft,
|
||||||
|
borderTopLeftRadius = if (other.borderTopLeftRadius != 0.dp) other.borderTopLeftRadius else this.borderTopLeftRadius,
|
||||||
|
borderTopRightRadius = if (other.borderTopRightRadius != 0.dp) other.borderTopRightRadius else this.borderTopRightRadius,
|
||||||
|
borderBottomRightRadius = if (other.borderBottomRightRadius != 0.dp) other.borderBottomRightRadius else this.borderBottomRightRadius,
|
||||||
|
borderBottomLeftRadius = if (other.borderBottomLeftRadius != 0.dp) other.borderBottomLeftRadius else this.borderBottomLeftRadius,
|
||||||
listStyleType = other.listStyleType ?: this.listStyleType,
|
listStyleType = other.listStyleType ?: this.listStyleType,
|
||||||
listStyleImage = other.listStyleImage ?: this.listStyleImage,
|
listStyleImage = other.listStyleImage ?: this.listStyleImage,
|
||||||
pageBreakInsideAvoid = this.pageBreakInsideAvoid || other.pageBreakInsideAvoid,
|
pageBreakInsideAvoid = this.pageBreakInsideAvoid || other.pageBreakInsideAvoid,
|
||||||
|
|
@ -112,8 +125,7 @@ data class BlockStyle(
|
||||||
horizontalAlign = other.horizontalAlign ?: this.horizontalAlign,
|
horizontalAlign = other.horizontalAlign ?: this.horizontalAlign,
|
||||||
filter = other.filter ?: this.filter,
|
filter = other.filter ?: this.filter,
|
||||||
borderCollapse = other.borderCollapse ?: this.borderCollapse,
|
borderCollapse = other.borderCollapse ?: this.borderCollapse,
|
||||||
borderSpacing = if (other.borderSpacing != 0.dp) other.borderSpacing else this.borderSpacing,
|
borderSpacing = if (other.borderSpacing != 0.dp) other.borderSpacing else this.borderSpacing
|
||||||
borderRadius = if (other.borderRadius != 0.dp) other.borderRadius else this.borderRadius
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -288,11 +288,11 @@ class SuspendingAndroidBlockMeasurementProvider(
|
||||||
var splitRowIndex = -1
|
var splitRowIndex = -1
|
||||||
|
|
||||||
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.borderTop?.width?.toPx() ?: 0f)
|
||||||
}.roundToInt()
|
}.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.borderBottom?.width?.toPx() ?: 0f)
|
||||||
}.roundToInt()
|
}.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")
|
||||||
|
|
@ -313,7 +313,8 @@ 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.borderTop?.width?.toPx() ?: 0f) +
|
||||||
|
(cell.style.blockStyle.borderBottom?.width?.toPx() ?: 0f)
|
||||||
}.roundToInt()
|
}.roundToInt()
|
||||||
maxRowHeight = maxOf(maxRowHeight, cellHeight + cellDecoration)
|
maxRowHeight = maxOf(maxRowHeight, cellHeight + cellDecoration)
|
||||||
}
|
}
|
||||||
|
|
@ -344,11 +345,11 @@ class SuspendingAndroidBlockMeasurementProvider(
|
||||||
var splitChildIndex = -1
|
var splitChildIndex = -1
|
||||||
|
|
||||||
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.borderTop?.width?.toPx() ?: 0f)
|
||||||
}.roundToInt()
|
}.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.borderBottom?.width?.toPx() ?: 0f)
|
||||||
}.roundToInt()
|
}.roundToInt()
|
||||||
|
|
||||||
currentHeight += decorationTop
|
currentHeight += decorationTop
|
||||||
|
|
@ -677,10 +678,9 @@ private suspend fun measureBlockHeight(
|
||||||
with(density) {
|
with(density) {
|
||||||
verticalPaddingPx = block.style.padding.top.toPx() + block.style.padding.bottom.toPx()
|
verticalPaddingPx = block.style.padding.top.toPx() + block.style.padding.bottom.toPx()
|
||||||
horizontalPaddingPx = block.style.padding.left.toPx() + block.style.padding.right.toPx()
|
horizontalPaddingPx = block.style.padding.left.toPx() + block.style.padding.right.toPx()
|
||||||
block.style.border?.let {
|
|
||||||
verticalBorderPx = it.width.toPx() * 2
|
verticalBorderPx = (block.style.borderTop?.width?.toPx() ?: 0f) + (block.style.borderBottom?.width?.toPx() ?: 0f)
|
||||||
horizontalBorderPx = it.width.toPx() * 2
|
horizontalBorderPx = (block.style.borderLeft?.width?.toPx() ?: 0f) + (block.style.borderRight?.width?.toPx() ?: 0f)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val isBorderBox = block.style.boxSizing == "border-box"
|
val isBorderBox = block.style.boxSizing == "border-box"
|
||||||
|
|
@ -817,7 +817,8 @@ private suspend fun measureBlockHeight(
|
||||||
var cellDecorationHeight = 0f
|
var cellDecorationHeight = 0f
|
||||||
with(density) {
|
with(density) {
|
||||||
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 }
|
cellDecorationHeight += (cellBlockStyle.borderTop?.width?.toPx() ?: 0f)
|
||||||
|
cellDecorationHeight += (cellBlockStyle.borderBottom?.width?.toPx() ?: 0f)
|
||||||
}
|
}
|
||||||
maxRowHeight = maxOf(maxRowHeight, (cellContentHeight + cellDecorationHeight).roundToInt())
|
maxRowHeight = maxOf(maxRowHeight, (cellContentHeight + cellDecorationHeight).roundToInt())
|
||||||
}
|
}
|
||||||
|
|
@ -1002,11 +1003,11 @@ private suspend fun splitParagraphBlock(
|
||||||
if (text.isEmpty()) return null
|
if (text.isEmpty()) return null
|
||||||
|
|
||||||
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.borderTop?.width?.toPx() ?: 0f)
|
||||||
}.roundToInt()
|
}.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.borderBottom?.width?.toPx() ?: 0f)
|
||||||
}.roundToInt()
|
}.roundToInt()
|
||||||
|
|
||||||
val availableTextHeight = availableHeight - decorationTop - decorationBottom
|
val availableTextHeight = availableHeight - decorationTop - decorationBottom
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue