Pantrix UI

Code Block

Code set out to be read: in the theme's code type, a line to a line, each run of it in the colour of what it is, and ready to be copied.

import com.pantrixui.components.data.codeblock.PantrixCodeBlock

Examples

A snippet of code

At its plainest: a text of code, the tokens that colour it, and the language's name. The block knows no language; tokens says what is a keyword, what a string and what a comment. Here the app's own reader of Kotlin gives them.

Keywords are in the colour of the theme's primary role, types in info, strings in success, numbers in warning, and names that start with @ in danger; a comment is written in textSecondary, and slanted. The button in the header copies all of the code and shows a tick for two seconds.

PantrixCodeBlock(
    code = GREETING,
    tokens = remember { kotlinTokens(GREETING) },
    language = "Kotlin",
    tagKey = "snippet"
)

private val GREETING =
    """
    @Composable
    fun Greeting(name: String) {
        // The greeting a new account is met with.
        val unread = 3
        PantrixText(text = "Hello, " + name + ": " + unread + " new messages")
    }
    """.trimIndent()

A file, in a height of its own

A file: title gives its name, icon the icon before it, lineNumbers the line numbers. The block was given a height: the code scrolls in it, under a header that stays, and only the lines on the screen are built.

A line that does not fit is not broken. Scroll the code sideways: the line numbers stay where they are, and the lines pass under them. On a screen with a pointer both of the block's scroll bars show, and a mouse drags them; once Tab has come to the lines, the arrow keys scroll the code.

PantrixCodeBlock(
    code = REPOSITORY,
    modifier = Modifier.height(220.dp),
    tokens = remember { kotlinTokens(REPOSITORY) },
    title = "OrderRepository.kt",
    language = "Kotlin",
    icon = PantrixIcons.Folder,
    lineNumbers = true,
    tagKey = "file"
)

private val REPOSITORY =
    """
    package com.example.orders

    import kotlinx.coroutines.flow.Flow
    import kotlinx.coroutines.flow.map

    /** Reads the orders of an account, newest first. */
    class OrderRepository(private val api: OrderApi, private val cache: OrderCache, private val clock: Clock) {
        fun orders(account: String): Flow<List<Order>> = cache.observe(account).map { it.sortedBy(Order::placedAt) }

        suspend fun refresh(account: String) {
            val fresh = api.orders(account, since = cache.newest(account))
            cache.put(account, fresh, at = clock.now())
        }

        suspend fun cancel(order: Order): Boolean {
            if (order.shipped) return false
            api.cancel(order.id)
            cache.remove(order.id)
            return true
        }
    }
    """.trimIndent()

A finding, under the line it is about

A review: the finding stands under the line it is about. In the form that takes lines each line carries its own mark; codeLines gives the lines of the text, and the screen marks the ones it wants. A mark draws the soft fill of its tone's role behind the line and a bar in the role's colour down its left edge; message is written under the line, after the tone's icon, and source stands at its end.

A marked line is written in the one colour the theme pairs with that fill. firstLineNumber gives where the part started in its file: the finding's line carries its number in the file.

val lines = remember {
    codeLines(REVIEWED, kotlinTokens(REVIEWED)).mapIndexed { index, line ->
        when (index) {
            1 -> {
                line.copy(
                    mark = CodeMark(
                        tone = CodeTone.Danger,
                        message = "The token may be null here; `!!` brings the app down.",
                        source = "kotlin:S2259"
                    )
                )
            }

            4 -> {
                line.copy(mark = CodeMark(CodeTone.Warning, message = "The length of time belongs in a named constant."))
            }

            else -> {
                line
            }
        }
    }
}
PantrixCodeBlock(
    lines = lines,
    title = "AuthRepository.kt",
    lineNumbers = true,
    firstLineNumber = 41,
    tagKey = "finding"
)

private val REVIEWED =
    """
    suspend fun token(): String {
        val cached = cache.token!!
        if (cached.isFresh()) return cached.value
        val fresh = api.refresh(cached.refreshToken)
        cache.keep(fresh, seconds = 3600)
        return fresh.value
    }
    """.trimIndent()

Lines added and taken out

A mark without a message is a band and nothing more: in the difference between two versions the added lines are in the Success tone and the ones taken out in Danger. Here the + and the - at the start of a line say what the band means too: a colour says nothing to someone who cannot see it.

val lines = remember {
    codeLines(CHANGES, kotlinTokens(CHANGES)).map { line ->
        when {
            line.text.startsWith("+") -> line.copy(mark = CodeMark(CodeTone.Success))
            line.text.startsWith("-") -> line.copy(mark = CodeMark(CodeTone.Danger))
            else -> line
        }
    }
}
PantrixCodeBlock(lines = lines, title = "Greeting.kt", tagKey = "changes")

