Deutsch
GitHub
Design

Der Satz

Komponenten

Jedes Bild hier wurde während dieses Builds vom Toolkit gerendert, und jeder Ausschnitt ist der Code, der das Bild daneben erzeugt hat.

Button
Button

Button

Code
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").setSecondary(true));
    row.add(disabled);
    return scene(row);
}
Label
Label

Label

Code
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

Code
static Built textField() {
    Column column = new Column();
    column.gap(12);
    column.add(new TextField().setText("Typed text"));
    column.add(new TextField().setPlaceholder("Placeholder"));
    return scene(column);
}
Checkbox and switch
Checkbox and switch

Checkbox and switch

Code
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

Code
static Built comboBox() {
    ComboBox combo = new ComboBox(List.of("Limn", "Limn Light", "Nord", "Dracula"));
    combo.setSelectedIndex(0);
    return scene(new SizedBox(260, SizedBox.UNSET, combo));
}
ListView
ListView

ListView

Code
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.
    ListView list = new ListView(new ListView.Adapter() {
        @Override
        public int rowCount() {
            return 1000;
        }

        @Override
        public Widget rowAt(int index) {
            Label row = new Label("Row " + (index + 1));
            return new Padding(limn.scene.Insets.symmetric(8, 12), row);
        }
    });
    return scene(new SizedBox(320, 130, list));
}
ScrollView and ScrollBar
ScrollView and ScrollBar

ScrollView and ScrollBar

Code
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

Code
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)
            .addButton("Cancel", "cancel")
            .addPrimaryButton("Discard", "discard")
            .show(built.scene());
    return built;
}
ToolBar
ToolBar

ToolBar

Code
static Built toolBar() {
    ToolBar bar = new ToolBar();
    bar.addItem(new Button("New"));
    bar.addItem(new Button("Open").setSecondary(true));
    bar.addSeparator();
    bar.addItem(new Button("Save").setSecondary(true));
    return scene(bar);
}
TabbedPane
TabbedPane

TabbedPane

Code
static Built tabbedPane() {
    TabbedPane tabs = new TabbedPane();
    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

Code
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

Code
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

Code
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

Code
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)

Code
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

Code
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);
    return scene(view);
}
SearchField
SearchField

SearchField

Code
static Built searchField() {
    return scene(new SearchField());
}
PasswordField
PasswordField

PasswordField

Code
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

Code
static Built slider() {
    Slider slider = new Slider(0, 100);
    slider.setValue(65);
    return scene(new SizedBox(280, SizedBox.UNSET, slider));
}
Spinner
Spinner

Spinner

Code
static Built spinner() {
    Spinner spinner = new Spinner(0, 100, 1);
    spinner.setValue(42);
    return scene(spinner);
}
SplitPane
SplitPane

SplitPane

Code
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

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

BarChart

Code
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

Code
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

Code
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

Code
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));
}
BackdropPanel
BackdropPanel

BackdropPanel

Code
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 Widget() {
        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

Code
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").setSecondary(true));
    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;
}

Die Video-Ansicht nutzt die reine Java-Testquelle, zeigt also das Widget in Betrieb und nicht die Codec-Abdeckung. An diesem Bild ist kein nativer Decoder beteiligt.

Viewport3D
Viewport3D

Viewport3D

Code
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.onDispose(() -> {
        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);
}