日本語
GitHub
テーマ

一式

コンポーネント

ここの画像はすべて、このビルド中にツールキットが描画したものです。コードはどれも、隣の画像を生んだそのコードです。

Button
Button

Button

コード
static Built button() {
    Row row = new Row();
    row.gap(12).crossAlignment(Flex.CrossAlignment.CENTER);
    Button disabled = new Button("Disabled");
    disabled.setEnabled(false);
    row.add(new Button("Primary"));
    row.add(new Button("Secondary").setStyle(Button.Style.SECONDARY));
    row.add(new Button("Tertiary").setStyle(Button.Style.TERTIARY));
    row.add(new Button("Delete").setStyle(Button.Style.SECONDARY).setDestructive(true));
    row.add(disabled);
    return scene(row);
}
Label
Label

Label

コード
static Built label() {
    Column column = new Column();
    column.gap(10);
    column.add(new Label("Body text"));
    column.add(new Label("Muted text").setMuted(true));
    Label clipped = new Label("A long line that does not fit, measured and ellipsised");
    clipped.setOverflow(Label.Overflow.ELLIPSIS);
    column.add(new SizedBox(260, SizedBox.UNSET, clipped));
    return scene(column);
}
TextField
TextField

TextField

コード
static Built textField() {
    Column column = new Column();
    column.gap(12);
    column.add(new TextField().setText("Typed text"));
    column.add(new TextField().setPlaceholder("Placeholder"));
    // A mask writes its own literals: what is typed is KLM4217, what shows is KLM-4217.
    column.add(new TextField().setPlaceholder("Serial number").setMask("AAA-0000")
            .setText("KLM4217"));
    return scene(column);
}
Field
Field

Field

コード
static Built field() {
    // The caption names the control, to a screen reader too. The line under it is the helper
    // text until the application decides a state, and then the message, after the state's
    // glyph, so the state never rests on colour alone.
    Field email = new Field("Email", new TextField().setText("ana.lima@"))
            .setRequired(true)
            .setValidation(Validation.ERROR, "Enter an address like ana@example.com");
    Field phone = new Field("Phone", new TextField().setPlaceholder("+55 11 91234-5678"))
            .setHelperText("Only if a delivery goes wrong");
    Field user = new Field("Username", new TextField().setText("ana.lima"))
            .setValidation(Validation.SUCCESS, "Available");
    Column form = new Column();
    form.gap(12).crossAlignment(Flex.CrossAlignment.STRETCH);
    form.add(email);
    form.add(phone);
    form.add(user);
    return scene(new SizedBox(300, SizedBox.UNSET, form));
}
Checkbox and switch
Checkbox and switch

Checkbox and switch

コード
static Built checkbox() {
    Column column = new Column();
    column.gap(12);
    column.add(new Checkbox(Checkbox.Variant.BOX, "Box, checked").setChecked(true));
    column.add(new Checkbox(Checkbox.Variant.BOX, "Box, unchecked"));
    column.add(new Checkbox(Checkbox.Variant.SWITCH, "Switch, on").setChecked(true));
    column.add(new Checkbox(Checkbox.Variant.SWITCH, "Switch, off"));
    return scene(column);
}
ComboBox
ComboBox

ComboBox

コード
static Built comboBox() {
    // Editable: typing narrows the list, the items that start with the text first, and a
    // click or Enter picks one. Without setEditable(true) the field is a button.
    ComboBox<String> combo = new ComboBox<>(List.of("Alps", "Andes", "Appalachians", "Atlas",
            "Carpathians", "Caucasus", "Himalayas", "Pyrenees", "Rockies", "Urals"))
            .setEditable(true);
    combo.setSelectedItem("Andes");
    // A height, and the list is what it is for: this entry is filmed opening its list, and
    // the site crops the still and the film of an entry to ONE box, so that pressing play
    // swaps a picture rather than resizing the card. That box is the widget's, measured
    // while the layout settles -- before any list exists. Cropped to the field alone, the
    // film published a drop-down with its head cut off.
    Column room = new Column();
    room.mainAlignment(Flex.MainAlignment.START).crossAlignment(Flex.CrossAlignment.STRETCH);
    room.add(combo);
    return scene(new SizedBox(260, 176, room));
}
ComboBox (many)
ComboBox (many)

ComboBox (many)

コード
static Built comboBoxMany() {
    // Holding many: a chip per item in the list's order, and a count of what does not fit.
    ComboBox<String> combo = new ComboBox<>(List.of("Alps", "Andes", "Appalachians", "Atlas",
            "Carpathians", "Caucasus", "Himalayas", "Pyrenees", "Rockies", "Urals"))
            .setSelectionMode(SelectionMode.MULTI);
    combo.setSelectedItems(List.of("Andes", "Atlas", "Caucasus", "Himalayas", "Pyrenees", "Urals"));
    return scene(new SizedBox(300, SizedBox.UNSET, combo));
}
ListView
ListView