private val CHANGES =
    """
      @Composable
      fun Greeting(name: String) {
    -     PantrixText(text = "Hello")
    +     PantrixText(text = "Hello, " + name)
    +     PantrixDivider()
      }
    """.trimIndent()

The steps of a guide

Marks are the screen's data: when the step of a guide changes, other lines stand out. Here each step marks its own lines in the Primary tone; change the step, and the band moves to the other lines.

var step by remember { mutableIntStateOf(0) }
val lines = remember(step) {
    codeLines(GREETING, kotlinTokens(GREETING)).mapIndexed { index, line ->
        if (index in STEP_LINES[step]) line.copy(mark = CodeMark(CodeTone.Primary)) else line
    }
}
PantrixSegmentButton(
    options = STEPS,
    selected = step,
    onSelect = { step = it },
    fullWidth = false,
    tagKey = "step"
)
PantrixCodeBlock(lines = lines, lineNumbers = true, copyable = false, tagKey = "steps")

private val STEPS = listOf(
    SegmentOption(value = 0, label = "1. Signature", tagKey = "signature"),
    SegmentOption(value = 1, label = "2. State", tagKey = "state"),
    SegmentOption(value = 2, label = "3. Drawing", tagKey = "drawing")
)

private val STEP_LINES = listOf(0..1, 3..3, 4..4)

private val GREETING =
    """
    @Composable
    fun Greeting(name: String) {
        // The greeting a new account is met with.
        val unread = 3
        PantrixText(text = "Hello, " + name + ": " + unread + " new messages")
    }
    """.trimIndent()

Lines that go on under themselves

A long command. With softWrap the line goes on under itself where it does not fit the block, and the block does not scroll sideways; without it the command is one line, and the code scrolls sideways. Turn the switch and compare the two.

A line that goes on under itself loses its indent: softWrap suits a command or a line of a log, not code whose indent carries meaning.

var wraps by remember { mutableStateOf(true) }
PantrixSwitchButton(
    checked = wraps,
    onCheckedChange = { wraps = it },
    label = "Let a long line go on under itself",
    tagKey = "code_wraps"
)
PantrixCodeBlock(code = COMMAND, softWrap = wraps, language = "Shell", tagKey = "command")

private val COMMAND =
    "./gradlew :ui:components:jvmTest --tests 'com.pantrixui.components.CodeBlockTest' --offline --console=plain"

Copying, and the end of the header

onCopy is called after the button was pressed, with whether the code got to the clipboard: a screen that says so in its own way listens here. trailing puts something of the screen's own at the end of the header, before the button that copies: here, the number of lines.

In the second block copyable and selectable are off and nothing else stands in the header: it has no header, and its code is not selected.

val report = LocalExampleEvents.current
PantrixCodeBlock(
    code = DEPENDENCIES,
    title = "build.gradle.kts",
    onCopy = { copied -> report(if (copied) "Code copied to the clipboard" else "The code could not be copied") },
    trailing = { PantrixText(text = "4 lines", style = PantrixTheme.typography.caption) },
    tagKey = "copied"
)
PantrixCodeBlock(code = DEPENDENCIES, selectable = false, copyable = false, tagKey = "plain")

private val DEPENDENCIES =
    """
    dependencies {
        implementation("com.pantrixui:foundation:0.1.0")
        implementation("com.pantrixui:components:0.1.0")
    }
    """.trimIndent()

Tabs in the header

leading puts the screen's own thing at the header's start, before the icon and the title: here two tabs that choose between two forms of the same line, as a documentation's setup step does. They are written in the header's text colour.

textStyle sets the code in another style than the theme's code: here at the size of caption, in the code's family, for a narrow column. The style's headerLineWidth is the width of the line under the header; headerHorizontalPadding keeps what the header holds nearer the side than the lines are.

var form by remember { mutableIntStateOf(0) }
val code = FORMS[form].second
val type = PantrixTheme.typography
PantrixCodeBlock(
    code = code,
    tokens = remember(code) { kotlinTokens(code) },
    style = CodeBlockStyle(headerLineWidth = 2.dp, headerHorizontalPadding = PantrixTheme.spacing.xs),
    leading = {
        FORMS.forEachIndexed { index, (name, _) ->
            PantrixButton(
                text = name,
                onClick = { form = index },
                variant = if (index == form) ButtonVariant.Soft else ButtonVariant.Text,
                size = ButtonSize.Small,
                tagKey = "form_$index"
            )
        }
    },
    textStyle = type.caption.copy(fontFamily = type.code.fontFamily),
    tagKey = "tabs"
)

private val FORMS = listOf(
    "Kotlin" to """implementation("com.pantrixui:components:0.1.0")""",
    "Groovy" to """implementation 'com.pantrixui:components:0.1.0'"""
)

