Pantrix UI

ADAPTIVE UI

One screen, every window. Folds too.

Pantrix reads the window, not the device: its width class, its height, a foldable's posture and the pointer. The scaffolds move the navigation, split the panes and meet at the fold by themselves. Pick a window below and watch one app lay itself out.

widthClass

Large

heightClass

Medium

posture

Flat

isNarrow

false

isShort

false

paneCount()

2

layout.margin

32dp

layout.gutter

24dp

columns

12

navigation

Start · sidebar

Why it looks like this: The list and detail scaffold measures its own box, not the window: 1032dp beside the sidebar is Expanded, so 2 panes.

Five widths, three heights

The breakpoints are Android's window size classes, the same on every platform. What is measured is the window: an app in a split screen, a resized desktop window or a browser tab is as wide as that window. The marker is the window chosen above.

Compact

under 600dp

Medium

600–839

Expanded

840–1199

Large

1200–1599

ExtraLarge

1600 and up

Laptop · 1280dp

Height Compact

under 480dp

A phone on its side: no bar at the bottom, the main action in the header.

Height Medium

480–899dp

A phone upright, a tablet on its side.

Height Expanded

900dp and up

A tablet upright, a desktop window.

The window comes with the theme

No wrapper, no setup. Every screen under PantrixTheme finds the window ready, with the page's margin and gutter for its class.

val window = PantrixTheme.window

window.widthClass              // Compact … ExtraLarge
window.heightClass             // Compact, Medium, Expanded
window.posture.isBook          // half open, the hinge upright
window.posture.isTabletop      // half open, the hinge across
window.paneCount()             // 1, 2 or 3 side by side

PantrixTheme.layout.margin     // 16, 24 or 32dp
PantrixTheme.layout.gutter     // 16 or 24dp

val columns = window.byWidth(compact = 2, medium = 4)
val panes = window.byPosture(flat = 1, book = 2)
if (window.widthClass >= WindowWidthClass.Expanded) DetailPane()

Which helper, when

You need

Write

A value by width

window.byWidth(compact = 2, medium = 4)

The same in a composable

responsive(compact = 1, medium = 2, large = 3)

The page’s margin and gutter

PantrixTheme.layout.margin / .gutter

A value by height

window.byHeight(compact = 44.dp, medium = 56.dp)

A value by posture

window.byPosture(flat = 1, book = 2, tabletop = 2)

"The phone layout?"

window.isNarrow

"A bar fits at the bottom?"

window.isShort

From this class on

widthClass >= WindowWidthClass.Expanded

A threshold of your own

rememberWindowIsAtLeast(1000.dp)

The component’s own room

BoxWithPantrixWindow { box -> … }

A finger or a mouse

PantrixTheme.platform.isTouch

Six patterns, ready made

Each is a component that measures its own room, keeps its state while it moves, and knows the fold.

1 · PantrixScaffold

Navigation that moves with the width

Along the bottom while the scaffold is narrower than 600dp, along the start edge from there on. On a foldable set on a table it stays at the bottom, where the hands are. The scaffold measures its own width, not the window's.

PantrixScaffold(navigation = { placement, insets ->
    when (placement) {
        ScaffoldNavigationPlacement.Bottom -> PantrixNavigationBar(items, selected, onSelect, insets = insets)
        else -> PantrixNavigationRail(items, selected, onSelect, insets = insets)
    }
}) { padding -> Content(Modifier.padding(padding)) }

2 · PantrixListDetailScaffold

List and detail

An inbox, settings, contacts. In one pane the detail slides in over the list and the back gesture brings it back, Android's predictive back and iOS's edge swipe included. The focus follows the pane, and the list's width can be dragged.

val inbox = rememberListDetailState()
PantrixListDetailScaffold(
    state = inbox,
    list = { Messages(onOpen = { id -> inbox.select(id) }) },
    detail = { id -> Message(id) },
    extra = { id -> History(id) },
    resizable = true
)

3 · PantrixSupportingPaneScaffold

Main content and a supporting pane

A document and its comments, a chart and its filters. Beside the content where there is room; in a narrow room a sheet from the bottom on a phone and from the end on a tablet, or the content's own place. A comment typed stays while the pane moves.

val comments = rememberSupportingPaneState()
PantrixSupportingPaneScaffold(
    state = comments,
    main = { Document(onComments = { comments.show() }) },
    supporting = { Comments() },
    supportingTitle = stringResource(Res.string.comments),
    presentation = SupportingPanePresentation.Sheet
)

4 · PantrixTwoPane · PantrixTabletopLayout

Folds: two panes and the tabletop

Two panes part exactly at a hinge that separates them, and the hinge's strip stays empty. On a table, what is looked at fills the half above the fold and the controls take the half below. Choose a foldable in the preview above to see both.

PantrixTabletopLayout(
    content = { Video(player) },
    controls = { PlayerControls(player, large = arrangement == TabletopArrangement.Tabletop) },
    flatControls = TabletopControls.Overlay
)

5 · PantrixTheme.layout

Columns, margins and the content's width

Margins of 16, 24 and 32dp, gutters of 16 and 24dp, and 4, 8 or 12 columns come with the class. A part spans columns; nothing is chosen by hand.

val layout = PantrixTheme.layout
Row(Modifier.maxContentWidth(), horizontalArrangement = Arrangement.spacedBy(layout.gutter)) {
    Article(Modifier.width(layout.spanWidth(maxWidth, span = 8)))
    Related(Modifier.width(layout.spanWidth(maxWidth, span = 4)))
}

6 · BoxWithPantrixWindow

A component asks its own room

The window is for decisions about the page. A component inside a narrow panel would draw its wide layout if it asked the window: it asks the box it is given instead, with the window's breakpoints.

BoxWithPantrixWindow(modifier) { box ->
    if (box.isNarrow) OrderList(orders)
    else OrderGrid(orders, showDate = box.widthClass >= WindowWidthClass.Expanded)
}

A fold in a preview and a test

PantrixWindowFrame makes a box a window of its own, with a hinge measured from its corner. The demo's cards draw the iPhone Duo's inner screen this way, as a book and on a table.

val book = PantrixWindow(
    DpSize(951.dp, 669.dp),
    posture = WindowPosture(listOf(WindowFold(
        bounds = hinge, orientation = FoldOrientation.Vertical,
        isHalfOpened = true, isSeparating = true,
        isOccluding = false, clearance = strip
    )))
)
PantrixWindowFrame(book, Modifier.size(951.dp, 669.dp)) { InboxScreen() }

Pitfalls

Not this

Calling the same content from two places by class

But this

Call it from one place; give what changes by parameter, or move it

Not this

Asking the window inside a component

But this

Ask its own box with BoxWithPantrixWindow

Not this

Reading window.size for a layout

But this

Read the class; a threshold of your own with rememberWindowIsAtLeast

Not this

Choosing the margin by hand: responsive(16.dp, 24.dp, 32.dp)

But this

PantrixTheme.layout.margin

Not this

Taking isNarrow for "a phone"

But this

A phone on its side is Expanded: isShort tells it

Not this

Looking at the platform for "touch"

But this

PantrixTheme.platform.isTouch: a phone’s browser is the web

Apache License 2.0

English

·