diff --git a/app/src/main/java/com/aryan/reader/paginatedreader/ContentStyler.kt b/app/src/main/java/com/aryan/reader/paginatedreader/ContentStyler.kt index d78841c..8769f59 100644 --- a/app/src/main/java/com/aryan/reader/paginatedreader/ContentStyler.kt +++ b/app/src/main/java/com/aryan/reader/paginatedreader/ContentStyler.kt @@ -221,11 +221,20 @@ class ContentStyler( } else { original.backgroundColor } - val newBorder = original.border?.let { - val newBorderColor = CssParser.adaptColorForTheme(it.color, isDarkTheme, isBackground = false) - it.copy(color = newBorderColor) + + fun themeBorder(b: BorderStyle?): BorderStyle? { + 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) diff --git a/app/src/main/java/com/aryan/reader/paginatedreader/CssParser.kt b/app/src/main/java/com/aryan/reader/paginatedreader/CssParser.kt index 70cad84..b1e486b 100644 --- a/app/src/main/java/com/aryan/reader/paginatedreader/CssParser.kt +++ b/app/src/main/java/com/aryan/reader/paginatedreader/CssParser.kt @@ -385,6 +385,26 @@ object CssParser { var marginBottomStr: 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 -> val parts = prop.split(':', limit = 2).map { it.trim() } if (parts.size == 2) { @@ -401,7 +421,6 @@ object CssParser { valueWithImportant } - // Helper to update border props ONLY if this border is significant fun updateUnifiedBorder( widthStr: String?, colorStr: String?, @@ -410,12 +429,6 @@ object CssParser { 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) } - // 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 if (parsedWidth > maxBorderWidthFound) { @@ -423,19 +436,15 @@ object CssParser { if (parsedColor != null) finalBorderColor = parsedColor if (styleStr != null) finalBorderStyle = styleStr } else if (parsedWidth == maxBorderWidthFound && maxBorderWidthFound > 0.dp) { - // If equal non-zero width, let last defined win (cascade) if (parsedColor != null) finalBorderColor = parsedColor if (styleStr != null) finalBorderStyle = styleStr } else if (!isExplicitWidth) { - // Just updating color or style without width if (parsedColor != null) finalBorderColor = parsedColor if (styleStr != null) finalBorderStyle = styleStr } } 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" -> { fontFamilies = value.split(',') .map { it.trim().removeSurrounding("\"").removeSurrounding("'").lowercase() } @@ -581,40 +590,78 @@ object CssParser { backgroundColor = this@CssParser.adaptColorForTheme(originalColor, isDarkTheme, isBackground = true) } - // Border Properties - Logic Updated - "border-width" -> updateUnifiedBorder(value, null, null) - "border-color" -> updateUnifiedBorder(null, value, null) - "border-style" -> updateUnifiedBorder(null, null, value) - - "border-top-width", "border-bottom-width", "border-left-width", "border-right-width" -> { - updateUnifiedBorder(value, null, null) + // Border Properties + "border-width" -> { + val widths = parseShorthand4(value, baseFontSizeSp, density, containerWidthPx) + borderTopWidth = widths[0]; borderRightWidth = widths[1]; borderBottomWidth = widths[2]; borderLeftWidth = widths[3] } - "border-top-color", "border-bottom-color", "border-left-color", "border-right-color" -> { - updateUnifiedBorder(null, value, null) + "border-style" -> { + 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" -> { - updateUnifiedBorder(null, null, value) + "border-color" -> { + 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" -> { - val borderParts = value.split(" ").filter { it.isNotBlank() } - var widthVal: String? = null - var colorVal: String? = null - var styleVal: String? = null + "border-top-width" -> borderTopWidth = parseCssSizeToDp(value, baseFontSizeSp, density, containerWidthPx) + "border-right-width" -> borderRightWidth = parseCssSizeToDp(value, baseFontSizeSp, density, containerWidthPx) + "border-bottom-width" -> borderBottomWidth = parseCssSizeToDp(value, baseFontSizeSp, density, containerWidthPx) + "border-left-width" -> borderLeftWidth = parseCssSizeToDp(value, baseFontSizeSp, density, containerWidthPx) - borderParts.forEach { part -> - val parsedWidth = parseCssSizeToDp(part, baseFontSizeSp, density, containerWidthPx) - if (parsedWidth > 0.dp || part == "0" || part == "0px" || BORDER_WIDTH_KEYWORDS.containsKey(part)) { - widthVal = part - } else if (part in listOf("solid", "dotted", "dashed", "double", "groove", "ridge", "inset", "outset")) { - styleVal = part - } else if (parseColor(part) != null) { - colorVal = part - } - } - updateUnifiedBorder(widthVal, colorVal, styleVal) + "border-top-style" -> borderTopStyle = value + "border-right-style" -> borderRightStyle = value + "border-bottom-style" -> borderBottomStyle = value + "border-left-style" -> borderLeftStyle = value + + "border-top-color" -> borderTopColor = parseColor(value) + "border-right-color" -> borderRightColor = parseColor(value) + "border-bottom-color" -> borderBottomColor = parseColor(value) + "border-left-color" -> borderLeftColor = parseColor(value) + + "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" -> { if (value in listOf("collapse", "separate")) { @@ -624,9 +671,6 @@ object CssParser { "border-spacing" -> { 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" -> { listStyleType = value @@ -727,7 +771,6 @@ object CssParser { null } - // Updated: Use the maxBorderWidthFound and corresponding colors val finalBorder = if (maxBorderWidthFound > 0.dp && finalBorderStyle != null) { val borderColor = finalBorderColor ?: spanStyle.color.takeIf { it.isSpecified } ?: Color.Black BorderStyle( @@ -737,9 +780,35 @@ object CssParser { ) } 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( 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, listStyleImage = listStyleImage, pageBreakInsideAvoid = pageBreakInsideAvoid, @@ -759,12 +828,75 @@ object CssParser { horizontalAlign = finalHorizontalAlign, filter = filter, borderCollapse = borderCollapse, - borderSpacing = borderSpacing, - borderRadius = borderRadius + borderSpacing = borderSpacing ) 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 { + 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 { + 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 { + 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 { + 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 internal fun parseCssSizeToDp( size: String, diff --git a/app/src/main/java/com/aryan/reader/paginatedreader/HtmlParser.kt b/app/src/main/java/com/aryan/reader/paginatedreader/HtmlParser.kt index 1dcf5b4..38af4e1 100644 --- a/app/src/main/java/com/aryan/reader/paginatedreader/HtmlParser.kt +++ b/app/src/main/java/com/aryan/reader/paginatedreader/HtmlParser.kt @@ -256,9 +256,15 @@ private class SemanticHtmlParser( val result = when (val tagName = element.tagName().lowercase()) { "div", "header", "section", "article", "aside", "main", "footer", "nav", "figure" -> { 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.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) { val children = element.children().flatMap { child -> diff --git a/app/src/main/java/com/aryan/reader/paginatedreader/PaginatedReader.kt b/app/src/main/java/com/aryan/reader/paginatedreader/PaginatedReader.kt index b04d1ce..0b3a032 100644 --- a/app/src/main/java/com/aryan/reader/paginatedreader/PaginatedReader.kt +++ b/app/src/main/java/com/aryan/reader/paginatedreader/PaginatedReader.kt @@ -88,11 +88,11 @@ import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.composed -import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.toRect import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Brush 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.Path 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.clipPath import androidx.compose.ui.graphics.drawscope.clipRect @@ -1588,25 +1589,9 @@ internal fun PaginatedReaderContent( val styleModifier = alignModifier .then(if (block.style.horizontalAlign == "center") widthModifier else Modifier) - .then( - if (block.style.borderRadius > 0.dp) Modifier.clip(RoundedCornerShape(block.style.borderRadius)) - else Modifier - ) - .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 + .drawCssBorders( + blockStyle = block.style, + density = density ) val diagnosticModifier = Modifier @@ -1636,12 +1621,11 @@ internal fun PaginatedReaderContent( .then(styleModifier) 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 + start = block.style.padding.left.coerceAtLeast(0.dp) + (block.style.borderLeft?.width ?: 0.dp), + top = block.style.padding.top.coerceAtLeast(0.dp) + (block.style.borderTop?.width ?: 0.dp), + end = block.style.padding.right.coerceAtLeast(0.dp) + (block.style.borderRight?.width ?: 0.dp), + bottom = block.style.padding.bottom.coerceAtLeast(0.dp) + (block.style.borderBottom?.width ?: 0.dp) ).then( if (block.style.horizontalAlign != "center") widthModifier else Modifier.fillMaxWidth() ) @@ -2322,61 +2306,16 @@ internal fun PaginatedReaderContent( } is SpacerBlock -> { - val border = block.style.border - if (border != null && border.width > 0.dp) { - Box( - modifier = Modifier - .fillMaxWidth() - .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)) - } + Box( + modifier = Modifier + .fillMaxWidth() + .height(block.height) + .drawCssBorders(block.style, density) + ) } is TableBlock -> { - // Table-level margin/background/border/padding - // are already - // applied by the outer Box wrapper. Only set - // width here. - val tableModifier = paddingModifier - - Column(modifier = tableModifier) { + Column(modifier = paddingModifier) { block.rows.forEach { tableRow -> Row( Modifier @@ -2428,14 +2367,7 @@ internal fun PaginatedReaderContent( Modifier } ) - .then(cellStyle.border?.let { border -> - Modifier.border( - BorderStroke( - border.width, - border.color - ) - ) - } ?: Modifier) + .drawCssBorders(cellStyle, density) .padding( start = cellStyle.padding.left.coerceAtLeast( 0.dp @@ -2508,23 +2440,11 @@ internal fun PaginatedReaderContent( } 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( - modifier = spacerModifier + modifier = Modifier + .fillMaxWidth() + .height(blockInCell.height) + .drawCssBorders(blockInCell.style, density) ) } @@ -3311,17 +3231,12 @@ private fun RenderFlexChildBlock( } is SpacerBlock -> { - val border = childBlock.style.border - if (border != null && border.width > 0.dp) { - Box( - modifier = Modifier - .fillMaxWidth() - .height(border.width) - .background(border.color) - ) - } else { - Spacer(Modifier.height(childBlock.height)) - } + Box( + modifier = Modifier + .fillMaxWidth() + .height(childBlock.height) + .drawCssBorders(childBlock.style, density) + ) } is TableBlock -> { @@ -3353,11 +3268,7 @@ private fun RenderFlexChildBlock( ) else Modifier ) - .then(cellStyle.border?.let { - Modifier.border( - BorderStroke(it.width, it.color) - ) - } ?: Modifier) + .drawCssBorders(cellStyle, density) .padding( start = cellStyle.padding.left.coerceAtLeast( 0.dp @@ -3586,4 +3497,161 @@ private fun Modifier.realisticBookPage( 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) + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/aryan/reader/paginatedreader/PaginatedReaderData.kt b/app/src/main/java/com/aryan/reader/paginatedreader/PaginatedReaderData.kt index 7491f06..58afcb1 100644 --- a/app/src/main/java/com/aryan/reader/paginatedreader/PaginatedReaderData.kt +++ b/app/src/main/java/com/aryan/reader/paginatedreader/PaginatedReaderData.kt @@ -51,28 +51,34 @@ data class BlockStyle( @ProtoNumber(4) @Serializable(with = DpSerializer::class) val maxWidth: 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(7) val border: BorderStyle? = null, - @ProtoNumber(8) val listStyleType: String? = null, - @ProtoNumber(9) val listStyleImage: String? = null, - @ProtoNumber(10) val pageBreakInsideAvoid: Boolean = false, - @ProtoNumber(11) val pageBreakAfterAvoid: Boolean = false, - @ProtoNumber(12) val boxSizing: String? = null, - @ProtoNumber(13) val float: String? = null, - @ProtoNumber(14) val clear: String? = null, - @ProtoNumber(15) val position: String? = null, - @ProtoNumber(16) @Serializable(with = DpSerializer::class) val top: Dp = Dp.Unspecified, - @ProtoNumber(17) @Serializable(with = DpSerializer::class) val right: Dp = Dp.Unspecified, - @ProtoNumber(18) @Serializable(with = DpSerializer::class) val bottom: Dp = Dp.Unspecified, - @ProtoNumber(19) @Serializable(with = DpSerializer::class) val left: Dp = Dp.Unspecified, - @ProtoNumber(20) val display: String? = null, - @ProtoNumber(21) val flexDirection: String? = null, - @ProtoNumber(22) val justifyContent: String? = null, - @ProtoNumber(23) val alignItems: String? = null, - @ProtoNumber(24) val horizontalAlign: String? = null, - @ProtoNumber(25) val filter: String? = null, - @ProtoNumber(26) val borderCollapse: String? = null, - @ProtoNumber(27) @Serializable(with = DpSerializer::class) val borderSpacing: Dp = 0.dp, - @ProtoNumber(28) @Serializable(with = DpSerializer::class) val borderRadius: Dp = 0.dp + @ProtoNumber(7) val borderTop: BorderStyle? = null, + @ProtoNumber(8) val borderRight: BorderStyle? = null, + @ProtoNumber(9) val borderBottom: BorderStyle? = null, + @ProtoNumber(10) val borderLeft: BorderStyle? = null, + @ProtoNumber(11) val listStyleType: String? = null, + @ProtoNumber(12) val listStyleImage: String? = null, + @ProtoNumber(13) val pageBreakInsideAvoid: Boolean = false, + @ProtoNumber(14) val pageBreakAfterAvoid: Boolean = false, + @ProtoNumber(15) val boxSizing: String? = null, + @ProtoNumber(16) val float: String? = null, + @ProtoNumber(17) val clear: String? = null, + @ProtoNumber(18) val position: String? = null, + @ProtoNumber(19) @Serializable(with = DpSerializer::class) val top: Dp = Dp.Unspecified, + @ProtoNumber(20) @Serializable(with = DpSerializer::class) val right: Dp = Dp.Unspecified, + @ProtoNumber(21) @Serializable(with = DpSerializer::class) val bottom: Dp = Dp.Unspecified, + @ProtoNumber(22) @Serializable(with = DpSerializer::class) val left: Dp = Dp.Unspecified, + @ProtoNumber(23) val display: String? = null, + @ProtoNumber(24) val flexDirection: String? = null, + @ProtoNumber(25) val justifyContent: String? = null, + @ProtoNumber(26) val alignItems: String? = null, + @ProtoNumber(27) val horizontalAlign: String? = null, + @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 { return BlockStyle( @@ -92,7 +98,14 @@ data class BlockStyle( maxWidth = if (other.maxWidth != Dp.Unspecified) other.maxWidth else this.maxWidth, height = if (other.height != Dp.Unspecified) other.height else this.height, 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, listStyleImage = other.listStyleImage ?: this.listStyleImage, pageBreakInsideAvoid = this.pageBreakInsideAvoid || other.pageBreakInsideAvoid, @@ -112,8 +125,7 @@ data class BlockStyle( horizontalAlign = other.horizontalAlign ?: this.horizontalAlign, filter = other.filter ?: this.filter, borderCollapse = other.borderCollapse ?: this.borderCollapse, - borderSpacing = if (other.borderSpacing != 0.dp) other.borderSpacing else this.borderSpacing, - borderRadius = if (other.borderRadius != 0.dp) other.borderRadius else this.borderRadius + borderSpacing = if (other.borderSpacing != 0.dp) other.borderSpacing else this.borderSpacing ) } } diff --git a/app/src/main/java/com/aryan/reader/paginatedreader/Paginator.kt b/app/src/main/java/com/aryan/reader/paginatedreader/Paginator.kt index a34be24..b0bb99f 100644 --- a/app/src/main/java/com/aryan/reader/paginatedreader/Paginator.kt +++ b/app/src/main/java/com/aryan/reader/paginatedreader/Paginator.kt @@ -288,11 +288,11 @@ class SuspendingAndroidBlockMeasurementProvider( var splitRowIndex = -1 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() 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() Timber.tag("PAGINATION_DEBUG").d("SplitTable: avail=$availableHeight, topDec=$decorationTop, botDec=$decorationBottom") @@ -313,7 +313,8 @@ class SuspendingAndroidBlockMeasurementProvider( } val cellDecoration = with(density) { 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() maxRowHeight = maxOf(maxRowHeight, cellHeight + cellDecoration) } @@ -344,11 +345,11 @@ class SuspendingAndroidBlockMeasurementProvider( var splitChildIndex = -1 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() 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() currentHeight += decorationTop @@ -677,10 +678,9 @@ private suspend fun measureBlockHeight( with(density) { verticalPaddingPx = block.style.padding.top.toPx() + block.style.padding.bottom.toPx() horizontalPaddingPx = block.style.padding.left.toPx() + block.style.padding.right.toPx() - block.style.border?.let { - verticalBorderPx = it.width.toPx() * 2 - horizontalBorderPx = it.width.toPx() * 2 - } + + verticalBorderPx = (block.style.borderTop?.width?.toPx() ?: 0f) + (block.style.borderBottom?.width?.toPx() ?: 0f) + horizontalBorderPx = (block.style.borderLeft?.width?.toPx() ?: 0f) + (block.style.borderRight?.width?.toPx() ?: 0f) } val isBorderBox = block.style.boxSizing == "border-box" @@ -817,7 +817,8 @@ private suspend fun measureBlockHeight( var cellDecorationHeight = 0f with(density) { 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()) } @@ -1002,11 +1003,11 @@ private suspend fun splitParagraphBlock( if (text.isEmpty()) return null 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() 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() val availableTextHeight = availableHeight - decorationTop - decorationBottom