In a card, in colours of its own

The block has a fill and an edge of its own: the fill is the theme's surface, the edge the theme's divider. In a card that has both, colors makes both transparent. A colour that is given is used, and one that is not stays the theme's.

style changes the block's geometry; only the fields it is given change. Here horizontalPadding is zero, so that the lines start where the card's own padding ends.

PantrixCard(modifier = Modifier.fillMaxWidth(), variant = CardVariant.Outlined) {
    PantrixText(text = "Setup", style = PantrixTheme.typography.title)
    PantrixCodeBlock(
        code = DEPENDENCIES,
        tokens = remember { kotlinTokens(DEPENDENCIES) },
        style = CodeBlockStyle(horizontalPadding = 0.dp),
        colors = CodeBlockColors(container = Color.Transparent, border = Color.Transparent),
        tagKey = "in_a_card"
    )
}

private val DEPENDENCIES =
    """
    dependencies {
        implementation("com.pantrixui:foundation:0.1.0")
        implementation("com.pantrixui:components:0.1.0")
    }
    """.trimIndent()

Tokens written by hand

The block carries no reader of a language; the app gives the tokens. For a short text with no reader the tokens can be written by hand: each CodeToken is what a run of the line is (CodeTokenKind), from start up to end. Here a shell command's lines are built as CodeLines directly: the comment Comment, the command Keyword, the task StringLiteral, the option Type.

PantrixCodeBlock(lines = SHELL_LINES, language = "Shell", tagKey = "hand_tokens")

private val SHELL_LINES = listOf(
    CodeLine(text = "# Run the desktop demo", tokens = listOf(CodeToken(0, 28, CodeTokenKind.Comment))),
    CodeLine(
        text = "./gradlew :app:desktopApp:run --offline",
        tokens = listOf(
            CodeToken(0, 9, CodeTokenKind.Keyword),
            CodeToken(10, 29, CodeTokenKind.StringLiteral),
            CodeToken(30, 39, CodeTokenKind.Type)
        )
    )
)

API

code

Code set out to be read: in the theme's code type, a line to a line, each run of it in the colour of what it is, and ready to be copied.

ParameterTypeDefaultWhat it is
codeStringthe code. Its lines end at a line feed, at a carriage return, or at the two together, and the break that ends the text starts no line.
modifierModifierModifierthe block's modifier. A height given here is the room the code scrolls in.
tokensList<CodeToken>emptyList()the runs of code that have a colour, placed by the letters of the whole text. None by default.
titleString?nullwhat the header says first: a file's name, or what the code is. A title too long for the header is cut short.
languageString?nullthe language's name, written after the title in the header, as it is to be read: Kotlin, TS. The block keeps no list of languages.
iconImageVector?nullan icon before the title.
lineNumbersBooleanfalsewhether each line carries its number before the code.
firstLineNumberInt1the number of the first line. A part taken out of a file starts where it started there.
softWrapBooleanfalsewhether a line too long for the block goes on under itself. By default no line does, and the code scrolls sideways.
selectableBooleantruewhether the code can be selected, by a pointer drawn over it or a finger held on it, and copied by the system's own means. What is copied so is what the lines on the screen show, without their numbers and without what is said about them. Code that scrolls in its own height has built the lines on its screen and no others, and "select all" there selects those. The button copies all of it.
copyableBooleantruewhether the block has the button that copies the code.
onCopy((copied: Boolean) -> Unit)?nullcalled after that button was pressed, with whether the code got to the clipboard: a screen that says so in its own way listens here. The button itself shows a tick for a moment and tells a screen reader, unless the system shows every copy itself (PantrixClipboard.traits): there nothing more is needed.
trailing(@Composable () -> Unit)?nullwhat the screen puts at the header's end, before the button that copies: a count, a button of its own. It is drawn in the block's text colour.
styleCodeBlockStyle?nullthis call's own style: over every enclosing one and under the density. Only the fields it sets change, on every platform.
colorsCodeBlockColors?nullthis block's own colours, where they are not the theme's: the ones set here, with the theme's for the rest.
tagKeyString?nullthe block's automation key: the block is CodeBlock_<tagKey>, a line of it CodeBlock_<tagKey>.Line_<number>, the message under a line CodeBlock_<tagKey>.Note_<number>, the lines' window where a keyboard scrolls it CodeBlock_<tagKey>.Lines_window, and the button that copies CodeBlock_<tagKey>.Button_copy. A press on that button is reported as a CodeBlock copy event with whether the code got there. Nothing of the code is reported.
leading(@Composable RowScope.() -> Unit)?nullwhat the screen puts at the header's start, before the icon and the title, written in the header's colour: the tabs of one file in two forms, as a page of documentation shows a setup for two repositories.
textStyleTextStyle?nullwhat the code is set in: the theme's code where it is not given, a smaller one on a phone's page.

