Português
GitHub
Tema

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@limn-toolkit/limn-toolkit
jbang --java-options=-XstartOnFirstThread demo@limn-toolkit/limn-toolkit
The demo application
The demo application

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.

The demo application in Arabic, right to left
The demo application in Arabic, right to left

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.

A form
A form
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.

A window laid out
A window laid out
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.

Every size step
Every size step

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.

The 3D viewport
The 3D viewport

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@limn-toolkit/limn-toolkit
jbang --java-options=-XstartOnFirstThread theme-editor@limn-toolkit/limn-toolkit
The theme editor
The theme editor