ListView

コード
static Built listView() {
    // Rows are materialised on demand: the list only ever builds what the viewport
    // shows, so this adapter would cost the same with a million rows.
    List<String> rows = new java.util.ArrayList<>(java.util.stream.IntStream.rangeClosed(1, 1000)
            .mapToObj(i -> "Row " + i).toList());
    ListView<String> list = new ListView<>(
            text -> new Padding(limn.scene.Insets.symmetric(8, 12), new Label(text)));
    list.setItems(rows);
    // Rows the user rearranges: dragged to another place, or moved with Alt+Shift+Up and
    // Down. The list shows the move and the handler makes it, in the application's own list.
    list.setDragEnabled(true);
    list.onReorder((moved, index) -> {
        rows.removeAll(moved);
        rows.addAll(index, moved);
    });
    return scene(new SizedBox(320, 130, list));
}
Table
Table

Table

コード
static Built table() {
    // Columns say how a row becomes a cell; the rows are the application's own list, held
    // by reference and read only when they are on screen.
    record Release(String name, String platform, int downloads) {
    }
    List<Release> rows = List.of(
            new Release("Meridian", "Windows", 4820), new Release("Halcyon", "macOS", 3915),
            new Release("Corvid", "Linux", 2760), new Release("Lumen", "Windows", 2314),
            new Release("Tessera", "macOS", 1987), new Release("Orrery", "Linux", 1502),
            new Release("Vantage", "Windows", 1210), new Release("Kestrel", "macOS", 864));
    limn.components.table.Table<Release> table = new limn.components.table.Table<>(List.of(
            limn.components.table.Column.text("Name", Release::name).width(120).weight(1),
            limn.components.table.Column.text("Platform", Release::platform).width(100),
            limn.components.table.Column.numeric("Downloads", Release::downloads).width(124)));
    table.setRows(rows);
    table.setSort(table.columns().get(2), limn.components.table.SortOrder.DESCENDING);
    table.setSelectedRow(2);
    // Nothing here asks for it: the user moves a column by dragging its header, or with
    // Alt+Shift+Left and Right, and columns() answers the order as it stands.
    return scene(new SizedBox(360, 200, table));
}
Tree
Tree

Tree

コード
static Built tree() {
    // The application's own node. A class and not a record: a node is the same node after
    // its children change, which is what the tree keeps a branch open by.
    final class Node {
        final String name;
        final java.util.List<Node> kids;

        Node(String name, Node... kids) {
            this.name = name;
            this.kids = new java.util.ArrayList<>(List.of(kids));
        }

        boolean take(Node node) {
            return kids.remove(node) || kids.stream().anyMatch(kid -> kid.take(node));
        }
    }
    // The model answers three questions about the application's own objects: where the tree
    // starts, what is under a node, and what widget draws one. Nothing here is a node type
    // the toolkit owns.
    Node top = new Node("",
            new Node("Documents",
                    new Node("Reports", new Node("2026.pdf")),
                    new Node("notes.md")),
            new Node("Media", new Node("clip.mp4")),
            new Node("archive.zip"));
    limn.components.tree.Tree<Node> tree = new limn.components.tree.Tree<>(
            new limn.components.tree.Tree.Model<Node>() {
                @Override
                public java.util.List<Node> roots() {
                    return top.kids;
                }

                @Override
                public java.util.List<Node> children(Node node) {
                    return node.kids;
                }

                @Override
                public Widget<?> cellFor(Node node) {
                    // A row is whatever the application builds: the tree reserves the indent
                    // and the triangle, and hands the rest of the width to this. An icon
                    // before the text and a count against the trailing edge are composed
                    // here, not configured on the widget.
                    Label text = new Label(node.name);
                    text.setIcon(limn.graphics.SvgIcon.fromResource(node.kids.isEmpty()
                            ? "/limn/components/icons/info.svg"
                            : "/limn/components/icons/settings.svg"));
                    Row row = new Row();
                    row.gap(8).crossAlignment(Flex.CrossAlignment.CENTER);
                    row.add(limn.scene.layout.Expanded.of(text));
                    if (!node.kids.isEmpty()) {
                        row.add(new Label(String.valueOf(node.kids.size())).setMuted(true));
                    }
                    return new Padding(limn.scene.Insets.symmetric(6, 4), row);
                }
            });
    // Nodes the user rearranges: dropped on a node they go into it, near its edge before
    // or after it, and Alt+Shift with an arrow moves the selected one. A null parent is the
    // top level.
    tree.setDragEnabled(true);
    tree.onReorder((nodes, parent, index) -> {
        nodes.forEach(top::take);
        (parent == null ? top : parent).kids.addAll(index, nodes);
    });
    Node documents = top.kids.get(0);
    tree.expand(documents);
    tree.setSelected(documents.kids.get(1));
    return scene(new SizedBox(300, 190, tree));
}
ScrollView and ScrollBar
ScrollView and ScrollBar