lines

PantrixCodeBlock given its lines one by one, each with what is said about it: a band behind a line that is to be looked at, the lines that were added and taken out, a finding with its message under the line it is about.

ParameterTypeDefaultWhat it is
linesList<CodeLine>the lines, in their order. The button copies their texts, a line to a line.
modifierModifierModifierthe block's modifier. A height given here is the room the code scrolls in.
titleString?nullwhat the header says first: a file's name, or what the code is.
languageString?nullthe language's name, written after the title in the header, as it is to be read.
iconImageVector?nullan icon before the title.
lineNumbersBooleanfalsewhether each line carries its number before the code.
firstLineNumberInt1the number of the first line. A part taken out of a file starts where it started there.
softWrapBooleanfalsewhether a line too long for the block goes on under itself.
selectableBooleantruewhether the code can be selected and copied by the system's own means.
copyableBooleantruewhether the block has the button that copies the code.
onCopy((copied: Boolean) -> Unit)?nullcalled after that button was pressed, with whether the code got to the clipboard.
trailing(@Composable () -> Unit)?nullwhat the screen puts at the header's end, before the button that copies.
styleCodeBlockStyle?nullthis call's own style: over every enclosing one and under the density. Only the fields it sets change, on every platform.
colorsCodeBlockColors?nullthis block's own colours, where they are not the theme's: the ones set here, with the theme's for the rest.
tagKeyString?nullthe block's automation key, as the other overload has it.
leading(@Composable RowScope.() -> Unit)?nullwhat the screen puts at the header's start, before the icon and the title: the tabs of a file in two forms, written in the header's colour.
textStyleTextStyle?nullwhat the code is set in: the theme's code where it is not given.

CodeBlockStyle

PantrixCodeBlock's geometry: the room around its lines, its header, the room before the code where the lines are numbered, and the edge.

ParameterTypeDefaultWhat it is
shapeShapeSpecShapeSpec.UnspecifiedThe corners of the block: a step of the brand's shape scale, or a fixed shape.
horizontalPaddingDpDp.UnspecifiedThe room between the block's sides and what it holds: the header's words, the lines, a message under a line. A mark's band is drawn across it, to the sides.
verticalPaddingDpDp.UnspecifiedThe room over the first line and under the last, and under a message. A message has half of it over its words.
linePaddingDpDp.UnspecifiedThe room over and under the words of each line.
headerHeightDpDp.UnspecifiedThe least the header is tall. It grows with what stands in it. A block that has nothing in its header but the button that copies has no header: the button's strip is as tall as the button.
gutterGapDpDp.UnspecifiedBetween the line numbers and the code, where the lines are numbered.
iconSizeDpDp.UnspecifiedThe width and height of the header's icon, of the icon of the button that copies, and of the one before a message.
borderWidthDpDp.UnspecifiedThe width of the edge around the block, and of the line under its header where headerLineWidth is not set. Zero draws none.
barWidthDpDp.UnspecifiedThe width of the bar down the left edge of a line with a mark. Zero draws none.
headerLineWidthDpDp.UnspecifiedThe width of the line under the header: borderWidth where it is not set, so that a block without an edge can still have its header ruled off. Zero draws none.
headerHorizontalPaddingDpDp.UnspecifiedThe room between the block's sides and what its header holds: horizontalPadding where it is not set. Tabs at the header's start stand nearer the side than the lines do.

CodeBlockColors

What a PantrixCodeBlock is painted with, for a block that does not take the theme's colours: a colour that is set is used, and one that is left out stays the theme's.

ParameterTypeDefaultWhat it is
containerColorColor.UnspecifiedThe fill behind the header and the lines: the theme's surface. Color.Transparent leaves what is behind the block to show.
textColorColor.UnspecifiedWhat no token colours, and the header's title. The theme's textPrimary.
commentColorColor.UnspecifiedA comment, which is slanted too. The theme's textSecondary.
keywordColorColor.UnspecifiedA word of the language. The theme's primary role.
typeColorColor.UnspecifiedThe name of a type. The theme's info role.
stringColorColor.UnspecifiedA string. The theme's success role.
numberColorColor.UnspecifiedA number. The theme's warning role.
punctuationColorColor.UnspecifiedBrackets, commas and semicolons. The theme's textSecondary.
annotationColorColor.UnspecifiedA name that says something about what follows it. The theme's danger role.
lineNumberColorColor.UnspecifiedThe line numbers, the header's language and its icon, the icon of the button that copies, and the scroll bars. The theme's textSecondary.
borderColorColor.UnspecifiedThe edge around the block and the line under its header. The theme's divider on the theme's own grounds, and on a fill, where that line would not read, a share of what is written there.

Accessibility