Skip to content

feat: let a region's hover draw authored art in its own colors - #113

Open
oglassdev wants to merge 6 commits into
mainfrom
feat/hover-art
Open

oglassdev wants to merge 6 commits into
mainfrom
feat/hover-art

Conversation

@oglassdev

@oglassdev oglassdev commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

coreShaders.hover could only outline a hovered region with a fixed 1px white rectangle. A pack with rounded buttons, or buttons whose icon overhangs the body, could not make the hover match its art or color it (a red hover on a close button, a gray one on a disabled control).

hover now takes art as well as a boolean:

<Region onClick={buy} hover={ring} />                                  // this art, stretched over the region
<Region onClick={buy} hover={{ art: ring, tint: "#ffd731" }} />        // the same art, tinted
<Region onClick={buy} hover={{ art: badge, fit: "center" }} />         // at its own size, centered
<Region onClick={buy} hover />                                         // the white rectangle, unchanged
<Region onClick={buy} hover={false} />                                 // none

This is the first step toward #112; nothing else from #112 is built here.

Examples

Bare art stretches over the region exactly as frame={art} would. A 5x5 ring with insets becomes a ring of any size:

const ring = texture("window/hover/ring.png", { insets: 2 });

<Region onClick={buy} tooltip="Click to upgrade" hover={ring} />

tint multiplies the art's colors at build time, so one white texture serves every color. It takes a hex color or a theme token:

export const hovers = {
    ring: { art: ring, tint: "#ffd731" },
    exit: { art: ring, tint: "#ed171b" },
} satisfies Record<string, Hover>;

<Button close span={3} hover={hovers.exit}>
    Exit
</Button>

fit: "center" draws the art at its own size, centered on the region. Art larger than the control overhangs it, which is how to draw a ring around a control rather than on its edge:

<Region
    onClick={pick}
    hover={{
        art: shape({ kind: "panel", fill: "#ffffff", radius: 4, width: 20, height: 20 }),
        fit: "center",
        tint: "#ff5555",
    }}
/>

Art can follow the theme like any other art. A control draws it only in the states that take clicks, so this disabled button shows no hover:

const lit = derive((get) => shape({ kind: "panel", fill: get(colors.accent), radius: 2 }));

<Button onClick={buy} hover={lit} disabled={{ tooltip: "Too expensive" }} enabled={canBuy}>
    Buy
</Button>

A preview can draw every region's hover at once, to check alignment without a client:

preview(upgrades, state, { name: "upgrades_hovers", scale: 3, hovers: true });

The example workshop, before and after

Before, a buyable card could only take the white rectangle:

<Region onClick={props.buy === true ? upgrade.at(i) : undefined} tooltip={props.tooltip} />
<Button close span={3}>
    Exit
</Button>

After, a buyable card draws a gold ring and the exit button a red one, both from one white 5x5 texture. The other controls keep the default outline:

props.buy === true ? (
    <Region onClick={upgrade.at(i)} tooltip={props.tooltip} hover={hovers.ring} />
) : (
    <Region tooltip={props.tooltip} />
)
<Button close span={3} hover={hovers.exit}>
    Exit
</Button>

Behavior

  • Hover art is drawn in its own colors and alpha while one of the region's slots is hovered.
  • hover={art} and fit: "stretch" (the default) cover the region as a frame would. fit: "center" draws the art at its own size, centered, and may overhang; an odd size difference puts the art the odd pixel toward the top-left. A shape centered this way needs a width and height, or the build fails.
  • tint is baked in. A runtime tint(...) handle on a hover is a type error and a build error.
  • It works on <Region> and the industrial controls that pass hover through (Button, Toggle, Choice, Tabs, Repeater, Hotspot, Face.hover). The prop's type is exported as Hover.
  • hovers: true on a preview draws what the pack draws: nothing when coreShaders.hover is off, no pixels under 10% alpha, and the same build error for a hover outside the size and reach limits. It does not draw the tooltip cut-out.
  • The tooltip cut-out, the hidden tooltip style, the 128 px reach limit, and the build error when coreShaders.hover is off are unchanged.

How

  • A hover glyph is now its art between a top and a bottom data row. The rows hold the ID and size pixels that used to sit inside the visible outline, so they no longer need the shader to paint every pixel white.
  • The vertex shader places the quad so the data rows fall one pixel above and below the art; the fragment shader discards them and keeps the texture's color.
  • The generated white outline uses the same layout, so there is one shader path.
  • Glyphs are keyed hover/{w}x{h} for the outline and hover/frame|image/{art}[/{tint}]/{w}x{h} for art, so hovers that draw the same pixels share a glyph.
  • The JVM runtime is unchanged apart from KDoc. HoverEntry.x/y is now the top-left of what is drawn rather than of the region.

Limits

  • A hover is at most 254 px tall (was 256) because of the two data rows; this applies to the outline too.
  • Hover art narrower than 4 px is a build error.
  • Pixels under 10% alpha are not drawn: the vanilla text shader discards them.
  • A texture id from another pack cannot be hover art.
  • There is no outset: stretched art always covers exactly the region, and overhang needs fit: "center" with art sized for that control.

Testing

  • just test-rust -p window-core, just lint, just fmt-check, just check-ts, just test-ts and just golden pass.
  • Golden hashes changed only for the hover case, which gained a stretched art hover and a centered, tinted one: both text shaders, font/ui.json, the two outline PNGs, debug.json, MenuView.kt, WindowEntries.kt, plus two new art glyph PNGs.
  • JVM tests were not run; only KDoc changed there.
  • The example pack builds with just example-pack; its generated WindowEntries.kt is regenerated for the new hovers, and its new upgrades_hovers preview shows gold rings on the buyable cards and a red one on Exit.

The shader change is untested in game. When playtesting, check that art keeps its colors, that no stray data pixels show above or below the art (especially at rounded corners), that the art is aligned to the control with its first and last rows intact, and that the tooltip cut-out still works.

🤖 Generated with Claude Code

`hover` on a region and the industrial controls takes art as well as a
boolean. Art that stretches covers the region; other art is drawn at its
own size, centered on it.

A hover glyph is now its art between two data rows that the text shader
reads and discards, so the art keeps its own colors and alpha. The
generated white outline uses the same layout, which limits a hover to
254 px tall.
Frames and images are interned in separate tables, so a frame and an
image could share a name and with it a hover glyph.
…iew option

hover={art} now always stretches the art over the region like a frame.
The object form { art, fit, tint } centers it at its own size or
multiplies its colors at build time, and preview(..., { hovers: true })
draws every region's hover as the pack draws it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant