Repository navigation
Hover overlays: draw Window elements over the hovered control instead of a fixed outline #112
Description
coreShaders.hover (#107) draws a 1px white rectangle around the hovered region. That is all it can draw: the shape is the region's rectangle, the color is fixed, and a slot showing an <Items> stack gets nothing. In a pack with rounded controls, icons that overhang their buttons, and rows whose tooltips are written at runtime, most controls end up with a hover that does not fit or with none.
This issue replaces the outline with hover overlays: any Window elements (frames, sprites, switches, bound text, bars, tints) drawn over a control while it is hovered, plus tooltips drawn by Window in the same way.
Decisions
- A hover is an element tree, not a fixed outline.
hovertakes children and they are drawn above the window while a slot of the region is hovered. - The overlay rides in the hitbox item's tooltip, pinned horizontally. The shader shifts it in x and rebuilds its y. Details below.
- Overlay controls do not use the vanilla tooltip. Window draws the tooltip as part of the overlay.
- Hover moves to
experimental(experimental: { hover: true }), next toanvilUpdates. It depends on vanilla's tooltip placement, the text shader, and pack formats 85 through 88. - A by-hand spike comes first, because none of the mechanism has been run in game.
How it works
flowchart LR
A["Build<br/>compiler lays out each overlay<br/>with the title pipeline"] --> B["Server<br/>writes the overlay run into the<br/>hitbox item's name and lore"]
B --> C["Client<br/>draws the tooltip only<br/>while the slot is hovered"]
C --> D["Text shader<br/>x: add one constant<br/>y: rebuild from the code in x"]
Pinning x
Vanilla places a tooltip at the cursor plus (12, -12). If it would run off the right edge, x becomes max(x - 24 - width, 4). A line wider than the screen therefore always starts at x = 4, wherever the cursor is.
Each overlay line is built so its net width is about 16384 px, using spacers. The tooltip box is hidden with the existing transparent hover tooltip style.
Marking and placing glyphs
Every overlay glyph is preceded by a large positive spacer and the line ends with a negative one, so the glyph is drawn far to the right while the line's width stays fixed. The glyph's x position then holds everything the shader needs:
Position.x = 4 + 1024 × code + layout x
│ │ └ 0–1023: where the glyph sits in the overlay
│ └ about 14 bits: the run's target y and its glyph height
└ where vanilla pins an over-wide tooltip
| Part | Bits | Meaning |
|---|---|---|
| layout x | 10 | Horizontal position in the overlay, set by ordinary spacers |
| target y | about 9 | Top of the run, relative to the screen center |
| glyph height | about 5 | Height of the run's glyph quads |
| marker | none | code is always at least 16, so x is past 16384, where nothing else draws |
A 32-bit float holds whole numbers exactly up to 2^24, which is where the 24-bit total comes from.
The shader then does two things to a marked vertex:
- x: subtract the pinned origin and the code, add the screen center. Letters keep their natural spacing, so a run of bound text is one component.
- y: discard it. A top vertex goes to the target y and a bottom vertex to the target y plus the height. Which corner a vertex is comes from its UV, as the current shader already works out.
Because y never depends on where the tooltip is, the tooltip can be one line tall, and the lines F3+H adds (the item id and component count) change nothing.
Colors and shadows
Position carries all the data, so text color and shadow color are the real ones. Both copies of a glyph see the same position and both are placed. Rebuilding y loses the shadow copy's 1px vertical offset, so overlay text sets its shadow alpha to 254 and the shader adds the pixel back for that alpha. Art glyphs set shadow alpha 0, which makes the client skip the shadow copy.
Tooltips
A tooltip is overlay content like any other, laid out at runtime:
- The server measures the text, as it already does for the cut-out, and wraps it to a fixed maximum width.
- Each row is a background strip, a negative spacer, then the row's text. Strips are a left cap, a right cap and middle pieces 1 to 128 px wide, in top, body and bottom variants: about 30 glyphs for any width and any number of rows.
- Every row is its own run with its own target y, so rows are placed to the pixel.
- The tooltip is anchored to the hovered slot (each slot has its own item) and goes below controls in the top half of the window and above the rest.
Runtime tooltips become bound text in the overlay. That removes the invisible tooltip stacks packs use today, which is what gives rows, cards and nodes a hover at all.
Authoring sketch
<Region onClick={buy} hover={<Box frame={art.buyLit} />} tooltip={tip} />
// States come from an ordinary switch inside the overlay.
<Region
onClick={previous}
hover={
<Switch bind={canPrevious}>
<Case value="true" frame={art.stepHover} />
<Case value="false" frame={art.stepHoverDisabled} />
</Switch>
}
/>
<Region onClick={visit} hover tooltip={zoneTip} /> // a default outline; the tooltip is a handle Kotlin fillshover as a boolean keeps drawing a default outline, through the new path. The data-pixel glyphs, the shadow-color packing and the tooltip cut-out are removed.
Checked against the 26.2 client
Read from the client jar and its core/text shader, not run in game:
DefaultTooltipPositioner: the placement and clamp rules above. Item tooltips in containers use it.- Tooltip size is the widest line by 10 px per line (8 px for one line).
- A bitmap provider's
ascentis any integer not greater thanheight; a space provider's advances were not checked. - A shadow color with alpha 0 draws no shadow copy; any other alpha reaches the shader unchanged when the text is opaque.
- With advanced tooltips the client appends two lines to an item like the hitbox.
- Text is queued for drawing unless it has no bounds; no screen culling was found at that point.
- The GUI text vertex shader receives
Position,Color,UV0, the atlas,ProjMatand theGlobalsblock (ScreenSize,GameTime). It has no cursor position.
Spike
One hand-built item and a small shader edit, to confirm:
- The tooltip pins at x = 4 with a 16384 px line, at several window sizes and GUI scales.
- A glyph placed millions of pixels to the right is still drawn, and spacers that large are accepted.
- The x shift and rebuilt y land on the pixel, for an art glyph and for a run of vanilla-font text.
- The hidden tooltip box costs nothing at that width.
- F3+H and a tooltip at the bottom of the screen change nothing.
Done when
hoveron<Region>and the industrial controls takes elements, including switches, bound text, bound sprites and tints, and draws them while the region is hovered.- A region's tooltip, static or from a handle, is drawn by Window and sized to its text.
- A slot showing a stack can be part of a hover region.
- Hover is enabled by
experimental.hover;coreShaders.hoveris a build error that names the new option. - Previews can draw a control hovered.
- AUTHORING.md describes overlays, the tooltip, the limits and what is client-version dependent.
Limits and open questions
- Glyph height. With about 5 bits a glyph is at most 32 px tall, so the compiler slices taller art into bands. Fewer layout bits would allow taller glyphs.
- Text heights. The shader needs each run's glyph height. A pack font has one; vanilla's default font mixes several, so the runtime needs a table and may split runs.
- Slots with a real stack. The slot has one tooltip. Either the stack keeps its vanilla tooltip and the slot has no overlay, or the overlay replaces it. Probably a prop.
- Updates. An overlay with bound values means resending the hitbox item when they change.
- Always on top. The tooltip layer draws above the window's text and items. An overlay can cover or tint what is under it, not go beneath.
- Client mods. x relies on vanilla's clamp, so a mod that moves or redraws tooltips can misplace overlays horizontally.
- Tooltip art. Where a pack sets the tooltip's frame and text style.
- Wide screens. A 16384 px line pins any screen narrower than that.
Considered and dropped
- Art in the existing hover glyph. Keeps data pixels in the texture and gives one piece of art per region. No text.
- Placing every glyph from its shadow color. Text needs a component per character with its width, and the 32 bits do not hold position, size, cut-out and color.
- Pinning y with 256 lore lines. Works, but F3+H then moves every overlay 20 px, and every hitbox item carries the padding.
- y in a font's ascent. Needs a font per art per row and still leaves the glyph's size unknown.
- A table of rectangles in the shader, indexed by an id. Fine for static rectangles, no help for content laid out at runtime.
Activity