Español
GitHub
Tema

El conjunto

Componentes

Cada imagen de aquí la renderizó el kit durante esta compilación, y cada fragmento es el código que produjo la imagen de al lado.

Button
Button

Button

Código
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

Código
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

Código
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

Código
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

Código
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

Código
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

Código
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

Código
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

Código
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

Código
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

Código
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

Código
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

Código
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

Código
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)

Código
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

Código
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

Código
static Built searchField() {
    return scene(new SearchField());
}
PasswordField
PasswordField

PasswordField

Código
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

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

Spinner

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

SplitPane

Código
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

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

BarChart

Código
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

Código
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

Código
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

Código
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

Código
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

Código
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;
}

La vista de vídeo usa la fuente de prueba en Java puro, así que muestra el widget funcionando y no la cobertura de códecs. En esta imagen no interviene ningún decodificador nativo.

Viewport3D
Viewport3D

Viewport3D

Código
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);
}