From bab2b19ad82a118506fd184642cc112b76c97505 Mon Sep 17 00:00:00 2001 From: Daniel Freiling Date: Fri, 31 Oct 2025 14:59:31 +0100 Subject: [PATCH 1/3] feat: add experimental positioning for default EPUB decoration styles this will position decorations between text and background, allowing for opaque highlight colors --- .../navigator/epub/EpubNavigatorFragment.kt | 30 +++++++++++++++++-- .../navigator/html/HtmlDecorationTemplate.kt | 27 ++++++++++++++--- .../r2/testapp/reader/EpubReaderFragment.kt | 3 ++ 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFragment.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFragment.kt index 98184974c9..7dd00bcab0 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFragment.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFragment.kt @@ -8,6 +8,7 @@ package org.readium.r2.navigator.epub +import android.graphics.Color import android.graphics.PointF import android.graphics.RectF import android.os.Bundle @@ -20,6 +21,7 @@ import android.webkit.JavascriptInterface import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse import android.webkit.WebView +import androidx.annotation.ColorInt import androidx.collection.forEach import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.os.BundleCompat @@ -214,14 +216,17 @@ public class EpubNavigatorFragment internal constructor( public constructor( servedAssets: List = emptyList(), readiumCssRsProperties: RsProperties = RsProperties(), - decorationTemplates: HtmlDecorationTemplates = HtmlDecorationTemplates.defaultTemplates(), + decorationTemplates: HtmlDecorationTemplates? = null, disablePageTurnsWhileScrolling: Boolean = false, selectionActionModeCallback: ActionMode.Callback? = null, shouldApplyInsetsPadding: Boolean? = true, + experimentalDecorationPositioning: Boolean? = false, ) : this( servedAssets = servedAssets, readiumCssRsProperties = readiumCssRsProperties, - decorationTemplates = decorationTemplates, + decorationTemplates = decorationTemplates ?: HtmlDecorationTemplates.defaultTemplates( + experimentalPositioning = experimentalDecorationPositioning == true + ), disablePageTurnsWhileScrolling = disablePageTurnsWhileScrolling, selectionActionModeCallback = selectionActionModeCallback, shouldApplyInsetsPadding = shouldApplyInsetsPadding, @@ -259,6 +264,27 @@ public class EpubNavigatorFragment internal constructor( ) } + /** + * Update default decorations, to change styling and positioning. + */ + public fun updateDefaultDecorations( + @ColorInt defaultTint: Int = Color.YELLOW, + lineWeight: Int = 2, + cornerRadius: Int = 3, + alpha: Double = 0.3, + experimentalPositioning: Boolean = false, + ) { + val updatedDefaultTemplates = + HtmlDecorationTemplates.defaultTemplates( + defaultTint, + lineWeight, + cornerRadius, + alpha, + experimentalPositioning, + ) + decorationTemplates = decorationTemplates.copyWith(updatedDefaultTemplates) + } + public companion object { public operator fun invoke(builder: Configuration.() -> Unit): Configuration = Configuration().apply(builder) diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/html/HtmlDecorationTemplate.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/html/HtmlDecorationTemplate.kt index bb1bcce051..5da4b6d18c 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/html/HtmlDecorationTemplate.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/html/HtmlDecorationTemplate.kt @@ -94,13 +94,15 @@ public data class HtmlDecorationTemplate( lineWeight: Int, cornerRadius: Int, alpha: Double, + experimentalPositioning: Boolean = false, ): HtmlDecorationTemplate = createTemplate( asHighlight = true, defaultTint = defaultTint, lineWeight = lineWeight, cornerRadius = cornerRadius, - alpha = alpha + alpha = alpha, + experimentalPositioning = experimentalPositioning, ) /** Creates a new decoration template for the underline style. */ @@ -109,13 +111,15 @@ public data class HtmlDecorationTemplate( lineWeight: Int, cornerRadius: Int, alpha: Double, + experimentalPositioning: Boolean = false, ): HtmlDecorationTemplate = createTemplate( asHighlight = false, defaultTint = defaultTint, lineWeight = lineWeight, cornerRadius = cornerRadius, - alpha = alpha + alpha = alpha, + experimentalPositioning = experimentalPositioning, ) /** @@ -128,6 +132,7 @@ public data class HtmlDecorationTemplate( lineWeight: Int, cornerRadius: Int, alpha: Double, + experimentalPositioning: Boolean = false, ): HtmlDecorationTemplate { val className = createUniqueClassName(if (asHighlight) "highlight" else "underline") val padding = Padding(left = 1, right = 1) @@ -141,6 +146,9 @@ public data class HtmlDecorationTemplate( if (asHighlight || isActive) { append("background-color: ${tint.toCss(alpha = alpha)} !important;") } + if (experimentalPositioning) { + append("--decoration-z-index: -1;") + } if (!asHighlight || isActive) { append("--underline-color: ${tint.toCss()};") } @@ -154,6 +162,7 @@ public data class HtmlDecorationTemplate( border-radius: ${cornerRadius}px; box-sizing: border-box; border: 0 solid var(--underline-color); + z-index: var(--decoration-z-index); } /* Horizontal (default) */ @@ -201,6 +210,13 @@ public class HtmlDecorationTemplates private constructor( public fun copy(): HtmlDecorationTemplates = HtmlDecorationTemplates(styles.toMutableMap()) + public fun copyWith(other: HtmlDecorationTemplates): HtmlDecorationTemplates = + copy().apply { + other.styles.forEach { (key, value) -> + styles[key] = value + } + } + public companion object { public operator fun invoke(build: HtmlDecorationTemplates.() -> Unit): HtmlDecorationTemplates = HtmlDecorationTemplates().apply(build) @@ -213,6 +229,7 @@ public class HtmlDecorationTemplates private constructor( lineWeight: Int = 2, cornerRadius: Int = 3, alpha: Double = 0.3, + experimentalPositioning: Boolean = false, ): HtmlDecorationTemplates = HtmlDecorationTemplates { set( Style.Highlight::class, @@ -220,7 +237,8 @@ public class HtmlDecorationTemplates private constructor( defaultTint = defaultTint, lineWeight = lineWeight, cornerRadius = cornerRadius, - alpha = alpha + alpha = alpha, + experimentalPositioning = experimentalPositioning, ) ) set( @@ -229,7 +247,8 @@ public class HtmlDecorationTemplates private constructor( defaultTint = defaultTint, lineWeight = lineWeight, cornerRadius = cornerRadius, - alpha = alpha + alpha = alpha, + experimentalPositioning = experimentalPositioning, ) ) } diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/EpubReaderFragment.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/EpubReaderFragment.kt index d61850e0e2..6b4d5ff91d 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/EpubReaderFragment.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/EpubReaderFragment.kt @@ -87,6 +87,9 @@ class EpubReaderFragment : VisualReaderFragment() { decorationTemplates[DecorationStyleAnnotationMark::class] = annotationMarkTemplate() decorationTemplates[DecorationStylePageNumber::class] = pageNumberTemplate() + // Update default decorations to be opaque and use experimental positioning. + updateDefaultDecorations(experimentalPositioning = true, alpha = 1.0) + // Declare a custom font family for reflowable EPUBs. addFontFamilyDeclaration(FontFamily.LITERATA) { addFontFace { From affc4d09b38737d5e5c316167bbe1b7fca2902eb Mon Sep 17 00:00:00 2001 From: Daniel Freiling Date: Fri, 31 Oct 2025 15:10:27 +0100 Subject: [PATCH 2/3] chore: fix lint --- .../navigator/epub/EpubNavigatorFragment.kt | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFragment.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFragment.kt index 7dd00bcab0..dcd12e56ad 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFragment.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFragment.kt @@ -274,16 +274,16 @@ public class EpubNavigatorFragment internal constructor( alpha: Double = 0.3, experimentalPositioning: Boolean = false, ) { - val updatedDefaultTemplates = - HtmlDecorationTemplates.defaultTemplates( - defaultTint, - lineWeight, - cornerRadius, - alpha, - experimentalPositioning, - ) - decorationTemplates = decorationTemplates.copyWith(updatedDefaultTemplates) - } + val updatedDefaultTemplates = + HtmlDecorationTemplates.defaultTemplates( + defaultTint, + lineWeight, + cornerRadius, + alpha, + experimentalPositioning, + ) + decorationTemplates = decorationTemplates.copyWith(updatedDefaultTemplates) + } public companion object { public operator fun invoke(builder: Configuration.() -> Unit): Configuration = From 532d167a061cece583e796e25e08b33264aeb20c Mon Sep 17 00:00:00 2001 From: Daniel Freiling Date: Thu, 6 Nov 2025 13:38:57 +0100 Subject: [PATCH 3/3] refactor: simplify implementation --- CHANGELOG.md | 10 ++++++- .../navigator/epub/EpubNavigatorFragment.kt | 30 ++----------------- .../navigator/html/HtmlDecorationTemplate.kt | 7 ----- .../r2/testapp/reader/EpubReaderFragment.kt | 11 +++++-- 4 files changed, 19 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e79be5826d..306e0d6651 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,15 @@ All notable changes to this project will be documented in this file. Take a look **Warning:** Features marked as *experimental* may change or be removed in a future release without notice. Use with caution. - +## [Unreleased] + +### Added + +#### Navigator + +* New experimental positioning of EPUB decorations that places highlights behind text to improve legibility with opaque decorations (contributed by [@ddfreiling](https://github.com/readium/kotlin-toolkit/pull/721)). + * To opt-in, initialize the `EpubNavigatorFragment.Configuration` object with `decorationTemplates = HtmlDecorationTemplates.defaultTemplates(alpha = 1.0, experimentalPositioning = true)`. + ## [3.1.2] diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFragment.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFragment.kt index dcd12e56ad..98184974c9 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFragment.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFragment.kt @@ -8,7 +8,6 @@ package org.readium.r2.navigator.epub -import android.graphics.Color import android.graphics.PointF import android.graphics.RectF import android.os.Bundle @@ -21,7 +20,6 @@ import android.webkit.JavascriptInterface import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse import android.webkit.WebView -import androidx.annotation.ColorInt import androidx.collection.forEach import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.os.BundleCompat @@ -216,17 +214,14 @@ public class EpubNavigatorFragment internal constructor( public constructor( servedAssets: List = emptyList(), readiumCssRsProperties: RsProperties = RsProperties(), - decorationTemplates: HtmlDecorationTemplates? = null, + decorationTemplates: HtmlDecorationTemplates = HtmlDecorationTemplates.defaultTemplates(), disablePageTurnsWhileScrolling: Boolean = false, selectionActionModeCallback: ActionMode.Callback? = null, shouldApplyInsetsPadding: Boolean? = true, - experimentalDecorationPositioning: Boolean? = false, ) : this( servedAssets = servedAssets, readiumCssRsProperties = readiumCssRsProperties, - decorationTemplates = decorationTemplates ?: HtmlDecorationTemplates.defaultTemplates( - experimentalPositioning = experimentalDecorationPositioning == true - ), + decorationTemplates = decorationTemplates, disablePageTurnsWhileScrolling = disablePageTurnsWhileScrolling, selectionActionModeCallback = selectionActionModeCallback, shouldApplyInsetsPadding = shouldApplyInsetsPadding, @@ -264,27 +259,6 @@ public class EpubNavigatorFragment internal constructor( ) } - /** - * Update default decorations, to change styling and positioning. - */ - public fun updateDefaultDecorations( - @ColorInt defaultTint: Int = Color.YELLOW, - lineWeight: Int = 2, - cornerRadius: Int = 3, - alpha: Double = 0.3, - experimentalPositioning: Boolean = false, - ) { - val updatedDefaultTemplates = - HtmlDecorationTemplates.defaultTemplates( - defaultTint, - lineWeight, - cornerRadius, - alpha, - experimentalPositioning, - ) - decorationTemplates = decorationTemplates.copyWith(updatedDefaultTemplates) - } - public companion object { public operator fun invoke(builder: Configuration.() -> Unit): Configuration = Configuration().apply(builder) diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/html/HtmlDecorationTemplate.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/html/HtmlDecorationTemplate.kt index 5da4b6d18c..9d4a4d7f8a 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/html/HtmlDecorationTemplate.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/html/HtmlDecorationTemplate.kt @@ -210,13 +210,6 @@ public class HtmlDecorationTemplates private constructor( public fun copy(): HtmlDecorationTemplates = HtmlDecorationTemplates(styles.toMutableMap()) - public fun copyWith(other: HtmlDecorationTemplates): HtmlDecorationTemplates = - copy().apply { - other.styles.forEach { (key, value) -> - styles[key] = value - } - } - public companion object { public operator fun invoke(build: HtmlDecorationTemplates.() -> Unit): HtmlDecorationTemplates = HtmlDecorationTemplates().apply(build) diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/EpubReaderFragment.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/EpubReaderFragment.kt index 6b4d5ff91d..72c8ccdec1 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/EpubReaderFragment.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/EpubReaderFragment.kt @@ -29,6 +29,7 @@ import org.readium.r2.navigator.Decoration import org.readium.r2.navigator.epub.* import org.readium.r2.navigator.epub.css.FontStyle import org.readium.r2.navigator.html.HtmlDecorationTemplate +import org.readium.r2.navigator.html.HtmlDecorationTemplates import org.readium.r2.navigator.html.toCss import org.readium.r2.navigator.preferences.FontFamily import org.readium.r2.shared.ExperimentalReadiumApi @@ -83,13 +84,17 @@ class EpubReaderFragment : VisualReaderFragment() { "annotation-icon.svg" ) + // Enable experimental decorations positioning that places highlights behind + // text to improve legibility with opaque decorations. + decorationTemplates = HtmlDecorationTemplates.defaultTemplates( + alpha = 1.0, + experimentalPositioning = true + ) + // Register the HTML templates for our custom decoration styles. decorationTemplates[DecorationStyleAnnotationMark::class] = annotationMarkTemplate() decorationTemplates[DecorationStylePageNumber::class] = pageNumberTemplate() - // Update default decorations to be opaque and use experimental positioning. - updateDefaultDecorations(experimentalPositioning = true, alpha = 1.0) - // Declare a custom font family for reflowable EPUBs. addFontFamilyDeclaration(FontFamily.LITERATA) { addFontFace {