ScrollView and ScrollBar

コード
static Built scrollView() {
    Column tall = new Column();
    tall.gap(8);
    for (int i = 1; i <= 12; i++) {
        tall.add(new Label("Scrollable line " + i));
    }
    ScrollView scroller = new ScrollView(new Padding(limn.scene.Insets.all(12), tall));
    // ALWAYS, not the default: a bar that fades on idle is invisible in a still.
    scroller.setScrollbarPolicy(ScrollBar.Policy.ALWAYS);
    return scene(new SizedBox(320, 130, scroller));
}
Dialog
Dialog

Dialog

コード
static Built dialog() {
    // IN_SCENE, not the default native window: an overlay drawn inside the owner is
    // what one framebuffer can capture. The native-window mode is the same widget in
    // a window of its own, which a screenshot of this one would not contain.
    Column owner = new Column();
    owner.gap(8);
    owner.add(new Label("Owner content, dimmed by the scrim").setMuted(true));
    Built built = scene(new SizedBox(340, 140, owner));
    new Dialog("Discard changes?", "Your edits will be lost.")
            .setDisplayMode(DisplayMode.IN_SCENE)
            .addPrimaryButton("Keep editing", "cancel")
            .addDestructiveButton("Discard", "discard")
            .show(built.scene());
    return built;
}
ToolBar
ToolBar

ToolBar

コード
static Built toolBar() {
    ToolBar bar = new ToolBar();
    bar.addItem(new Button("New"));
    bar.addItem(new Button("Open").setStyle(Button.Style.SECONDARY));
    bar.addSeparator();
    bar.addItem(new Button("Save").setStyle(Button.Style.SECONDARY));
    return scene(bar);
}
TabbedPane
TabbedPane

TabbedPane

コード
static Built tabbedPane() {
    // Closable: the selected tab and the one under the pointer show their close button, and
    // every tab is dragged along the bar to a new place.
    TabbedPane tabs = new TabbedPane().setTabsClosable(true);
    tabs.addTab("Overview", new Label("Content of the selected tab."));
    tabs.addTab("Details", new Label("Second tab."));
    tabs.addTab("History", new Label("Third tab."));
    return scene(new SizedBox(360, 120, tabs));
}
TextArea
TextArea

TextArea

コード
static Built textArea() {
    TextArea area = new TextArea();
    area.setText("""
            A multiline editor with draggable scrollbars.
            Arrow keys move across lines; the wheel scrolls.
            Selection is by grapheme cluster, so combining
            marks and ZWJ emoji are never split.""");
    return scene(new SizedBox(360, 120, area));
}
RadioButton
RadioButton

RadioButton

コード
static Built radioButton() {
    RadioButton one = new RadioButton("Selected");
    RadioButton two = new RadioButton("Not selected");
    new limn.components.ButtonGroup().add(one).add(two);
    one.select();
    Column column = new Column();
    column.gap(12);
    column.add(one);
    column.add(two);
    return scene(column);
}
Separator
Separator

Separator

コード
static Built separator() {
    Column column = new Column();
    column.gap(14);
    column.add(new Label("Above"));
    column.add(new SizedBox(260, SizedBox.UNSET, Separator.horizontal()));
    column.add(new Label("Below").setMuted(true));
    return scene(column);
}
ProgressBar
ProgressBar

ProgressBar

コード
static Built progressBar() {
    Column column = new Column();
    column.gap(16);
    // Determinate only: an indeterminate bar animates, and an animated widget captured
    // mid-sweep gives a different picture every run.
    column.add(new Label("Determinate").setMuted(true));
    column.add(new ProgressBar().setProgress(0.62f).setPreferredWidth(280));
    return scene(column);
}
ProgressBar (indeterminate)
ProgressBar (indeterminate)

ProgressBar (indeterminate)

コード
static Built progressIndeterminate() {
    // The sweep animates, so this entry is captured at a pinned scene time rather than
    // whenever the frame happened to land (see Gallery's fixed warmup).
    ProgressBar bar = new ProgressBar();
    bar.setIndeterminate(true).setPreferredWidth(280);
    return scene(bar);
}
ImageView
ImageView

ImageView

コード
static Built imageView() {
    // A generated image rather than a file: the capture must not depend on an asset
    // that a checkout might not have.
    int size = 96;
    byte[] pixels = new byte[size * size * 4];
    for (int y = 0; y < size; y++) {
        for (int x = 0; x < size; x++) {
            int i = (y * size + x) * 4;
            pixels[i] = (byte) (x * 255 / size);
            pixels[i + 1] = (byte) (y * 255 / size);
            pixels[i + 2] = (byte) 0xC0;
            pixels[i + 3] = (byte) 0xFF;
        }
    }
    ImageView view = new ImageView(new limn.graphics.Image(size, size, pixels));
    view.setPreferredSize(96, 96);
    // The picture can be dragged out, to a drop area of this program or to another one.
    view.setDragEnabled(true);
    return scene(view);
}
SearchField
SearchField

SearchField

コード
static Built searchField() {
    return scene(new SearchField());
}
PasswordField
PasswordField

PasswordField

コード
static Built passwordField() {
    // The dot is DRAWN rather than typeset, so it needs no glyph coverage at any
    // control size, which is why this reads the same in every locale on the site.
    PasswordField masked = new PasswordField();
    masked.setText("correct horse");
    return scene(masked);
}
Slider
Slider

Slider

コード
static Built slider() {
    Slider slider = new Slider(0, 100);
    slider.setValue(65);
    return scene(new SizedBox(280, SizedBox.UNSET, slider));
}
RangeSlider
RangeSlider

RangeSlider

コード
static Built rangeSlider() {
    // Two thumbs that never cross: the lower stops at the upper, and the other way round.
    RangeSlider price = new RangeSlider(0, 500);
    price.setStep(10);
    price.setValues(120, 340);
    return scene(new SizedBox(280, SizedBox.UNSET, price));
}
Expander
Expander

Expander

コード
static Built expander() {
    // A group keeps one section open; the summary says what a closed one holds.
    ExpanderGroup group = new ExpanderGroup();
    Column sections = new Column();
    Label shipping = new Label("Delivered in three to five working days.");
    for (Expander section : List.of(
            new Expander("Shipping", shipping).setSummary("Free"),
            new Expander("Returns", new Label("Within thirty days, unused.")).setSummary("30 days"),
            new Expander("Warranty", new Label("Two years against defects.")).setSummary("2 years"))) {
        group.add(section);
        sections.add(section);
    }
    group.setExpandedIndex(0);
    return scene(new SizedBox(320, SizedBox.UNSET, sections));
}
Card
Card

Card

コード
static Built card() {
    // A customer's record, and an invoice clicked anywhere above the actions under it.
    Card customer = new Card(new Label("Office furniture, delivered every Monday.").setWrap(true))
            .setTitle("Harbor Lane Supplies")
            .setSubtitle("Customer since 2019")
            .setHeaderSuffix(new Label("$48,200").setStrong(true));
    Card invoice = new Card(new Label("Three items, due on Friday.").setWrap(true))
            .setTitle("Invoice 1042")
            .setSubtitle("Harbor Lane Supplies")
            .setFooter(new Button("Hold").setStyle(Button.Style.SECONDARY), new Button("Approve"));
    invoice.onActivate(() -> { }); // a click above the footer opens the invoice
    Column cards = new Column();
    cards.gap(12).crossAlignment(Flex.CrossAlignment.STRETCH);
    cards.add(customer);
    cards.add(invoice);
    return scene(new SizedBox(340, SizedBox.UNSET, cards));
}
Badge
Badge

Badge

コード
static Built badge() {
    // A status in each tone, a count past what it shows, and a dot beside a name.
    Row tones = new Row();
    tones.gap(8).crossAlignment(Flex.CrossAlignment.CENTER);
    tones.add(new limn.components.Badge("Open").setTone(limn.components.Tone.INFO));
    tones.add(new limn.components.Badge("Paid").setTone(limn.components.Tone.SUCCESS));
    tones.add(new limn.components.Badge("Due soon").setTone(limn.components.Tone.WARNING));
    tones.add(new limn.components.Badge("Overdue").setTone(limn.components.Tone.DANGER));
    Row counts = new Row();
    counts.gap(8).crossAlignment(Flex.CrossAlignment.CENTER);
    counts.add(new Label("Inbox"));
    counts.add(limn.components.Badge.ofCount(3).setTone(limn.components.Tone.ACCENT));
    counts.add(new Label("Alerts"));
    counts.add(limn.components.Badge.ofCount(128).setTone(limn.components.Tone.DANGER));
    counts.add(new Label("Ana Lima"));
    counts.add(limn.components.Badge.dot(limn.i18n.I18nString.literal("Unread"))
            .setTone(limn.components.Tone.ACCENT));
    Column column = new Column();
    column.gap(12);
    column.add(tones);
    column.add(counts);
    return scene(column);
}
Avatar and AvatarGroup
Avatar and AvatarGroup

Avatar and AvatarGroup

