Theme files
Families, chromas, variants and the data files a theme is made of.
A theme is a family (the shapes: frames, bevels, buttons), a chroma (the colours) and a variant
(light, dark, glass or auto). Any chroma dresses any family. Both are data files under
assets/<namespace>/minuit/ in a mod jar or a resource pack, reloaded with F3+T. The kit ships the
families vanilla, bedrock, slate, bubbly and soft.
glass is a mode every chroma has, derived from its dark half (else its light one) by GlassPalette: surfaces,
sections and button faces become one smoke, #0B0D10 at 0.6, the accent roles are muted and darkened until white text
reads 4.5:1 over a white backdrop, and every edge, bevel row and shadow keeps the half's own colour. The family draws
its own shapes through GlassFrame, which blends the layers of each pixel into one colour, so the smoke shows no
seams where a bevel row lies over a face. auto never picks glass. A config saved with the old glass family reads
as vanilla in the glass mode.
A chroma
assets/mypack/minuit/chromas/moss.json:
{"order": 0,
"variants": {
"light": {"surface": "#FFEBE3F7", "text": "#FF271D3B", "primary": "#FF6236C4", "...": "..."},
"dark": {"surface": "#FF1E1830", "text": "#FFF1EBFF", "primary": "#FFA98BFF", "...": "..."}}}Each variant lists every Palette role, as #RRGGBB or #AARRGGBB. Two roles are optional: link, a link's
label in text, takes the accent when left out, and backdrop, the page behind panels where a host draws one (the
bench, the site), is none when left out. Copy the kit's
core/src/main/resources/assets/minuit/minuit/chromas/lilac.json and change the values. A chroma
may ship one variant. The name on the theme page is the lang key minuit.chroma.moss.
Roles
The roles are the theming contract: a widget asks for a role, a chroma sets its colour.
Generated by tools/palette_roles.py from Palette.java, the chromas and the code that reads each role. Edit the @param line in Palette.java, then run the script.
| Role | What it colours | Bedrock light | Bedrock dark | Read by | Contrast rule |
|---|---|---|---|---|---|
frame |
the outer frame: leather, slate | #FFDADBDF |
#FF48494A |
FieldInk, FrameStyles, Slider, SurfaceAnchor | - |
frameDark |
its shadowed edge | #FF1E1E1F |
#FF1E1E1F |
FieldInk, FrameStyle, FrameStyles, Paper, SurfaceAnchor, Window | - |
frameLight |
its lit edge, top left | #FFFFFFFF |
#FF5A5B5C |
FieldInk, FrameStyles | - |
trim |
the detail on the frame: stitches, a shine line | #FFB2B3B5 |
#FF313233 |
FieldInk, FrameStyles | - |
surface |
the panel's inside: paper, dark amethyst | #FFDADBDF |
#FF48494A |
Accordion, Alert, Breadcrumb, Calendar, Card, Cart, +37 more | - |
surfaceEdge |
the line around the inside | #FFB2B3B5 |
#FF5A5B5C |
Accordion, Card, Checkout, ColorPicker, FieldInk, FrameStyles, +5 more | - |
section |
a card on the surface | #FFDADBDF |
#FF48494A |
FieldInk, FrameStyles, Menubar, Table | - |
well |
a sunken area: a field, a list, a meter | #FFE7E8EC |
#FF313233 |
AsyncImage, Balance, Carousel, Cart, Chart, ColorPicker, +17 more | - |
wellEdge |
the frame of a well | #FF1E1E1F |
#FF1E1E1F |
AsyncImage, Button, ColorPicker, Dropdown, FieldInk, FormLook, +13 more | - |
text |
body text | #FF1E1E1F |
#FFFFFFFF |
Accordion, AvatarGroup, Badge, Button, Calendar, Card, +50 more | 4.5:1 on its face; Ink.on picks it or flips to the far ink |
textSoft |
labels and secondary text | #FF48494A |
#FFD0D1D4 |
Alert, AsyncImage, Dialog, DonutChart, Dropdown, FieldInk, +16 more | 4.5:1 on its face, else Ink.soft falls back to Ink.on |
textFaint |
hints, timestamps | #FF6E6F72 |
#FF8C8D90 |
AsyncImage, ContextMenu, Dropdown, FieldInk, FrameStyles, ItemPicker, +5 more | - |
raised |
a button at rest | #FFD0D1D4 |
#FFD0D1D4 |
Button, Calendar, Dropdown, EntityPreview, FieldInk, FormLook, +14 more | - |
raisedHover |
a button under the mouse | #FFF4F6F9 |
#FFF4F6F9 |
Button, Calendar, Dropdown, EntityPreview, FieldInk, FrameStyle, +12 more | - |
raisedText |
text on a button | #FF1E1E1F |
#FF1E1E1F |
Button, Dropdown, FieldInk, FrameStyle, FrameStyles, Radial, +10 more | - |
primary |
the one action a screen exists for | #FF3C8527 |
#FF3C8527 |
Avatar, Badge, Button, Calendar, Card, Carousel, +32 more | Ink.readable raises it to 3:1 as a mark or 4.5:1 as text where it is drawn as one |
primaryHover |
the same under the mouse | #FF4A9634 |
#FF4A9634 |
Button, Calendar, ComboBox, Dropdown, FieldInk, FrameStyle, +4 more | - |
primaryText |
text on it | #FFFFFFFF |
#FFFFFFFF |
Button, Calendar, ComboBox, CommandPalette, ContextMenu, Dropdown, +13 more | - |
accent |
toggles, sliders, the level meter | #FF3C8527 |
#FF3C8527 |
Alert, Avatar, Calendar, Carousel, ComboBox, CommandPalette, +25 more | Ink.readable raises it to 3:1 as a mark or 4.5:1 as text where it is drawn as one |
accentDeep |
the same, darker | #FF1C4E11 |
#FF1C4E11 |
Avatar, FieldInk, FrameStyles, Rating, SeriesColors, Toggle | - |
good |
done, heard, ready | #FF2E7D1E |
#FF6CC349 |
Alert, Avatar, Badge, BingoBoard, FieldInk, Gauge, +9 more | Ink.readable raises it to 4.5:1 as text where it is drawn as one |
warn |
something to look at, nothing broken | #FF8A5F00 |
#FFE8C453 |
Alert, Avatar, Badge, Countdown, FieldInk, Gauge, +8 more | Ink.readable raises it to 4.5:1 as text where it is drawn as one |
bad |
broken, refused | #FFAA2A2A |
#FFE05A5A |
Alert, Avatar, Badge, Balance, Cart, Checkout, +19 more | Ink.readable raises it to 3:1 as a mark or 4.5:1 as text where it is drawn as one |
destructive |
a button that deletes or resets, with its hover and text colours | #FFCA3636 |
#FFCA3636 |
Avatar, Badge, Button, FieldInk, FrameStyles, ReorderList, +2 more | - |
destructiveHover |
the same under the mouse | #FFD64A4A |
#FFD64A4A |
Button, FieldInk, FrameStyles, ReorderList, Steps | - |
destructiveText |
text on it | #FFFFFFFF |
#FFFFFFFF |
Button, FieldInk, ReorderList, Steps | - |
track |
the empty part of a bar | #FF8C8D90 |
#FF8C8D90 |
FieldInk, FrameStyle, FrameStyles, ItemPicker, Rating | - |
disabled |
a control that cannot be used | #FFD9DADD |
#FFD9DADD |
Button, Countdown, Dropdown, FieldInk, FrameStyle, FrameStyles, +6 more | - |
disabledText |
its text | #FF8C8D90 |
#FF8C8D90 |
Accordion, Alert, Avatar, AvatarGroup, Badge, Balance, +52 more | raised to 3:1 on its face (Ink.disabled); Ink.readable raises it to 3:1 as a mark where it is drawn as one |
shadow |
drop shadows | #40000000 |
#66000000 |
ColorPicker, CommandPalette, Dialog, Drawer, EntityPreview, FieldInk, +8 more | - |
link |
a link's label in text; 0 takes the accent (linkInk) |
#FF3366BB |
#FF90C0FE |
FieldInk | - |
backdrop |
the page behind panels where the host draws one (the site, the bench); 0 for none, the game's own background | unset | unset | FieldInk | - |
A family
assets/mypack/minuit/themes/moss/family.json:
{"style": "slate", "order": 80, "chromas": ["moss", "teal"],
"override": {"light": {"surface": "#99"}}}style names a drawing style from FrameStyles (bedrock, slate, leather, soft, bubbly,
vanilla); a new shape needs Java there. A style declares how far a held button's label moves (pressTravel,
pressShiftX, 1 and 1 by default) and how far a chosen option's label sits lower (selectedTravel, 0 by default);
bedrock sinks both 3 px straight down, into its 3 px lip (2 px on parts under 12 px tall), over 100 ms (pressMs, 0 by default: at once). chromas lists the recommended ones, the first is the default.
override changes roles in every chroma the family draws, per light, dark or glass: "#AA" sets only the alpha. The name is
the lang key minuit.family.moss. A file that does not parse is skipped with one log line naming
the file and the field.
A family from images
A family can draw parts from PNGs cut in nine, so a new shape needs no Java. family.json keeps style (it draws every
part with no PNG) and adds sprites:
{"style": "bedrock", "order": 15, "chromas": ["bedrock"],
"sprites": {"folder": "sprites", "scale": 1,
"parts": {"panel": {"inset": {"top": 2, "left": 1, "right": 1, "bottom": 1}},
"button": {"inset": 2, "tint": {"map": ["frame_dark", "face", "frame_light"]}},
"button.disabled": {"tint": "none"}}}}Each listed sprite is themes/<family>/<folder>/<name>.png. Parts: panel, header, section, well,
well_focused, field, field_focused, button, button_pressed, track, knob, knob_lit, slot, popup.
button, button_pressed and track take a role variant, button.raised_hover, drawn when the widget's colour is that
role (snake case). A variant takes the fields it leaves out from its part. inset is the border kept whole on each
side, in GUI pixels; outset how far the sprite reaches past the box (a shadow); fill is tile (default, the game's
nine-slice) or stretch; tint is "none" (the PNG's colours), {"multiply": role} or {"map": [role, ...]} (the
grey of each pixel runs along the roles, dark to light); face is the widget's own colour. scale: 2 reads art at two
texels per GUI pixel. python build.py imgfamily <family> <chroma> light <dir> in componentbench exports a code
family to such files and checks them pixel by pixel.
Until a renderer installs its image source (the bench and the tests do; the game ports come next), a family from
images draws as its style.
Chart series colours
A chart, donut or wheel colours its series from a SeriesPalette, set per widget with
seriesPalette(...). THEME, the default, follows the theme, and a chroma variant may list its own
with "series": ["#FF3E8E1C", "..."]. SHADES follows the theme's accent. CONTRAST, VIVID and
PASTEL are fixed lists, and code can pass its own with SeriesPalette.of(...). A mod or pack adds a
named one at assets/mypack/minuit/palettes/autumn.json:
{"colors": ["#C0392B", "#E67E22", "#F1C40F", "#27AE60"]}Code picks it with SeriesPalette.byId("mypack:autumn"). After the last colour the list starts
again.
The player's choice
config/minuit-client.json:
{"global": {"family": "slate", "chroma": "teal", "variant": "auto"},
"mods": {"mymod": {"family": "leather", "chroma": "paper", "variant": "light", "accent": "midnight"}}}A mod with no entry wears global; with no global, the theme it registered. auto takes the dark
variant when the chroma has one. accent replaces only the accent colours; the ids are blue,
green, orange, rose, violet and midnight. A pack adds one with assets/mypack/minuit/accents/moss.json,
{"order": 70, "light": "#FF4D7C0F", "dark": "#FF84CC16"} (order optional); the picker shows it after the
built-ins, tooltip from the lang key minuit.picker.accent.moss. The player edits all this on the theme page.
A mod's own look
Themes.register(Themes.defaultTheme().as("mymod")); // follow the kit's default (Vanilla, light)
Themes.register(Themes.LEATHER.withIcon(Icon.CLOSE, myArt).as("mymod")); // a preset with your icon
Theme screenTheme = Themes.byId("mymod"); // what your screens open with
new ThemeScreen(parentScreen, "mymod"); // the theme page, for your modRegister at client init. The registered theme is the default until the player picks one. A family
that only your theme page offers is a family.json in your own namespace with "private": true.
Layout and pixel rules
Rules for anyone who writes a component or a family. Each one lives in one shared place; a component calls it rather than drawing its own version.
R2. Nested content keeps 1 px of face inside the frame. A popup's rows, the ring around its lit row and its
scroll bar start at the frame's inner edge (panelInsets, per side) plus 2: one pixel of face, one for the ring
(PopupList.inset). Cases checked: vanilla insets 3 on every side, rows now at 5 (the keyboard ring used to sit
on the light bevel); bubbly 3, 3, 3, 4, rows at 5 left, top and right as before, 6 at the bottom (the ring sat on
the last face row); slate 3, rows at 5; bedrock 2 on every side, rows at 4; soft 2, rows at 4 (its face starts at 1, so it keeps 2 px). Users: ContextMenu, ComboBox, TagInput,
CommandPalette, DatePicker, and the 1.0 DropdownComponent (listHeight(frame, lines); the old listHeight(lines) answers
for vanilla).
R3. A cut over a pixel picture falls on a whole source pixel. AvatarGroup steps so that what the next head
leaves of a skin face ends on a face-pixel edge of the 8 px face drawn at the head's side (faceEdge, sampled at
pixel centres as the game does), taking the first edge at or past a third of overlap. A 20 px head steps 16:
15 px of face, six whole face pixels.
R4. A ring around a rounded tile reads whole. A head under another one ends in a straight cut on the next ring, so the ring runs straight from top to bottom and then follows the next tile's corner, with no sliver of the head beneath showing past the corner (AvatarGroup, every family; bubbly and soft show it most).
R5. A ring shares the corner of what it surrounds. The panel's focus ring is one 1 px line 2 px out, of
radius focusCorner(frame) + 2. A button answers the family's buttonCorner (vanilla and bedrock 0,
the others 3), so the vanilla ring around a square button is square. A chosen card's 2 px ring is
panelEdge, which follows the panel's own outline: vanilla's stepped corners, the rounded corner elsewhere.
Dialog rings its buttons through its inner panel, so it gets the same ring. The ring goes round focusBox(frame),
which a button and the Dropdown grow by the family's bleed: bedrock's ring closes under its 3 px lip (bleed 3), not across it.
R6. A scroll bar sits inside the content column. Its drawn track's outer edge lies on the right edge of the
content above it (ScrollBar.x(frame, right), with each family's scrollerGap: 2 px of blank in the default
3 px track, 0 for vanilla's 6 px sprite), and it spans the same top and bottom insets as the rows. Cases checked:
CommandPalette, ComboBox and ContextMenu in vanilla and bubbly, where the track now ends under the field's right
edge. In a well or a field (List, Table, Tree, TextArea) the column ends one pixel of face inside the 1 px border
(ScrollBar.wellX, rows give up wellRoom); a Kanban column ends at its card margin; Accordion, Steps and the Donut
legend have no frame, so the track ends on the box's own right edge.
R7. A variant draws like its 1.0 sibling. RangeSlider's game look is the slider's bar, handles and label, with no fill of its own; the label takes the stretch between the handles, else the wider one outside them, and hides when neither holds it, so it never sits under a handle. RadioGroup draws like the Checkbox: in vanilla the round mark takes checkbox.png's outline (white when lit), dark well and white dot. NumberField already matches TextField: the family's field, its text and hint inks, the library field doing the editing.
R8. Smooth mode is the pixel shape at screen resolution, anti-aliased. smooth(true) samples the same
centre and radii as the pixel shape at one cell per screen pixel and gives each edge cell its coverage as alpha (6
levels, then 3), from the cell centre's distance to the edge; arc ends and zone cuts are radial lines with the same
coverage. One ladder serves every smooth shape (Smooth.ladder); a budget of rectangles per unit of GUI scale
decides when it takes fewer levels, then coarser cells. Cases checked: Gauge (bench 86 x 70, the stair-stepped
inner edge, the jagged fill end and the blob at the red cap gone), Chart lines and areas, Sparkline. Scene3D and
ModelPreview already anti-alias their edges; Donut and Wheel have no smooth mode.
R9. Arrows are odd and round shapes may be. An arrow, caret or pointer is Shapes.arrow: row i from the tip
is 2i + 1 px wide, so the tip is one centred pixel and both sides step alike. A disc of side cells is centred
at side / 2, on a pixel corner when even, on a pixel centre when odd (PixelArt.span(r, k, odd)); the parity is
chosen per shape for the cleanest result. Cases checked: the Wheel is odd (diameter 2 * radius + 1), so its
pointer's tip, the line under it and the hub share one pixel column, and the wheel mirrors exactly about it;
popover arrow 1-3-5, Accordion 7-5-3-1, SplitPane 1-3-5, the 9 px CARET, NEXT and PREV glyphs (ComboBox,
Dropdown, Tree, Breadcrumb, ContextMenu, Carousel) and TreeTable's arrows already met the rule. Gauge and Donut
stay even, their text centred on the disc box.
Chart baseline. The zero line stays 1 px, as the grid lines and the series lines are: it stands out by a stronger ink (alpha 0x70 against 0x24), and a 2 px line would sit half under the bars it carries.
Tooltips that would hide what they name. A widget can return tooltipY to lay its tip under itself;
AvatarGroup does, so a name never covers the heads beside it.