Whole screens
Showcase
Not crops and not mock-ups. Each of these is a window the toolkit rendered while this site was being built.
The demo application
A screen of orders — a table that sorts and filters, each order opened in a panel that slides in beside it — and, down the side, every component by category. The four pickers at the top switch language, direction, size and theme for the whole window.
jbang demo-toolkit/limn-toolkitjbang --java-options=-XstartOnFirstThread demo-toolkit/limn-toolkit

The same window, right to left
The demo application under the Arabic bundle with the direction axis flipped: the menu bar, the navigation, the fields, the scrollbars and the table all read from the right, and the text inside them joins and reorders as Arabic should. No screen was rewritten — direction is one inherited axis, set the way a control size is.


A form
Captions, a validated field, a choice, a switch and the actions row: the worked example from the forms guide.


Code
public static Scene scene() {
Row centred = new Row();
centred.mainAlignment(Flex.MainAlignment.CENTER)
.crossAlignment(Flex.CrossAlignment.CENTER);
centred.add(new SizedBox(420, SizedBox.UNSET, form()));
Scene scene = new Scene(new Padding(Insets.all(32), centred));
return scene;
}
public static Widget<?> form() {
TextField name = new TextField();
name.setText("Ada Lovelace");
TextField email = new TextField();
email.setText("ada@example");
Field emailField = emailField(email);
validate(email, emailField);
// setText above was the application's write, and a handler runs for the user alone, so
// the rule in validate did not run: the starting state is set here, explicitly.
emailField.setValidation(Validation.ERROR, "Enter an address like ada@example.com");
PasswordField password = new PasswordField();
password.setText("correct horse battery");
ComboBox<String> plan = new ComboBox<>(List.of("Personal", "Team", "Enterprise"));
plan.setSelectedIndex(1);
Checkbox updates = new Checkbox(Checkbox.Variant.SWITCH, "Email me release notes");
updates.setChecked(true);
Column form = new Column();
form.gap(18).crossAlignment(Flex.CrossAlignment.STRETCH);
form.add(new Label("Create your account").setRole(Label.Role.TITLE));
form.add(counted(new Field("Full name", name).setRequired(true), name, 40));
form.add(emailField);
form.add(new Field("Password", password).setHelperText("At least 12 characters"));
form.add(new Field("Plan", plan));
form.add(updates);
form.add(Separator.horizontal());
form.add(actions());
return form;
}
static Field emailField(TextField email) {
return new Field("Email", email)
.setHelperText("The receipt goes here")
.setRequired(true);
}
static Field counted(Field field, TextField text, int limit) {
field.setHelperText(text.text().length() + " / " + limit);
text.observeChanges((widget, change) -> {
if (change.aspect() == Change.Aspect.TEXT) {
field.setHelperText(text.text().length() + " / " + limit);
}
});
return field;
}
static void validate(TextField email, Field field) {
email.onChange(text -> {
if (text.matches("[^@\\s]+@[^@\\s]+\\.[^@\\s]+")) {
field.setValidation(Validation.SUCCESS);
} else {
field.setValidation(Validation.ERROR, "Enter an address like ada@example.com");
}
});
}
static Widget<?> actions() {
Row actions = new Row();
actions.gap(12).crossAlignment(Flex.CrossAlignment.CENTER);
actions.add(Expanded.spacer(1));
actions.add(new Button("Cancel").setStyle(Button.Style.SECONDARY));
actions.add(new Button("Create account"));
return actions;
}A window laid out
A toolbar, a sidebar beside a content pane, and a status line: the worked example from the layout guide.