コード
static Built avatar() {
    // People as initials over a colour of their own, and approvers past the four drawn.
    Row people = new Row();
    people.gap(10).crossAlignment(Flex.CrossAlignment.CENTER);
    for (String name : List.of("Ana Lima", "Rui Costa", "Mei Chen")) {
        people.add(new limn.components.Avatar(name));
    }
    limn.components.AvatarGroup approvers = new limn.components.AvatarGroup();
    for (String name : List.of("Ana Lima", "Rui Costa", "Mei Chen", "Omar Haddad",
            "Lena Vogel", "Kofi Mensah")) {
        approvers.add(new limn.components.Avatar(name));
    }
    approvers.setAccessibleName("Approvers");
    people.add(new SizedBox(16, 1));
    people.add(approvers);
    return scene(people);
}
Popover
Popover

Popover

コード
static Built popover() {
    // IN_SCENE, as the dialog's: a popover in a window of its own is not in this window's
    // picture. The box leaves room under the button for the panel it opens.
    Button filter = new Button("Filter").setStyle(Button.Style.SECONDARY);
    Column panel = new Column();
    panel.gap(10);
    panel.add(new limn.components.Field("Customer contains", new TextField().setText("Kestrel")));
    panel.add(new Checkbox(Checkbox.Variant.BOX, "Overdue only"));
    limn.components.Popover popover = new limn.components.Popover(panel)
            .setDisplayMode(DisplayMode.IN_SCENE).setAccessibleName("Filter orders")
            .attachTo(filter);
    Column owner = new Column();
    owner.add(filter);
    Built built = scene(new SizedBox(300, 220, owner));
    popover.showFor(filter);
    return built;
}
Spinner
Spinner

Spinner

コード
static Built spinner() {
    Spinner spinner = new Spinner(0, 100, 1);
    spinner.setValue(42);
    return scene(spinner);
}
DateField
DateField

DateField

コード
static Built dateField() {
    // The segments, their order and the separators are the language's own: this same code
    // reads day/month/year in Portuguese and month/day/year in American English.
    DateField field = new DateField();
    field.setDate(LocalDate.of(2026, 9, 9));
    return scene(field);
}
DatePicker
DatePicker

DatePicker

コード
static Built datePicker() {
    DatePicker picker = new DatePicker();
    picker.setDate(LocalDate.of(2026, 9, 9));
    picker.setMinDate(LocalDate.of(2026, 1, 1));
    // Room for the calendar, for the combo box's reason above: this entry is filmed
    // opening it in the scene, and the site crops the still and the film to the one box
    // measured before any calendar exists. The card measured 248 by 268 points, opened
    // under the field's 32; as wide as the card, the field and the card share their edges.
    Column room = new Column();
    room.mainAlignment(Flex.MainAlignment.START).crossAlignment(Flex.CrossAlignment.STRETCH);
    room.add(picker);
    return scene(new SizedBox(248, 308, room));
}
CalendarView
CalendarView

CalendarView

コード
static Built calendarView() {
    CalendarView calendar = new CalendarView();
    calendar.setVisibleMonth(LocalDate.of(2026, 9, 9));
    calendar.setSelectionMode(CalendarView.SelectionMode.RANGE);
    calendar.setSelectedRange(new DateRange(LocalDate.of(2026, 9, 14),
            LocalDate.of(2026, 9, 25)));
    calendar.setShowWeekNumbers(true);
    calendar.setDateFilter(day -> day.getDayOfWeek() != DayOfWeek.SATURDAY
            && day.getDayOfWeek() != DayOfWeek.SUNDAY);
    return scene(calendar);
}
SplitPane
SplitPane

SplitPane

コード
static Built splitPane() {
    SplitPane split = SplitPane.horizontal(
            new Padding(limn.scene.Insets.all(12), new Label("Left")),
            new Padding(limn.scene.Insets.all(12), new Label("Right").setMuted(true)));
    split.setRatio(0.4f);
    return scene(new SizedBox(340, 130, split));
}
SegmentedControl
SegmentedControl

SegmentedControl

コード
static Built segmentedControl() {
    SegmentedControl control = new SegmentedControl(List.of("Day", "Week", "Month"));
    control.setSelectedIndex(1);
    return scene(control);
}
BarChart
BarChart

BarChart

コード
static Built barChart() {
    BarChart chart = BarChart.of(List.of("Q1", "Q2", "Q3", "Q4"),
            ChartSeries.of("Direct", 120, 145, 132, 168),
            ChartSeries.of("Partner", 80, 92, 105, 99));
    chart.setPreferredSize(380, 230);
    return scene(chart);
}
LineChart
LineChart

LineChart

コード
static Built lineChart() {
    LineChart chart = LineChart.of(List.of("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"),
            ChartSeries.of("p50", 24, 21, 26, 30, 28, 25, 27),
            ChartSeries.of("p99", 62, 58, 71, 88, 74, 66, 70));
    chart.setSmooth(true).setArea(true).setPreferredSize(380, 230);
    return scene(chart);
}
DonutChart
DonutChart

DonutChart

コード
static Built donutChart() {
    DonutChart chart = DonutChart.of(List.of("Direct", "Search", "Social", "Mail"),
            42, 31, 18, 9);
    // The hole is a slot, not a hole: whatever widget goes in it is laid out and drawn
    // like any other child, so a total, a label or a whole column can live there.
    Column centre = new Column();
    centre.gap(2).crossAlignment(Flex.CrossAlignment.CENTER);
    centre.add(new Label("100").setRole(Label.Role.TITLE));
    centre.add(new Label("sessions").setMuted(true));
    chart.setCenter(centre);
    chart.setPreferredSize(300, 230);
    return scene(chart);
}
ColorPicker
ColorPicker

ColorPicker

コード
static Built colorPicker() {
    ColorPicker picker = new ColorPicker();
    picker.setColor(limn.graphics.Color.rgb(0xAF7AFF));
    // Given no width to work with, each channel rail measures to an icon's width and
    // the sliders read as ticks. The box is what bounds it, and every rail then gets
    // the room left over on its line.
    return scene(new SizedBox(380, SizedBox.UNSET, picker));
}
ColorPickerButton
ColorPickerButton

ColorPickerButton

コード
static Built colorPickerButton() {
    // A chip and its hex, the picker in a dialog on a click. Every move in the picker is a
    // change, and so is Cancel, which puts the colour back: color() is always the answer.
    ColorPickerButton accent = new ColorPickerButton(Color.rgb(0xAF7AFF));
    accent.onChange(colour -> { });
    // A caption of the application's own, and none at all: a square chip for a dense column.
    ColorPickerButton highlight = new ColorPickerButton(Color.rgb(0xF59E0B)).setText("Highlight");
    ColorPickerButton chipOnly = new ColorPickerButton(Color.rgb(0x2BB673)).setText("");
    Row row = new Row();
    row.gap(12).crossAlignment(Flex.CrossAlignment.CENTER);
    row.add(accent);
    row.add(highlight);
    row.add(chipOnly);
    return scene(row);
}
Surface
Surface

Surface

コード
static Built surface() {
    // One surface at each level that casts a shadow, on the page's own canvas: what the
    // theme gives a card, a panel that covers and a dialog.
    Row row = new Row();
    row.gap(28).crossAlignment(Flex.CrossAlignment.CENTER);
    for (limn.components.Elevation level : List.of(limn.components.Elevation.RAISED,
            limn.components.Elevation.OVERLAY, limn.components.Elevation.MODAL)) {
        String name = level.name().charAt(0) + level.name().substring(1).toLowerCase(java.util.Locale.ROOT);
        row.add(new limn.components.Surface(
                new limn.scene.layout.Padding(limn.scene.Insets.symmetric(18, 22), new Label(name)))
                .setElevation(level));
    }
    // Room around the row for the widest shadow, which is painted outside its surface.
    return scene(new limn.scene.layout.Padding(limn.scene.Insets.all(40), row));
}
BackdropPanel
BackdropPanel

BackdropPanel

コード
static Built backdropPanel() {
    // A checkerboard, because the effect this panel exists for is refraction at the
    // rim, and a straight line bending as it passes under the edge is the only backdrop
    // that shows it. It scrolls diagonally, because a rim that bends a STILL grid is a
    // picture of distortion and a rim that bends a moving one is the effect happening.
    Theme theme = Theme.current();
    Widget<?> board = new Board() {
        private float offset;

        @Override
        protected limn.scene.Size onMeasure(limn.scene.Constraints constraints) {
            return new limn.scene.Size(constraints.maxWidth(), constraints.maxHeight());
        }

        @Override
        protected void onPaint(limn.graphics.Canvas canvas) {
            // Clipped to its own box: the squares are drawn from a cell outside each
            // edge so one sliding in is whole when it crosses, and without a clip that
            // overspill is what the picture shows: a board whose own edge walks
            // diagonally out of the frame.
            canvas.save();
            canvas.clipRect(0, 0, width(), height());
            float cell = 10;
            // Drawn from one cell outside the box on each side, so a square sliding in
            // is already whole when it crosses the edge.
            float shift = offset % (cell * 2);
            // From TWO cells outside, not one: the shift runs up to two cells, so a
            // single spare row leaves the top of the board unpainted once per cycle,
            // a seam that slides down the picture every time the pattern wraps.
            for (int row = -2; row * cell < height() + cell * 2; row++) {
                for (int col = -2; col * cell < width() + cell * 2; col++) {
                    if (((row + col) & 1) != 0) {
                        continue;
                    }
                    canvas.fillRect(col * cell + shift, row * cell + shift,
                            cell, cell, theme.primary());
                }
            }
            canvas.restore();
        }

        @Override
        protected void onAttached() {
            // 45°: x and y advance together, one point per frame at the capture's step.
            scene().addTicker(dt -> {
                offset += (float) (dt * 12.5);
                invalidate();
                return true;
            });
        }
    };

    // Clear, not Wash: this is the variant that displaces the backdrop at the rim
    // instead of recolouring it flat. The tint is the page's own canvas at a little
    // under half: enough that the label keeps its contrast over either square, little
    // enough that the grid still visibly bends through it.
    BackdropPanel panel = new BackdropPanel(
            new limn.graphics.BackdropEffect.Clear(theme.background().withAlpha(0.45f), 12f, 0.45f),
            limn.scene.Insets.symmetric(20, 44),
            new Label("Clear glass"));
    panel.setCornerRadius(18);

    Stack stack = new Stack().alignment(Stack.Alignment.CENTER);
    stack.add(board);
    stack.add(panel);
    return scene(new SizedBox(340, 120, stack));
}
VideoView
VideoView

