Brodgar client

UI scaling

One number decides how big the whole 2D interface is drawn. Everything the client lays out is written at a design size and multiplied by it once, so the source reads in the art’s own pixels and the screen gets device pixels.

The number is UI.scalef — private static double, filled LAZILY by UI.scalef() from UI.loadscale() on the first conversion that asks for it, under a double-checked synchronized(UI.class). It is read once and never changes for the life of the process, which is why the Options panel’s own slider is labelled as requiring a restart. Every converter below goes through scalef(), never the bare field.

Converting

Where What
Design → device UI.scale(double) · scale(float) · scale(int) · scale(Coord) · scale(int, int) · scale(Coord2d)
Device → design UI.unscale(double) · unscale(float) · unscale(int) · unscale(Coord)
Round a fraction UI.rscale(double) · rscale(double, double) — Math.round(v * scalef), for a factor that is not a whole design pixel
A font UI.scale(Font, float) → Font.deriveFont(scale(size)); see text and fonts
A texture UI.scale(Tex) → a ScaledTex wrapping it at UI.scale(tex.sz()) — a view, disposing the impl; scale(ScaledTex) answers the same object, so the call is idempotent and cannot double-scale. See render-gl.md’s blit path
An image raster PUtils.uiscale(BufferedImage, Coord) — the resampler everything above is built on

scale(int)/unscale(int) round through float; Coord.mul(double)/Coord.div(double) round each axis, which is what scale(Coord)/unscale(Coord) are.

Where the number comes from

Step Member
The factor in force UI.scalef(), via UI.loadscale()
An override, before any preference UI.uiscale, a Config.Variable over the haven.uiscale property
The stored preference Utils.getprefd("uiscale", defscale), clamped to [1.0, UI.maxscale()]
Where the user sets it OptWnd’s Interface panel — an HSlider whose changed() calls Utils.setprefd("uiscale", …) and nothing else
The display-derived bounds UI.initscale() fills maxscale (from the largest display’s resolution against 800×600, floored at 1.25) and defscale (from dev.density(), capped by maxscale); UI.maxscale() is the public reader

The preference and the factor are two different numbers. Utils.getprefd("uiscale", 1.0) — what the Options panel reads back — is 1.0 on a fresh install, while loadscale starts from the density-derived defscale and clamps; they agree only once the user has moved the slider, and only until the next launch.

Art is scaled when it is loaded, not when it is drawn

Resource.Image does the conversion once, at load:

Member What
sz the raw image’s own pixel size
scale the factor the resource declares (a scale key in the image layer, default 1) — the client’s button art declares 4
ssz round(UI.scale(sz / scale)) — the device size, and the size everything measures
scaled() the resampled BufferedImage at ssz; Resource.loadsimg(name) is loadrimg(name).scaled()
tex() a TexI over scaled() — already device-sized. rawtex() is the unscaled one
o / so the declared offset, and the same offset scaled

tex() and rawtex() are memoised on the layer; a TexI a SITE builds is not. A handful of statics wrap loadsimg in a TexI of their own rather than calling loadtex — Window.cm, so DefaultDeco.checkhit can sample TexI.back’s alpha raster, and HSlider.schain, which transposes the pixels first. Those are second objects over (or beside) the layer’s own, so anything keyed on the texture identity misses them and has to ask about TexI.back instead.

So a widget that measures its own art is already in device pixels, and scaling it again doubles it. The design size of a piece of art is sz / scale — a whole number the artist chose, and the same integer at every UI scale.

The scaled constants a layout meets

Where Member
A plain button’s height Button.hs (bl.getHeight()) and Button.hl (bm.getHeight()) — device, from the art above; Button.largep(int) picks between them by width, Button.margin is UI.scale(10)
A window’s chrome Window.dlmrgn / Window.dsmrgn, both UI.scaled; Window.DefaultDeco.iresize adds mrgn twice plus the deco’s own tlm/brm to the content size
A text field’s height TextEntry’s ctor — TextEntry.bgheight(), whose stock answer is mext.sz().y; toffx/wmarg (lcap + rcap + UI.scale(1)) are what its end caps take horizontally
A checkbox’s box CheckBox.sbox/lbox (with smark/lmark); the ctor’s height is max(box.sz().y, lbl.sz().y) once it has a caption, and settext recomputes it
A slider’s height HSlider’s ctor — sflarp.sz().y (the thumb); HSlider.resize(int) takes a width and keeps that height
A horizontal rule’s height HRuler’s ctor — (marg.y * 2) + 1, marg defaulting to (w / 10, UI.scale(2)). The one constant here that is not the same design number at every scale: it is built from a scaled term plus 1, not from art
A dropdown’s closed box SDropBox’s ctor — Coord.of(w, itemh), with the drop arrow (dropimg, via makedrop) added at 1.0, 0.5 on the right edge, so a taller arrow overhangs the row rather than growing it
Text Text.Foundry sizes pass through UI.scale(float) — see text and fonts

Gotchas

See also