Deutsch
GitHub
Design

Ganze Oberflächen

Oberflächen

Keine Ausschnitte und keine Mock-ups. Jedes davon ist ein Fenster, das das Toolkit gerendert hat, während diese Website gebaut wurde.

Die Demo-Anwendung

Ein Bestellbildschirm — eine Tabelle, die sortiert und filtert, jede Bestellung in einem Bereich geöffnet, der daneben hereingleitet — und an der Seite jede Komponente nach Kategorie. Die vier Auswahlfelder oben wechseln Sprache, Richtung, Größe und Design des ganzen Fensters.

jbang demo@limn-toolkit/limn-toolkit
jbang --java-options=-XstartOnFirstThread demo@limn-toolkit/limn-toolkit
The demo application
The demo application

Dasselbe Fenster, von rechts nach links

Die Demo-Anwendung unter dem ar-Bündel mit umgekehrter Richtungsachse: Menüleiste, Navigation, Felder, Bildlaufleisten und die Tabelle lesen von rechts, und der Text darin verbindet und ordnet sich, wie Arabisch es verlangt. Kein Bildschirm wurde umgeschrieben — die Richtung ist eine vererbte Achse, gesetzt wie eine Steuerelementgröße.

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

Ein Formular

Beschriftungen, ein validiertes Feld, eine Auswahl, ein Schalter und die Aktionszeile: das ausgearbeitete Beispiel aus der Formular-Anleitung.

A form
A form
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;
}

Ein gesetztes Fenster

Eine Werkzeugleiste, eine Seitenleiste neben einem Inhaltsbereich und eine Statuszeile: das ausgearbeitete Beispiel aus der Layout-Anleitung.

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

Jede Größenstufe

Dieselben fünf Bedienelemente, fünfmal, von XSMALL oben bis XLARGE unten. Keinem davon wird eine Breite, eine Schrift oder ein Innenabstand gegeben: jede Zeile bekommt eine Steuergröße und sonst nichts, und Innenabstand, Typografie, Eckradien und Trefferflächen bewegen sich gemeinsam.

Every size step
Every size step

Der 3D-Viewport

Physikalisch basierte Materialien unter drei Lichtern, in ein lineares HDR-Ziel gerendert und als 2D-Ebene zusammengesetzt. Eine Scroll-Ansicht beschneidet ihn wie jedes andere Widget. Ziehen zum Kreisen, scrollen zum Zoomen.

The 3D viewport
The 3D viewport

Der Theme-Editor, ein Programm, das eine Datei schreibt

Eine Palette sind Daten, also ist ihre Bearbeitung ein Bildschirm; dieser ist ein Programm, das Sie starten, keine Abhängigkeit, die Ihre Anwendung mitträgt. Er speichert eine Theme-Datei, die Ihre Anwendung mit ThemeFormat liest, allein mit limn-toolkit. Ziehen Sie den Ecken-Regler, und das Fenster kleidet sich im selben Bild neu ein: jedes Feld, jeder Knopf, jedes Farbfeld. Der Bericht daneben misst jede Tinte gegen jede Fläche, auf der sie landen kann, sodass eine Palette mit zu wenig Kontrast sichtbar scheitert.

jbang theme-editor@limn-toolkit/limn-toolkit
jbang --java-options=-XstartOnFirstThread theme-editor@limn-toolkit/limn-toolkit
The theme editor
The theme editor