VideoView

コード
static Built videoView() {
    // The PURE-JAVA source, deliberately: the toolkit's synthetic generator needs no
    // native, no third-party codec and no media file, so this renders identically on
    // any machine and on a CI runner that has never built FFmpeg. The still therefore
    // shows the WIDGET working, not a codec; the caption on the site says so, because
    // a frame of H.264 is not what this picture is evidence of.
    SyntheticSpec spec = SyntheticSpec.of(400, 226)
            .withPattern(SyntheticPattern.BARS)
            .withFrameCount(1);
    // FILL, and a box the size of the frame: the default CONTAIN letterboxes inside its
    // box whenever the two aspects differ at all, and a transport aligned to the box then
    // hangs over the edge of the picture it is supposed to be sitting on.
    VideoView view = new VideoView(SyntheticVideoDecoder.open(spec));
    view.setFit(VideoView.Fit.FILL).setPreferredSize(400, 226);

    // The transport is NOT part of the widget: it is ordinary controls under the picture.
    // CENTER rather than STRETCH, and a transport narrower than the frame: a video view
    // sizes itself from the stream it is showing, so the two widths are never a number
    // this file gets to pick, and centring is what stays right when they differ.
    Column player = new Column();
    player.gap(4).crossAlignment(Flex.CrossAlignment.CENTER);
    player.add(view);
    player.add(new SizedBox(320, SizedBox.UNSET, transport()));
    return scene(player);
}

/**
 * A play/scrub/volume bar for the picture above it: a Button, two Sliders and a Label,
 * and nothing the video widget itself provides. One size step down is what keeps a
 * transport reading as chrome rather than as content.
 */
private static Widget<?> transport() {
    Slider scrub = new Slider(0, 100);
    scrub.setValue(38);
    Slider volume = new Slider(0, 100);
    volume.setValue(70);

    Row row = new Row();
    row.gap(8).crossAlignment(Flex.CrossAlignment.CENTER);
    row.add(new Button("Pause").setStyle(Button.Style.SECONDARY));
    row.add(limn.scene.layout.Expanded.of(scrub));
    row.add(new Label("0:24 / 1:03").setMuted(true));
    row.add(new SizedBox(52, SizedBox.UNSET, volume));

    Padding bar = new Padding(limn.scene.Insets.symmetric(10, 2), row);
    bar.setControlSize(limn.scene.ControlSize.SMALL);
    return bar;
}

動画ビューは純 Java のテスト用ソースを使っているので、コーデックの対応範囲ではなくウィジェットの動作を示しています。この画像にネイティブデコーダーは関与していません。

MediaControls
MediaControls

MediaControls

コード
static Built mediaControls() {
    // Real H.264 here, where the entry above is deliberately synthetic. A transport is a
    // control over a film: a scrub that lands somewhere is the claim this picture makes, and
    // colour bars that look the same at every position cannot carry it. The clip is the
    // ten-second Big Buck Bunny excerpt the build already copies into this jar for the demo's
    // video screen (ADR 027) -- (c) 2008 Blender Foundation, CC BY 3.0, credited under the
    // pictures on the page that publishes them.
    Videos.installDecoder(new FfmpegVideoDecoder());
    String missing = FfmpegVideoDecoder.unavailableReason();
    if (missing != null) {
        // Loudly, and before anything is drawn: a capture that publishes a black rectangle
        // satisfies the manifest and tells the reader the widget is broken.
        throw new IllegalStateException("the MediaControls entry films real H.264 and the "
                + "FFmpeg decoder will not load: " + missing);
    }
    VideoView view = new VideoView(Videos.open(BundledClip.path()));
    // Smaller than the video view's own entry, and the reason is the film rather than the
    // layout: every frame of moving footage is a photograph the published animation has to
    // carry, and at 400 points wide this one came to 1.5 MB against the 400 kB a component
    // film is allowed (build-gallery.mjs). The bar is the widget on show; the picture only
    // has to be big enough to see the scrub land in it.
    view.setFit(VideoView.Fit.FILL).setPreferredSize(320, 180);

    // The bar is as wide as the picture it drives: a transport narrower than its film reads
    // as a control that belongs to something else.
    Column player = new Column();
    player.gap(6).crossAlignment(Flex.CrossAlignment.CENTER);
    player.add(view);
    player.add(new SizedBox(320, SizedBox.UNSET, new MediaControls(view)));
    return scene(player);
}

