Español
GitHub
Tema

Pantallas enteras

Pantallas

Ni recortes ni maquetas. Cada una es una ventana que el kit renderizó mientras se construía este sitio.

La aplicación de demostración

Una pantalla de pedidos — una tabla que ordena y filtra, cada pedido abierto en un panel que se desliza a su lado — y, en el lateral, todos los componentes por categoría. Los cuatro selectores de arriba cambian idioma, dirección, tamaño y tema de toda la ventana.

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

La misma ventana, de derecha a izquierda

La aplicación de demostración bajo el paquete ar con el eje de dirección invertido: la barra de menús, la navegación, los campos, las barras de desplazamiento y la tabla se leen desde la derecha, y el texto que contienen se une y se reordena como pide el árabe. Ninguna pantalla se reescribió — la dirección es un eje heredado, que se fija como se fija un tamaño de control.

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

Un formulario

Etiquetas, un campo validado, una elección, un interruptor y la fila de acciones: el ejemplo completo de la guía de formularios.

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

Una ventana compuesta

Una barra de herramientas, una barra lateral junto a un panel de contenido y una línea de estado: el ejemplo completo de la guía de disposición.

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 los pasos de tamaño

Los mismos cinco controles, cinco veces, de XSMALL arriba a XLARGE abajo. A ninguno se le da ancho, tipografía ni relleno: cada fila recibe un tamaño de control y nada más, y el relleno, la tipografía, los radios de esquina y las zonas de pulsación se mueven juntos.

Every size step
Every size step

La vista 3D

Materiales físicamente realistas bajo tres luces, renderizados en un destino lineal de alto rango dinámico y compuestos como una capa 2D. Una vista con desplazamiento la recorta igual que a cualquier otro widget. Arrastra para orbitar, desplaza para acercar.

The 3D viewport
The 3D viewport

El editor de temas, un programa que escribe un archivo

Una paleta son datos, así que editarla es una pantalla; esta es un programa que ejecutas, no una dependencia que carga tu aplicación. Guarda un archivo de tema, que tu aplicación lee con ThemeFormat usando solo limn-toolkit. Arrastra el control de esquinas y la ventana se re-viste en el mismo fotograma: cada campo, botón y pocillo de la imagen. El informe contiguo mide cada tinta contra toda superficie donde puede caer, así que una paleta que falla en contraste falla a la vista.

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