Skip to content

Appearances

An appearance says how values of one type look to a user: an icon, a name and a tooltip. Register one per type, and every list, tree, grid, combo box and table text column shows those values with their icon, name and tooltip, with no cell factory.

Strata.appearances().register(Crate.class, Appearance.<Crate>builder()
        .icon(Crate::icon)
        .name(crate -> Text.of(crate.name()))
        .tooltip(crate -> Tooltip.text(crate.stock() + " left"))
        .build());

// A listing looks like its crate
Strata.appearances().register(Listing.class, Appearance.as(Listing::crate));
// Rows show each crate's icon, name and stock, with no cell factory
final ListView<Listing> list = ui.<Listing>listView().setItems(listings);

// Label functions take Text, a String, or a value with an appearance
final ComboBox<Listing> cheapest = ui.<Listing>comboBox(listing -> Text.of(listing.price() + " coins"))
        .setItems(listings);

Registering

Strata.appearances() returns the Appearances registry for the whole process, so you can register while your project starts, before any screen exists, from any thread. Inside a screen, ui.appearances() returns the same registry.

Part Default
icon(value) None
name(value) String.valueOf(value) as text
tooltip(value) None. Asked anew each time the pointer arrives, so it can follow settings that change.

Appearance.builder() makes one from functions. The icon and tooltip functions may return null for none, and the name function must return a name. Parts you leave unset keep the defaults above. Appearance.as(mapping) makes a type look like another value it leads to, such as a listing like its item, and looks that value's appearance up anew each time.

You can also implement Appearance yourself. Each method must be a pure function of the value as it is at the call. Controls call them often, on the UI thread, so keep them quick. An icon follows the adapter's rules for snapshots and live visuals; see Visuals and icons.

register returns a Subscription. A later registration for the same type hides an earlier one until you remove it, so you can replace an engine adapter's default look for a type.

A value takes the appearance registered for its class, else for its nearest superclass with one, else for the nearest of its interfaces with one. Interfaces are searched breadth first: those the class and its superclasses implement directly, then the interfaces those extend. Of two interfaces at the same distance, the one whose type was registered first wins. A value with nothing registered has no icon and no tooltip, and is named by String.valueOf. Lookups are cached per class until a registration changes.

Strata registers Text, named by itself, and Visual, whose icon is itself. The Swing adapter registers BufferedImage. Other adapters register their engine's types, such as items.

Where appearances show

Control What it shows
ListView, TreeView, GridView made without a cell factory factory.cell(item): one cell label with the icon as a picture in the text, then the name, and the tooltip
ComboBox The name of what the label function returns, the option itself by default
CycleButton The name of what the value label function returns, the value itself by default
TableView text columns, TableColumn.setCellText The name of what the function returns; the column sorts by it, ignoring case
Image.setValue(value), factory.image(value) The icon at its natural size, and the tooltip until you set another

Label functions take any result: a String, a Text, or a value with an appearance of its own. Use factory.cell(value) in your own cell factories to keep the default row and add to it.

Asking for a look

iconOf, nameOf and tooltipOf on the registry take any value and answer as the controls do, so your own rows and components can show values the same way. find(type) returns the appearance a class takes, if any. Ask on the UI thread.

// A heading for a listing: its icon and name in gold, with the same tooltip a list row would have
final Appearances look = ui.appearances();
final Text name = look.nameOf(listing).color(0xFFFFAA00);
final Optional<Visual> icon = look.iconOf(listing);
final Label heading = ui.label(icon.map(visual -> Text.visual(visual).append(" ").append(name)).orElse(name));
look.tooltipOf(listing).ifPresent(tooltip -> heading.setTooltip(tooltip));

A control that only shows a value differently, such as a slot with an item and its price, is a few lines with CustomComponent, Painter.visual and appearances(); see Extending Strata.

Visuals and icons

An icon is a Visual, something only the engine can draw, such as a loaded image. Adapters make visuals in classes of their own, such as SwingVisuals.image on Swing, and draw a placeholder for a visual another adapter made. A visual is a snapshot or live, and the adapter method that makes it says which. A snapshot copies the object when it is made, such as an engine object the engine itself keeps changing; to show a change, make a new visual, and two snapshots of the same thing are equal. A live visual holds an object you made and own, such as an image you draw into, and shows each change on the next paint; two live visuals are equal only when they hold the same object. SwingVisuals.image makes live visuals.