Zebra Stripes¶
This example shows how to build a ViewPlugin that applies alternating line decorations — a common pattern for line-level styling.
The approach¶
Use Decoration.line() with a SpanStyle background color, applied through a ViewPlugin that rebuilds decorations when the document changes.
Full implementation¶
// Dark-theme stripe color matching CodeMirror reference (#34474788)
private val stripe = Decoration.line(
LineDecorationSpec(style = SpanStyle(background = Color(0x88344747)))
)
private fun buildStripes(doc: Text, step: Int): DecorationSet {
val builder = RangeSetBuilder<Decoration>()
for (i in 1..doc.lines) {
if (i % step == 0) {
val line = doc.line(LineNumber(i))
builder.add(line.from, line.from, stripe)
}
}
return builder.finish()
}
private class ZebraPlugin(
session: EditorSession,
private val step: Int
) : PluginValue, DecorationSource {
override var decorations: DecorationSet = buildStripes(session.state.doc, step)
override fun update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged) {
decorations = buildStripes(update.state.doc, step)
}
}
}
private val zebraPlugin = ViewPlugin.fromDecorationSource { session ->
ZebraPlugin(session = session, step = 2)
}
Using it¶
val session = rememberEditorSession(
doc = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6",
extensions = basicSetup + zebraStripes
)
KodeMirror(session = session)
Even-numbered lines get a subtle gray background.
Configurable step¶
Make the stripe interval configurable using a Facet:
val stripeFacet = Facet.define(
combine = { values: List<Int> -> values.firstOrNull() ?: 2 }
)
fun zebraStripes(step: Int = 2): Extension =
stripeFacet.of(step) + ViewPlugin.fromClass(::ConfigurableZebraPlugin).asExtension()
class ConfigurableZebraPlugin(view: EditorSession) : PluginValue, DecorationSource {
private var step = view.state.facet(stripeFacet)
override var decorations: DecorationSet = computeDecorations(view)
private set
override fun update(update: ViewUpdate) {
val newStep = update.state.facet(stripeFacet)
if (update.docChanged || newStep != step) {
step = newStep
decorations = computeDecorations(update.view)
}
}
private fun computeDecorations(view: EditorSession): DecorationSet {
val builder = RangeSetBuilder<Decoration>()
val doc = view.state.doc
for (i in 1..doc.lines) {
if (i % step == 0) {
val line = doc.line(i)
builder.add(line.from, line.from, stripeDecoration)
}
}
return builder.finish()
}
}
Key concepts¶
Decoration.line()creates a line-level decoration (applied to the whole line, not a character range). In Kodemirror, useSpanStylefor styling instead of CSS classes.RangeSetBuilderbuilds a sorted range set. Ranges must be added in ascendingfromorder.ViewPlugin.fromClass()wraps a class that implements bothPluginValueandDecorationSource, automatically wiring up thedecorationsproperty.DecorationSourceis the Compose alternative to the upstreamdecorationsplugin spec option.
Based on the CodeMirror Zebra Stripes example.