Code
public static Scene scene() {
Scene scene = new Scene(shell());
return scene;
}
public static Widget<?> shell() {
ToolBar bar = new ToolBar();
bar.addItem(new Button("Import"));
bar.addSeparator();
bar.addItem(new Button("New collection").setStyle(Button.Style.SECONDARY));
bar.addItem(new Button("Share").setStyle(Button.Style.SECONDARY));
SplitPane panes = SplitPane.horizontal(sidebar(), library());
panes.setRatio(0.24f).setMinimums(150, 320);
Row status = spread(
new Label("18 assets · 3 selected").setMuted(true),
new Label("Synced 2 minutes ago").setMuted(true));
return column(0,
bar,
Expanded.of(panes, 1),
Separator.horizontal(),
new Padding(Insets.symmetric(8, 14), status));
}
private static Widget<?> sidebar() {
return column(0,
Expanded.of(collections(), 1),
Separator.horizontal(),
new Padding(Insets.all(14), details("Textures", "Studio drive")));
}
private static Widget<?> collections() {
ListView<String> list = new ListView<>(
name -> new Padding(Insets.symmetric(9, 14), new Label(name)));
list.setItems(COLLECTIONS);
list.setSelectedIndex(1);
// A list with no caption takes its name directly; a reader says "Collections, list".
list.setAccessibleName("Collections");
return list;
}
private static Widget<?> library() {
SearchField search = new SearchField();
SegmentedControl view = new SegmentedControl(List.of("Grid", "List"));
view.setSelectedIndex(0);
view.setAccessibleName("View");
Row tools = new Row();
tools.gap(10).crossAlignment(Flex.CrossAlignment.CENTER);
tools.add(new SizedBox(190, SizedBox.UNSET, search));
tools.add(view);
List<Widget<?>> tiles = new ArrayList<>();
for (String[] asset : ASSETS) {
tiles.add(tile(asset));
}
return new Padding(Insets.all(20), column(18,
spread(new Label("Textures").setRole(Label.Role.TITLE), tools),
tags(TAGS),
Expanded.of(new ScrollView(tiles(3, tiles)), 1)));
}
private static Widget<?> tile(String[] asset) {
Label badge = new Label(asset[1]);
badge.setColor(Color.WHITE).setStrong(true);
return column(6,
overlay(swatch(asset[3], asset[4]), badge),
new Label(asset[0]).setStrong(true),
new Label(asset[2]).setMuted(true));
}
private static Widget<?> swatch(String from, String to) {
int width = 96;
int height = 64;
Color a = Color.fromHex(from);
Color b = Color.fromHex(to);
byte[] pixels = new byte[width * height * 4];
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
float t = (x / (float) (width - 1) + y / (float) (height - 1)) / 2f;
int i = (y * width + x) * 4;
Color mixed = a.lerp(b, t);
pixels[i] = (byte) Color.toByte(mixed.r());
pixels[i + 1] = (byte) Color.toByte(mixed.g());
pixels[i + 2] = (byte) Color.toByte(mixed.b());
pixels[i + 3] = (byte) 0xFF;
}
}
return new ImageView(new Image(width, height, pixels))
.setFit(ImageView.Fit.COVER)
.setPreferredSize(600, 86);
}
private static final List<String> COLLECTIONS =
List.of("All assets", "Textures", "Models", "HDRIs", "Fonts", "Archive");
/** The library's filters, as many and as long as a real collection's would be. */
private static final List<String> TAGS = List.of("Tileable", "PBR", "4K", "Outdoor",
"Brushed metal", "Wood", "Stone", "Organic", "Seamless", "Hand-painted", "Sci-fi panels");
/** name · overlay badge · size on disk · the two ends of the swatch's ramp. */
private static final String[][] ASSETS = {
{"Concrete 04", "2048 × 2048", "4.2 MB", "1E1230", "6D00E0"},
{"Brushed steel", "1024 × 1024", "1.8 MB", "10131E", "5A6E8C"},
{"Oak planks", "2048 × 2048", "3.6 MB", "241206", "9A5A22"},
{"Sand dunes", "4096 × 4096", "18.4 MB", "2A1A08", "C79A4B"},
{"Rust patina", "2048 × 2048", "5.1 MB", "2B0D08", "A8442A"},
{"Marble tile", "2048 × 2048", "3.9 MB", "141626", "8E93C8"},
{"Moss bank", "2048 × 2048", "4.7 MB", "0C1A0E", "3F8A46"},
// Eight, not nine: the last line of the grid is deliberately short, so the
// picture on the guide page shows what a short line does to the tile width.
{"Sea ice", "4096 × 4096", "21.0 MB", "081A22", "6FBFD6"},
};
static Column column(float gap, Widget<?>... children) {
Column column = new Column();
column.gap(gap).crossAlignment(Flex.CrossAlignment.STRETCH);
for (Widget<?> child : children) {
column.add(child);
}
return column;
}
static Row spread(Widget<?> leading, Widget<?> trailing) {
Row row = new Row();
row.gap(12).crossAlignment(Flex.CrossAlignment.CENTER);
row.add(leading);
row.add(Expanded.spacer(1));
row.add(trailing);
return row;
}
static Widget<?> overlay(Widget<?> content, Widget<?> on) {
Stack stack = new Stack();
stack.alignment(Stack.Alignment.CENTER);
stack.add(content);
stack.add(on);
return stack;
}
static Wrap tags(List<String> names) {
Wrap tags = new Wrap();
tags.gap(8).runGap(8);
tags.setControlSize(ControlSize.SMALL);
for (String name : names) {
tags.add(new Button(name).setStyle(Button.Style.SECONDARY));
}
return tags;
}
static Grid details(String name, String location) {
TextField nameField = new TextField();
nameField.setText(name);
TextField locationField = new TextField();
locationField.setText(location);
Checkbox shared = new Checkbox(Checkbox.Variant.BOX, "Shared");
shared.setChecked(true);
Grid form = new Grid(2);
form.gap(12).rowGap(10).expandedColumn(1)
.columnAlignment(1, Flex.CrossAlignment.STRETCH);
form.add(new Label("Name").setMuted(true).setLabelFor(nameField));
form.add(nameField);
form.add(new Label("Location").setMuted(true).setLabelFor(locationField));
form.add(locationField);
form.add(new SizedBox(0, 0)); // an empty cell: the checkbox carries its own caption
form.add(shared);
return form;
}
static Column tiles(int perRow, List<Widget<?>> tiles) {
Column rows = new Column();
rows.gap(14).crossAlignment(Flex.CrossAlignment.STRETCH);
for (int start = 0; start < tiles.size(); start += perRow) {
Row line = new Row();
line.gap(14).crossAlignment(Flex.CrossAlignment.START);
for (int i = start; i < Math.min(start + perRow, tiles.size()); i++) {
line.add(Expanded.of(tiles.get(i), 1));
}
for (int i = tiles.size(); i < start + perRow; i++) {
line.add(Expanded.spacer(1));
}
rows.add(line);
}
return rows;
}Every size step
The same five controls, five times, from XSMALL at the top down to XLARGE at the bottom. Not one of them is given a width, a font or a padding: each row is told a control size and nothing else, and the padding, the type, the corner radii and the hit targets all move together.


The 3D viewport
Physically-based materials under three lights, rendered to a linear high-dynamic-range target and composited as a 2D layer. A scroll view clips it like any other widget. Drag to orbit it, scroll to zoom.


The theme editor, a program that writes a file
A palette is data, so editing one is a screen; this one is a program you run, not a dependency your application carries. It saves a theme file, which your application loads with ThemeFormat from limn-toolkit alone. Drag the corner slider and the window re-skins in the same frame: every field, button and well in the picture. The report beside it measures each ink against every surface it can land on, so a palette that fails contrast fails visibly.
jbang theme-editor-toolkit/limn-toolkitjbang --java-options=-XstartOnFirstThread theme-editor-toolkit/limn-toolkit


