Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Canvas, images and QR codes

A canvas is the surface an application draws on itself, an image shows a decoded picture, and a QR code is a payload drawn as modules of whole device pixels.

By the end of this chapter you can draw with paths, strokes, gradients and images on a canvas, hear the pointer and the keyboard on it, place a picture with one of four fit modes, and put a scannable code in a dialog.

The showcase's Canvas screen: a card of paths, strokes and a gradient, a card drawing one image four ways, a card of the same picture decoded from five formats, a card of the image widget at four fits, and three QR codes at three error-correction levels

The showcase's Canvas screen. The drawings are a canvas each, the four photographs on the right are image widgets, and the three codes are qr-code.

canvas

An immediate-mode drawing surface: a box whose painter is handed the frame, translated to the box’s content corner and clipped to it.

canvas id="plot" class="chart"
import dev.goldberry.widgets.core.canvas.Canvas;

new Canvas((frame, size) -> {
    frame.fillRect(0, 0, size.width(), size.height() / 2, 0xFF88C0D0);
}, Attributes.NONE.id("plot").classes("chart"));

Markup names no painter. A canvas node inflates to a styled, sized surface that draws nothing, because a painter is code and a document names things rather than building them. The painter is Java.

The painter

A Painter is paint(Frame frame, LogicalSize size). The frame’s origin is the canvas’s top-left and the clip is its content box, so a painter cannot escape its bounds. The toolkit brackets the call in save and restore, so a painter may set a clip or a transform and leave it set (ADR-0193). It runs on the UI thread inside the frame, so it must not block and must not keep the frame.

var hill = Path.builder()
        .moveTo(0, height)
        .lineTo(0, height * 0.55)
        .cubicTo(width * 0.25, height * 0.2, width * 0.45, height * 0.9, width * 0.6, height * 0.5)
        .lineTo(width, height)
        .close()
        .build();
frame.fillPath(hill, Gradient.fade(0, height * 0.2, 0, height, CssColor.fade(ink, 0.55)));
frame.strokePath(Path.circle(cx, cy, 14), Stroke.round(2), accent);
frame.strokePath(Path.line(0, y, width, y), Stroke.of(1).dashed(4, 4), muted);
frame.drawImage(logo, 16, 16);

Frame fills and strokes a Path, with a colour or a Gradient, draws an Image at its natural size or scaled and faded, clips with clipTo, and transforms with transform, concat and resetTransform. Path has a builder with moveTo, lineTo, quadTo, cubicTo, arcTo, close and append, and factories line, polyline, rect, roundRect, ellipse, circle and arc. A path is a value, so path.rotated(radians, cx, cy), translated, scaled and transformed(affine) make a turned copy, and frame.concat composes a transform onto the one the canvas already has (ADR-0390). A Stroke is of(width) or round(width), with cap, join, dashed(on, off) and dash(Dash). A dash is the toolkit’s own arithmetic (ADR-0278).

A three-parameter painter is a StyledPainter and is handed a CanvasStyle: the node’s resolved font and ink, this frame’s nowMillis, and reducedMotion. So canvas { color: var(--gb-text) } reaches the drawing (ADR-0288).

new Canvas((frame, size, style) -> {
    Paragraph.of(style.font(), "Revenue").paint(frame, 0, 0, size.width(), style.ink());
});

A canvas is painted once and left there. animating asks for the next frame for as long as a predicate over the same CanvasStyle says so (ADR-0348):

new Canvas(floor::paint).animating(style -> style.nowMillis() - mounted < SETTLE_MILLIS);

Input

An Input beside the painter makes the canvas hear the pointer and the keyboard. The events are the toolkit’s own PointerEvent and KeyEvent, and event.content() is measured from the corner the painter draws at (ADR-0281).

new Canvas(board::paint, new Input() {
    @Override
    public void onPointer(PointerEvent event) {
        var at = event.content();
        switch (event.kind()) {
            case PRESSED -> board.beginDrag(at.x(), at.y());
            case MOVED -> board.dragTo(at.x(), at.y());
            case WHEEL -> board.zoom(event.deltaY(), at.x(), at.y());
            default -> { }
        }
    }
});

A drag that leaves the canvas keeps reporting, because the router captures the pointer on press. event.consume() keeps a wheel from scrolling the pane the canvas sits in. onKey, onText and onPreedit arrive when the canvas has focus, and wantsText() turns the platform’s input method on for a canvas that holds an Editor. accessibleName() names the figure for a reader.

Attributes

AttributeTypeDefaultWhat it does
idstringnoneThe canvas’s id
classstringnoneClasses on its box

Styling

The CSS type is canvas. It is a box first: background, border, border-radius, padding, width, height and every layout property are the stylesheet’s, and the painter draws inside the padding. A canvas has no size of its own, so one in a row with nothing to size it is zero wide. cursor works as on any box. Its role is figure.

Keyboard

A canvas is focusable exactly when it has an Input whose focusable() is true. Keys reach onKey. A canvas with no input is not a Tab stop.

