Telas inteiras
Telas
Não são recortes nem mock-ups. Cada uma delas é uma janela que o toolkit renderizou enquanto este site era construído.
A aplicação de demonstração
Uma tela de pedidos — uma tabela que ordena e filtra, cada pedido aberto num painel que desliza ao lado dela — e, na lateral, todos os componentes por categoria. Os quatro seletores no topo trocam idioma, direção, tamanho e tema da janela inteira.
jbang demo-toolkit/limn-toolkitjbang --java-options=-XstartOnFirstThread demo-toolkit/limn-toolkit

A mesma janela, da direita para a esquerda
A aplicação de demonstração sob o pacote árabe com o eixo de direção invertido: a barra de menus, a navegação, os campos, as barras de rolagem e a tabela passam a ler a partir da direita, e o texto dentro deles se une e se reordena como o árabe pede. Nenhuma tela foi reescrita — a direção é um eixo herdado, definido como se define um tamanho de controle.


Um formulário
Rótulos, um campo validado, uma escolha, um switch e a linha de ações: o exemplo completo do guia de formulários.


Código
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;
}Uma janela com layout
Uma barra de ferramentas, uma barra lateral ao lado de um painel de conteúdo e uma linha de status: o exemplo completo do guia de layout.


Código
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;
}Todos os passos de tamanho
Os mesmos cinco controles, cinco vezes, do XSMALL no topo até o XLARGE embaixo. Nenhum deles recebe largura, fonte ou padding: cada linha recebe um tamanho de controle e nada mais, e o padding, a tipografia, os raios de canto e as áreas de toque se movem juntos.


O viewport 3D
Materiais fisicamente corretos sob três luzes, renderizados em um alvo linear de alta faixa dinâmica e compostos como uma camada 2D. Uma scroll view o recorta como recortaria qualquer outro widget. Arraste para orbitar, role para dar zoom.


O editor de tema, um programa que grava um arquivo
Uma paleta é dado, então editá-la é uma tela; esta é um programa que você roda, não uma dependência que sua aplicação carrega. Ele salva um arquivo de tema, que sua aplicação lê com o ThemeFormat, só com o limn-toolkit. Arraste o slider de cantos e a janela se re-veste no mesmo frame: cada campo, botão e poço da imagem. O relatório ao lado mede cada tinta contra toda superfície onde ela pode cair, então uma paleta que falha em contraste falha à vista.
jbang theme-editor-toolkit/limn-toolkitjbang --java-options=-XstartOnFirstThread theme-editor-toolkit/limn-toolkit


