Pantrix UI

Segment Button

A segment button: a small set of choices side by side in one control, one of them chosen.

import com.pantrixui.components.elements.segmentbutton.PantrixSegmentButton

Examples

Segments that share the width equally

At its plainest: the options, the selected value and onSelect. The control spans the width it is given and the three segments share it equally (fullWidth, the default). The choice is applied at once: the line under the control changes with what is chosen.

The pill slides under the segment that is selected, and that segment's label is written in the primary colour. With a keyboard the control is one Tab stop; the arrow keys move the selection to the next segment or to the previous one.

var sort by rememberSaveable { mutableStateOf("newest") }
PantrixSegmentButton(
    options = SORTS,
    selected = sort,
    onSelect = { sort = it },
    modifier = Modifier.widthIn(max = 480.dp),
    tagKey = "sort"
)
PantrixText(
    text = "Sort order: ${SORTS.first { it.value == sort }.label}",
    style = PantrixTheme.typography.footnote,
    color = PantrixTheme.colors.textSecondary,
    tagKey = "sort_result"
)

private val SORTS = listOf(
    SegmentOption("newest", "Newest", tagKey = "newest"),
    SegmentOption("cheapest", "Cheapest", tagKey = "cheapest"),
    SegmentOption("nearest", "Nearest", tagKey = "nearest")
)

As wide as its labels

fullWidth = false makes each segment as wide as its label: in a toolbar or beside a heading the control takes only the room it needs. If there is not room for them all, the short labels stay whole and the long ones share what is left.

Both controls are like this: a time span with four options and a choice of platform with three.

var span by rememberSaveable { mutableStateOf("week") }
var platform by rememberSaveable { mutableStateOf("web") }
PantrixSegmentButton(
    options = SPANS,
    selected = span,
    onSelect = { span = it },
    fullWidth = false,
    tagKey = "span"
)
PantrixText(
    text = "Showing: the last ${SPANS.first { it.value == span }.label.lowercase()}",
    style = PantrixTheme.typography.footnote,
    color = PantrixTheme.colors.textSecondary,
    tagKey = "span_result"
)
PantrixSegmentButton(
    options = PLATFORMS,
    selected = platform,
    onSelect = { platform = it },
    fullWidth = false,
    tagKey = "platform"
)

private val SPANS = listOf(
    SegmentOption("day", "Day", tagKey = "day"),
    SegmentOption("week", "Week", tagKey = "week"),
    SegmentOption("month", "Month", tagKey = "month"),
    SegmentOption("year", "Year", tagKey = "year")
)

private val PLATFORMS = listOf(
    SegmentOption("android", "Android", tagKey = "android"),
    SegmentOption("ios", "iOS", tagKey = "ios"),
    SegmentOption("web", "Web", tagKey = "web")
)

Icons and a segment that cannot be selected

SegmentOption(icon = …) puts an icon before the label: the icon is in the label's colour, and a screen reader passes over it.

"Folders" is not open yet (enabled = false): it is written in the disabled colour, a press on it does nothing and the arrow keys skip it.

var view by rememberSaveable { mutableStateOf("list") }
PantrixSegmentButton(
    options = listOf(
        SegmentOption("list", "List", icon = PantrixIcons.Menu, tagKey = "list"),
        SegmentOption("search", "Search", icon = PantrixIcons.Search, tagKey = "search"),
        SegmentOption("folders", "Folders", icon = PantrixIcons.Folder, enabled = false, tagKey = "folders")
    ),
    selected = view,
    onSelect = { view = it },
    modifier = Modifier.widthIn(max = 480.dp),
    tagKey = "view"
)

With none selected

If selected is not the value of any option, no segment is selected and no pill is drawn; null is such a value too. It is used where an answer must not be chosen in advance.

Press one: the pill appears there. From then on the selection slides between the two segments; the control does not clear the selection by itself.

var helpful by rememberSaveable { mutableStateOf<String?>(null) }
PantrixText(text = if (helpful == null) "Was this page helpful?" else "Your answer was saved")
PantrixSegmentButton(
    options = listOf(
        SegmentOption("yes", "Yes", tagKey = "yes"),
        SegmentOption("no", "No", tagKey = "no")
    ),
    selected = helpful,
    onSelect = { helpful = it },
    fullWidth = false,
    tagKey = "helpful"
)

Its own colours

colors changes only the fields that are given: here the pill is in the app's own purple (pill), and the label on it in the colour that reads on that purple (selectedContent); the track and the unselected labels stay in the theme's colour.

The app's purple is in none of the theme's roles: the colour that reads on such a pill is the caller's to give too. A disabled control's track and a segment that cannot be selected are drawn in the theme's colours, even when colours are given.

val own = ownColors()
var span by rememberSaveable { mutableStateOf("week") }
PantrixSegmentButton(
    options = SPANS,
    selected = span,
    onSelect = { span = it },
    modifier = Modifier.widthIn(max = 480.dp),
    colors = SegmentButtonColors(pill = own.color, selectedContent = own.onColor),
    tagKey = "span_own"
)

private val SPANS = listOf(
    SegmentOption("day", "Day", tagKey = "day"),
    SegmentOption("week", "Week", tagKey = "week"),
    SegmentOption("month", "Month", tagKey = "month"),
    SegmentOption("year", "Year", tagKey = "year")
)

Disabled