この再生コントロールの下の映像は Big Buck Bunny(© 2008 Blender Foundation)で、CC BY 3.0 のもとで使用しています。このページのほかの画像はすべてツールキット自身が描いたものです。

Viewport3D
Viewport3D

Viewport3D

コード
static Built viewport3d() {
    // The floor runs well past the frame: a plane that ends inside it puts a hard edge
    // across the picture and the scene reads as a tabletop rather than a ground.
    MeshData ground = Primitives.plane(30, 30);
    MeshData cube = Primitives.cube(1.6f);
    MeshData ball = Primitives.sphere(0.95f, 40, 60);
    Scene3D[] built = {null};

    Viewport3D viewport = new Viewport3D().setPreferredSize(320, 200);
    viewport.camera().eye(new Vec3(3.1f, 2.5f, 5.0f)).target(new Vec3(0.1f, 0.1f, 0));
    viewport.setController(new OrbitController(viewport.camera()));
    viewport.setRenderer((pass, seconds) -> {
        if (built[0] == null) {
            built[0] = pbrScene(ground, cube, ball);
        }
        float aspect = viewport.height() > 0 ? viewport.width() / viewport.height() : 1f;
        built[0].render(pass, viewport.camera(), aspect);
    });
    // Nothing in the render depends on the clock, so asking for a frame per tick still
    // produces identical pixels. It is also what stops the FIRST 3D frame in a window,
    // which comes out empty, from being the one the capture keeps.
    viewport.setRenderScale(0.5f);
    viewport.setAnimated(true);
    viewport.observeDispose(() -> {
        if (built[0] != null) {
            built[0].dispose();
        }
    });
    return scene(viewport);
}

/** A checkered floor, a metal cube and a glossy sphere, under two lights. */
private static Scene3D pbrScene(MeshData ground, MeshData cube, MeshData ball) {
    Scene3D scene = new Scene3D()
            .background(new Vec4(0.06f, 0.05f, 0.09f, 1f))
            .ambient(new Vec3(0.05f, 0.05f, 0.07f))
            .exposure(1.15f)
            .castShadows(true);

    GpuTexture floor = Graphics3D.uploadTexture(
            checker(256, 8, 46, 44, 58, 188, 186, 200), Sampler.smooth());
    scene.root().add(new MeshNode(Graphics3D.upload(ground),
            Material.Pbr.of(1f, 1f, 1f).roughness(0.8f).textured(floor))
            .transform(Transform3D.at(new Vec3(0, -1f, 0))));
    // A dielectric, not a metal: there is no environment map in this scene, so a metal
    // has nothing to reflect and renders as a flat dark patch.
    scene.root().add(new MeshNode(Graphics3D.upload(cube),
            Material.Pbr.of(0.45f, 0.16f, 0.92f).metallic(0.1f).roughness(0.35f))
            .transform(new Transform3D(new Vec3(-1.35f, -0.2f, -0.1f),
                    Quat.fromAxisAngle(Vec3.UNIT_Y, 0.55f), Vec3.ONE)));
    scene.root().add(new MeshNode(Graphics3D.upload(ball),
            Material.Pbr.of(0.93f, 0.94f, 0.99f).metallic(0.2f).roughness(0.16f))
            .transform(Transform3D.at(new Vec3(1.3f, -0.05f, 0.45f))));

    scene.root().add(new LightNode(new Light.Directional(
            new Vec3(0.5f, 1.2f, 0.55f), new Vec3(1f, 0.97f, 0.92f), 3.4f)));
    scene.root().add(new LightNode(new Light.Point(
            new Vec3(-3.2f, 1.8f, 2.8f), new Vec3(0.6f, 0.45f, 1f), 14f, 14f)));
    return scene;
}

/** A checkerboard as pixels; the capture must not depend on an asset on disk. */
private static TextureData checker(int size, int cells,
                                   int ar, int ag, int ab, int br, int bg, int bb) {
    byte[] pixels = new byte[size * size * 4];
    int cell = Math.max(1, size / cells);
    for (int y = 0; y < size; y++) {
        for (int x = 0; x < size; x++) {
            boolean first = ((x / cell) + (y / cell)) % 2 == 0;
            int i = (y * size + x) * 4;
            pixels[i] = (byte) (first ? ar : br);
            pixels[i + 1] = (byte) (first ? ag : bg);
            pixels[i + 2] = (byte) (first ? ab : bb);
            pixels[i + 3] = (byte) 255;
        }
    }
    return new TextureData(size, size, pixels, ColorSpace.SRGB);
}