Read more

image

A picture, loaded off the frame on a virtual thread and drawn at its natural size until a stylesheet says otherwise.

column {
    image src="photos/harbour.jpg" alt="The harbour at dusk" fit="cover"
    image srcset="classpath:logo.png 1x, classpath:logo@2x.png 2x" alt="Goldberry"
    image src="classpath:divider.png" decorative=#true
}
import dev.goldberry.widgets.core.image.ImageView;
import dev.goldberry.widgets.core.image.ImageSource;
import dev.goldberry.widgets.core.image.Fit;

new ImageView(ImageSource.file(path), "The harbour at dusk").fit(Fit.COVER);
new ImageView(ImageSource.resource(App.class, "logo.png"), "Goldberry").variant(2, ImageSource.resource(App.class, "logo@2x.png"));
ImageView.decorative(ImageSource.resource(App.class, "divider.png"));

An ImageSource is a file(path), a resource(anchor, name), bytes(data), an Image already decoded with of(image), or supplied(key, supplier) for a thumbnail generator or an HTTP fetch run on a virtual thread. The shared loader decodes each source once per process, so ten thumbnails of one file decode once. While the pixels are on their way the box carries .loading. When they cannot be decoded it carries .error and shows Lucide’s image-off with the alt text (ADR-0358).

The record is ImageView. The Java name differs from the markup name because Image is the decoded value in dev.goldberry.image, which an application holds, pastes and encodes without a widget (ADR-0283). The format comes from the bytes: PNG, JPEG, QOI, WebP and GIF decode, and an animated GIF decodes to its first frame (ADR-0329).

Important

An image needs alt text or decorative=#true, and is refused when it has neither. A picture a reader is told is a figure and nothing more is worse than one they are not told about.

Fit modes

fitWhat is drawn
containThe whole image, as large as fits, letterboxed. The default
coverThe box filled, the image cropped to the box’s shape
fillThe box filled, the image stretched to its shape
noneThe image at its natural size, centred, cropped if larger

The image is always centred. cover crops rather than clips, so it needs no overflow: hidden and cannot paint over a neighbour.

Attributes

AttributeTypeDefaultWhat it does
srcstringnoneOne path. classpath: names a resource on the application’s class loader. Exactly one of src and srcset is required
srcsetstringnoneSeveral paths with scales: logo.png 1x, logo@2x.png 2x
altstring""What the picture shows, for a reader. Required unless decorative
decorativeboolean#falseThe picture shows nothing a reader needs, and leaves the semantics
fitcontain, cover, fill, nonecontainHow the picture fills a box of another shape. A misspelt value is refused
idstringnoneThe image’s id
classstringnoneClasses on its box

Styling

The CSS type is image. With no width and height the box is the picture’s natural size, one image pixel per device pixel, and max-width shrinks it in proportion. With one of them the other follows the picture’s shape. With both, fit decides. The classes loading and error are added while a load stands there, and image-alt is the part that shows the alt text on failure.

Keyboard

None. An image is not focusable.

Read more

An SVG does not decode. It is routed to a goldberry-vector module that does not exist, and shows the error state.

qr-code

A QR code for a payload, drawn in modules of whole device pixels so a phone camera reads it at any scale.

qr-code value="https://goldberry.dev" level="M" quiet-zone=4 name="Scan to open goldberry.dev"
import dev.goldberry.widgets.core.qrcode.QrCode;
import dev.goldberry.image.qr.Level;

new QrCode("https://goldberry.dev").level(Level.H).withAttributes(Attributes.NONE.name("Scan to open goldberry.dev"));
new QrCode(link, Level.L, 4, Attributes.NONE.id("qr-low"));

The encoder is the toolkit’s own, in dev.goldberry.image.qr beside the image codecs, and QrEncoder.encode(payload, level) returns the same QrMatrix with no widget (ADR-0494). The widget divides its box into a whole number of device pixels per module and turns the remainder into margin, so what changes between 100 % and 200 % is how many pixels a module is, never whether its edge lands on one. A box too small for one pixel per module draws nothing. A payload no version holds is refused when the widget is built. Rebuilding with the same payload does not re-encode.

The payload is not part of the accessible name, because a sign-in token is a credential. Give the widget a name that says what the code is for.

Attributes

AttributeTypeDefaultWhat it does
valuestring""The payload, encoded as UTF-8
levelL, M, Q, HMHow much of the code may be destroyed and still read. Anything else falls back to M
quiet-zonenumber4The light margin around the code, in modules. Four is the standard’s
namestringnoneWhat a reader is told the code is
idstringnoneThe code’s id
classstringnoneClasses on its box

Styling

The CSS type is qr-code. The default sheet makes it 160 by 160 with flex-shrink: 0, and an application sets width and height like anything else. The ink is --gb-qr-ink and the paper is --gb-qr-paper, and both are the same near-black on white in the dark theme as in the light one, because many scanners will not read an inverted code. The quiet zone is painted in the paper colour.

Keyboard

None. A code is a figure.

Read more