enabled = false disables every segment: the track and the labels are drawn in the disabled colours, and the control takes no press and no focus. The pill stays where it is: which option is selected can still be read.

This control's options are the same as those of the first example. The segments are named inside the control's test tag, so they do not clash: the one there is SegmentButton_sort.Segment_newest, and the one here SegmentButton_locked.Segment_newest.

PantrixSegmentButton(
    options = SORTS,
    selected = "newest",
    onSelect = {},
    modifier = Modifier.widthIn(max = 480.dp),
    enabled = false,
    tagKey = "locked"
)

private val SORTS = listOf(
    SegmentOption("newest", "Newest", tagKey = "newest"),
    SegmentOption("cheapest", "Cheapest", tagKey = "cheapest"),
    SegmentOption("nearest", "Nearest", tagKey = "nearest")
)

Style

style changes the geometry of a single control: the track's shape (shape) and its height (height), the room between the track's edge and the pill (trackPadding), and the room on each side of a label (segmentPadding). Only the fields that are given change; the rest comes from the theme's and the platform's style.

The pill has no shape of its own: it is the track's shape, with each corner made smaller by trackPadding. That way the corners of the two curve around the same centre.

var size by rememberSaveable { mutableStateOf("m") }
PantrixSegmentButton(
    options = listOf(
        SegmentOption("s", "Small", tagKey = "s"),
        SegmentOption("m", "Medium", tagKey = "m"),
        SegmentOption("l", "Large", tagKey = "l")
    ),
    selected = size,
    onSelect = { size = it },
    fullWidth = false,
    style = SegmentButtonStyle(
        shape = ShapeRole.Small,
        height = 48.dp,
        trackPadding = 6.dp,
        segmentPadding = 20.dp
    ),
    tagKey = "size"
)

In a type of its own

In a narrow column, in a type of its own: three options fit in the theme's footnote type (labelStyle). The colour is the segment's: the chosen one's the primary colour, the others' quieter.

var mode by rememberSaveable { mutableStateOf("auto") }
PantrixSegmentButton(
    options = listOf(
        SegmentOption("light", "Light", tagKey = "light"),
        SegmentOption("dark", "Dark", tagKey = "dark"),
        SegmentOption("auto", "System", tagKey = "auto")
    ),
    selected = mode,
    onSelect = { mode = it },
    modifier = Modifier.widthIn(max = 220.dp),
    tagKey = "mode",
    labelStyle = PantrixTheme.typography.footnote
)

API

options

A segment button: a small set of choices side by side in one control, one of them chosen.

ParameterTypeDefaultWhat it is
optionsList<SegmentOption<T>>the choices, in order. An empty list draws nothing.
selectedT?the value of the chosen segment, compared with each option's by ==. A value no option has, null among them, chooses none: no segment is selected and no pill is drawn.
onSelect(T) -> Unitcalled with the value of the segment the user chose, by a press or by a key, when it is not the chosen one already. The control chooses nothing by itself: a segment is chosen when selected says so.
modifierModifierModifierthe control's modifier, outside the touch target it reserves.
enabledBooleantruefalse disables every segment. The pill stays where it is, so that the choice can still be read.
fullWidthBooleantruetrue spans the control across the width it is given, each segment taking an equal share, so that the segments stay where they are whatever their labels say in another language. False makes each segment as wide as its label; in a room too narrow for that, the short labels stay whole and the long ones share what is left.
styleSegmentButtonStyle?nullthis call's own style: over every enclosing one and under the density. Only the fields it sets change, on every platform.
colorsSegmentButtonColors?nullthis control's own colours: the ones it sets, with the theme's for the rest. A disabled control's track and a segment that cannot be chosen keep the theme's.
tagKeyString?nullthe control's automation key: the control is SegmentButton_<tagKey>, and its segments are named inside it, SegmentButton_<tagKey>.Segment_<key>, so that two controls with the same options are told apart. A choice is reported as a SegmentButton event, select, which carries the segment's key, or its place without one, and never its label.
labelStyleTextStylePantrixTheme.typography.labelthe type the labels are set in: the theme's label unless given. A label set larger than the track makes it taller, as a large font scale does.

SegmentButtonStyle

PantrixSegmentButton's geometry: the track, the pill that slides inside it, and the room each segment gives its label.

ParameterTypeDefaultWhat it is
shapeShapeSpecShapeSpec.UnspecifiedThe track's shape: a step of the brand's shape scale, or a fixed shape.
heightDpDp.UnspecifiedThe track's height. A label set larger than it has room for, at a large font scale, makes the track taller.
trackPaddingDpDp.UnspecifiedThe room between the track's edge and the pill.
segmentPaddingDpDp.UnspecifiedThe room on each side of a segment's label.
iconGapDpDp.UnspecifiedBetween a segment's icon and its label.
iconSizeDpDp.UnspecifiedThe width and height of a segment's icon.

SegmentButtonColors

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

ParameterTypeDefaultWhat it is
trackColorColor.UnspecifiedThe track the segments stand on. The neutral role's container.
pillColorColor.UnspecifiedThe pill under the chosen segment. The theme's surface.
selectedContentColorColor.UnspecifiedThe label and the icon of the chosen segment: the colour that reads on pill. The theme's primary.
unselectedContentColorColor.UnspecifiedThe label and the icon of the other segments: the colour that reads on track. The theme's textSecondary.

Accessibility