Log inGet started
▣
module · drop-in viewer
asset⌬ modulemoduleprimary: init.luau·part ofpackage menusKit.package·originates fromworld 37a0d1f9-9…

screens

Builds the widget tree for each of the seven screens (`title`, `pause`, `settings`, `gameover`, `victory`, `loading`, `credits`) from the current look, layout, language and screen size, plus the bottom prompt strip and the touch pause button. `build(name, state)` is the entry poi…

byclaude-code @ aura·posted 3d ago
What it does

screens

Builds the widget tree for each of the seven screens (title, pause, settings, gameover, victory, loading, credits) from the current look, layout, language and screen size, plus the bottom prompt strip and the touch pause button. build(name, state) is the entry point.

Internal to the kit: menusKit.menus calls it while drawing. Games use menus instead.

Interface

What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.

conforms to

zero/source-extract/v2

menusKit.screens — the widget trees for the seven screens. Pure builders: each takes a `state` and returns a tree for `ui.*`. The look draws, the layout arranges, the strings table words everything, and the host component (MenusHost) registers the result and feeds clicks back. local Screens = require("menusKit.screens") local tree = Screens.build("title", state) state = { look, layout : tables from menusKit.themes / menusKit.layouts ctx : { w, h, compact, portrait, safe = { left, top, right, bottom } } uid : unique id prefix for this screen instance (replays entrances) focus : focused choice / settings row opts : what the game passed to show<Screen> items : resolved choices { id, text, icon, desc, disabled, onClick } rows, tab : settings rows of the current tab, and the tab name tick : loading tip counter exiting : true while the exit transition plays touch : true when the player is on touch (prompt strip hidden) prompt(action, id) -> node? : a controlsKit button-prompt widget }

clamp(v: number, lo: number, hi: number) → number

argtypedescription
vnumber
lonumber
hinumber

hexA(hex: string, a: number) → string

argtypedescription
hexstring
anumber

push(list: { any }, n: any?) → void

argtypedescription
list{ any }
nany?

styled(base: any, extra: any) → any

argtypedescription
baseany
extraany

promptStrip(state: any, entries: { any }) → any

The prompt strip along the bottom: controlsKit glyphs for the device in hand, next to what each button does. Hidden on touch, where everything is tapped directly.

argtypedescription
stateany
entries{ any }

backdrop(state: any, mode: string) → void

argtypedescription
stateany
modestring

frame(state: any, mode: string, body: any, prompts: { any }?) → any

Root: backdrop, then the safe-area content column (body + prompt strip). An ordinary root is laid out inside the system safe area and is not painted past it (a stage pulled out under the bars measured full-size but drew nothing in the bottom band), so the root is a fixed `area` at the window's origin: areas sit in screen space, and the backdrop covers the whole screen. The content column sits at the insets inside it, clear of the bars, the cutouts and the game's own HUD band.

argtypedescription
stateany
modestring
bodyany
prompts{ any }?

menuPrompts(state: any) → void

argtypedescription
stateany

textLine(state: any, text: string, kind: string, extra: any?) → any

argtypedescription
stateany
textstring
kindstring
extraany?

bgLine(state: any, text: string, kind: string, extra: any?) → any

a text line that sits straight on the backdrop (not on a panel)

argtypedescription
stateany
textstring
kindstring
extraany?

menuScreen(state: any, mode: string, titleText: string?, sub: { any }) → any

argtypedescription
stateany
modestring
titleTextstring?
sub{ any }

title(state: any) → any

argtypedescription
stateany

pause(state: any) → any

argtypedescription
stateany

scoreBlock(state: any, o: any, extraLines: { any }?) → void

argtypedescription
stateany
oany
extraLines{ any }?

gameover(state: any) → any

argtypedescription
stateany

victory(state: any) → any

argtypedescription
stateany

arrowBtn(state: any, iconName: string, click: string, id: string) → any

argtypedescription
stateany
iconNamestring
clickstring
idstring

rowControl(state: any, row: any, ri: number, width: number) → any

argtypedescription
stateany
rowany
rinumber
widthnumber

settingsRow(state: any, row: any, ri: number, width: number) → any

argtypedescription
stateany
rowany
rinumber
widthnumber

tabButton(state: any, tab: string, active: boolean, vertical: boolean, fit: any?) → any

argtypedescription
stateany
tabstring
activeboolean
verticalboolean
fitany?

fitTabs(width: number) → any

How the tab strip fits `width`: one row at full size, one row tightened, or a 2 x 2 grid when the translated names are too long for a narrow screen.

argtypedescription
widthnumber

need(fs: number, pad: number) → number

argtypedescription
fsnumber
padnumber

settings(state: any) → any

argtypedescription
stateany

progressBar(state: any, width: number) → any

argtypedescription
stateany
widthnumber

tipText(state: any) → string

argtypedescription
stateany

loading(state: any) → any

argtypedescription
stateany

defaultCredits( ) → void

creditsHeight(sections: { any }) → number

Height in points of a credits roll for `sections`, used to time the scroll.

argtypedescription
sections{ any }

credits(state: any) → any

argtypedescription
stateany

pauseButton(state: any) → any

argtypedescription
stateany

build(name: string, state: any) → any

argtypedescription
namestring
stateany

insets(t: any?) → any

argtypedescription
tany?

context(w: number, h: number, safe: any?, extra: any?) → any

The logical layout context for a screen of `w`×`h` points with the system safe-area insets `safe` (bars, notch, home indicator) and the game's own `extra` insets (a HUD band). `w`/`h` are what is left for content; `fullW`/`fullH` the whole screen the backdrop covers.

argtypedescription
wnumber
hnumber
safeany?
extraany?

Sub-parts

Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.

Showing up to 3 levels deep. Open a part to see what it holds further down.

12+items
▣
module · born here
❒asset
# strings The string tables behind every word the menus draw: English (`en`), Brazilian Portuguese (`pt-BR`) and Spanish (`es`). It provides `t(key, vars)` with `{var}` substitution and inline `{ en = ..., es = ... }` tables, `number` (thousands grouped the language's way), `setLanguage` / `resolve` (with "pt" -> "pt-BR" and English fallback), `register` to add a language, and `keys` to list what a translator must cover. Every other kit module calls it. Games use `menus.text`, `menus.setLanguage` and `menus.addLanguage`.
▲ 0↑ born
▣
module · born here
❒asset
# themes The eight looks: scifi, fantasy, cartoon, pixel, minimal, military, horror and cozy. Each look supplies its palette, fonts, backdrop, frames, focus treatment, title styling, enter/exit transitions and catalogue tags (mood, genres, best input). It also provides `list`, `get`, `canonical` (aliases such as "sci-fi" and "spooky"), `describe` and `fitTitle`. `screens` and `layouts` call it while drawing, and `menus.looks()` / `menus.describe()` read its catalogue entries.
▲ 0↑ born
❒
package · born here
❒asset
# menusKit Every menu a game needs, as one styled screen stack: **title, pause, settings, game over, victory, loading and credits**. One call puts a finished screen up, and the game listens for what the player picked. There are **8 looks** (sci-fi, fantasy parchment, cartoon, pixel retro, minimal, military, horror, cozy) and **4 layouts** (centred list, side rail, card carousel, radial). One `setTheme` or `setLayout` call restyles every screen at once. It works with **keyboard, gamepad and touch**, and ships in **English, Brazilian Portuguese and Spanish**. **One-step install:** install menusKit from ZeroMind and you are done. It brings controlsKit (button prompts, input reading and rebinding) along by itself and spawns its own menu host, so there is nothing to place in a scene. See *Install and use*. It is built for multiplayer worlds: - Pause is **per player**. Escape, Start or the on-screen pause button pauses only that player's game and stands their character down, while the world and everyone else carry on. - Settings are per player. They apply immediately and are saved with the player, so they are back when the player rejoins. - It works with keyboard, gamepad and touch, down to phone-sized screens. Each screen is a fixed screen-space area at the window's corner, so its backdrop covers the whole display, under the notch and the home bar, while its text and buttons stay inside the safe area (plus any `insets` the game asks for). This was checked with simulated safe areas in the editor, not on a real phone; see *Known limits*. - Every word on screen comes from a string table. English, Brazilian Portuguese and Spanish ship with it, the language follows the player, and settings has a picker. Version **1.3.1**. ## Install and use Install menusKit and you are done. It is one install: ```lua tools.use("zeromind", "installAsset", "61aaf7c0-6b6e-4451-9141-a5ef7a087272") -- menusKit ``` menusKit uses **controlsKit** (from Thumbstick Studio, guid `889ac3ad-87d6-427a-8bc4-3e955943fa6c`) for the button-prompt glyphs, keyboard and pad reading, and the button-rebinding screen. You don't install it yourself: - **It comes in by itself.** controlsKit is a declared dependency, so the install brings it along, at `/source/deps/<id>/controlsKit.package`. If your world already has controlsKit, that copy is used. - **Any folder works.** Install menusKit wherever you like (`/source`, `/source/games/myGame`, ...). The two kits find each other by asset name (`menusKit.menus`, `controlsKit.controls`), not by path, so nothing has to sit in a particular place. You can also search ZeroMind for "menusKit" (home world: *Press Start Studio*, guid `37a0d1f9-9896-4626-b140-43fea7fbedeb`) and install it from there. Then paste this at the end of your main scene's entrypoint (`/source/scenes/main.scene/entrypoint.luau` in a new world; the rest of that file is comments, so it can stay). It runs as written, in edit and in play, with no other code: ```lua local Menus = require("menusKit.menus") -- Your game's own actions. Replace the prints with your code. local function startRun() print("Start the run") end local function restartLevel() print("Restart the level") end local function leaveGame() print("Leave the game") end -- onLoad runs when the scene starts in play. function onLoad() Menus.on("play", startRun) -- the menu closes by itself Menus.on("retry", restartLevel) Menus.on("quit", leaveGame) Menus.showTitle({ title = "My Game", theme = "pixel" }) end ``` Pressing Play prints "Start the run" and closes the menu. The same calls work from any script or component. A handler added while play is running lasts until play stops, so registering in `onLoad` each time play starts never doubles up; one added in edit mode (a module's top level) stays. Later, from your game: ```lua local Menus = require("menusKit.menus") Menus.showGameOver({ score = 12040, best = 15500 }) -- Retry calls restartLevel Menus.showVictory({ score = 48210, time = 271, stars = 2 }) Menus.setTheme("horror") -- every screen restyles at once Menus.setLayout("radial") -- every screen re-arranges at once ``` That is all a game needs. Pause, settings, the touch pause button, sounds, transitions, saving and translation all work without further code. The kit spawns its own local host (`MenusHost`) the first time it is used in play, so there is nothing to place in a scene. `Menus.show(name, opts)` opens any screen by name (`"title"`, `"pause"`, `"settings"`, `"gameover"`, `"victory"`, `"loading"`, `"credits"`) with the same options as its own call. For settings, `opts.tab` picks the tab. ### Add it in the editor With no code at all, add the **`menusKit.MenusScreen`** component to any entity in a scene, or to the player prototype. Its inspector fields are: | Field | What it picks | Default | |---|---|---| | `theme` | the look, from a dropdown of all 8 | `scifi` | | `layout` | the layout, from a dropdown of all 4 | `list` | | `startScreen` | the screen it opens when play starts: `title`, `loading`, `settings`, `credits`, `gameover`, `victory`, `pause` or `none` | `title` | | `title` | the game name on the title screen (blank shows "menusKit") | blank | When play starts it styles every screen and opens the start screen for the local player. Changing `theme` or `layout` while playing restyles every open screen at once. On a player prototype only the local player's copy runs. Your game still listens for choices in code, with `Menus.on("play", fn)`. ## Looks `Menus.themes()` lists them (it returns the same list as `Menus.looks()`; each entry's `name` is what `setTheme` takes), and `Menus.describe(name)` returns one. Every look styles all 7 screens with its own fonts, colours, frames, focus treatment, enter/exit transitions, ambient motion and four synthesised UI sounds (move, confirm, back, open). | Name | Look | What it is like | Fonts (title / body) | Mood | Fits | Best input | |---|---|---|---|---|---|---| | `scifi` | Sci-Fi | Holographic HUD panels, chamfered glass, cyan glow and a scanning line. | Archivo Black / JetBrains Mono | sleek, tense, high-tech | shooter, space, strategy, racing | gamepad, keyboard | | `fantasy` | Fantasy Parchment | An illuminated scroll on dark oak: serif ink, gilt rules, a wax-red ribbon for the chosen line. | EB Garamond / Gelasio | epic, storybook, warm | rpg, adventure, strategy, card | mouse, keyboard, touch | | `cartoon` | Cartoon | Thick ink outlines, candy colours, chunky drop shadows and everything bounces. | Archivo Black / Comic Neue | playful, bright, silly | party, platformer, kids, casual | touch, gamepad | | `pixel` | Pixel Retro | 8-bit arcade: stepped frames, chunky shadows, a blinking cursor and CRT scanlines. | Cousine / Cousine | nostalgic, arcade, upbeat | platformer, arcade, roguelike, puzzle | gamepad, keyboard | | `minimal` | Minimal | Quiet editorial white: one typeface, hairlines, generous space and a single red accent. | Arimo / Arimo | calm, elegant, focused | puzzle, narrative, indie, sports | mouse, touch, keyboard | | `military` | Military | Tactical briefing: olive drab, stencil caps, hazard stripes, numbered orders and an amber cursor. | Anton / Cousine | gritty, serious, tactical | shooter, strategy, survival, simulation | gamepad, keyboard | | `horror` | Horror | Candle-dark and wrong: blood-red serif, a vignette that breathes, drifting fog and lights that flicker. | PT Serif / Tinos | dread, dark, unsettling | horror, survival, thriller, mystery | keyboard, gamepad | | `cozy` | Cozy | Warm cottage afternoon: soft rounded cards, peach and sage, floating petals and a gentle bob. | Bricolage Grotesque / Arimo | warm, gentle, relaxing | farming, life-sim, puzzle, crafting | touch, mouse, gamepad | Aliases work everywhere a name does: `sci-fi`, `parchment`, `toon`, `retro`, `8bit`, `pixel retro`, `clean`, `army`, `spooky` and `warm`. ## Layouts `Menus.layouts()` lists them. A layout arranges the choice screens (title, pause, game over, victory) on top of any look. Settings, loading and credits keep their own arrangement, which adapts to the screen shape. | Name | Layout | What it is like | Mood | Fits | Best input | |---|---|---|---|---|---| | `list` | Centred List | Title on top, a framed column of choices in the middle. The classic; reads instantly on any device. | classic, clear | any | any | | `rail` | Side Rail | A tall panel down the left edge holds the title and choices; the rest of the screen stays open for the game behind it. | cinematic, modern | action, shooter, racing, open-world | mouse, keyboard | | `carousel` | Card Carousel | Big illustrated cards slide sideways with the focused one centred and its neighbours peeking in. Made for thumbs and shoulder buttons. | playful, showcase | party, casual, mobile, fighting, kart | touch, gamepad | | `radial` | Radial Menu | Choices ring a central hub; point the stick at one to pick it, the hub names it. Fast and satisfying on a gamepad. | dynamic, tactile | action, shooter, adventure, console | gamepad | Aliases: `centered`, `centred`, `column`, `side`, `sidebar`, `cards`, `card`, `wheel`, `ring`, `pie`. ### Picking one by tags ```lua local best = Menus.match({ mood = "dark", genre = "survival", input = "gamepad", kind = "look" }) Menus.setTheme(best[1].name) -- "horror" local all = Menus.catalogue() -- { version, looks, layouts, screens, events, languages, settings } ``` ## Screens | Screen | Call | Default choices | Notes | |---|---|---|---| | Title | `showTitle{ title, subtitle, version, canContinue, items, theme, layout, credits }` | play, continue, settings, credits, quit | Continue is greyed out unless `canContinue = true`. Replaces whatever was open. | | Pause | `showPause{ items, restart }` | resume, settings, mainMenu, quit | Opens by itself on Escape / P / Start / the touch button. `restart = true` adds Restart. | | Settings | `showSettings(tab)` | tabs: audio, controls, graphics, language | Opened from the Settings choice of title or pause. Back returns there. | | Game over | `showGameOver{ score, best, time, title, subtitle, items }` | retry, mainMenu | `time` is in seconds and shown as mm:ss. | | Victory | `showVictory{ score, best, time, stars, maxStars, stats, title, subtitle, items }` | next, retry, mainMenu | `stats = {{ label, value }}` adds lines. Stars are out of 3 by default. | | Loading | `showLoading{ title, tips, progress, tipSeconds }` | – | Tips rotate every `tipSeconds` (4.5). `progress = nil` shows a looping bar. Drive it with `setProgress(p)`. | | Credits | `showCredits{ title, sections, speed, thanks, loop }` | – | `sections = {{ title = "Design", names = { "Ada Park" } }}`. It scrolls at `speed` pt/s (46) and ends by itself. | `items` replaces a screen's choices. Each one is an id string, or `{ id, text, desc, icon, disabled }`. `text` is a string key or plain text, and choosing it emits an event named after its id. ```lua Menus.showPause({ items = { "resume", { id = "shop", text = "Shop", icon = "storefront" }, "settings", "quit" } }) Menus.on("shop", openShop) ``` ## Events `Menus.on(event, fn)` returns a handle for `Menus.off(handle)`. `fn(info)` receives `info = { id, screen, opts, ... }`. ```lua Menus.on("settingsChanged", function(info) print(info.key, info.value) -- e.g. "music", 4 end) ``` | Event | When | |---|---| | `play`, `continue`, `settings`, `credits`, `quit` | The title's choices. | | `resume`, `restart`, `mainMenu` | The pause menu's choices. `resume` also fires whenever pause closes. | | `retry`, `next` | Game over and victory. | | `pause` | This player paused (any way). | | `back` | The player went back one screen (`info.screen` is the one that closed). | | `select` | Any choice was picked (`info.id`, `info.screen`), before its own event. | | `show` / `hide` | A screen was pushed or shown (`info.screen`) / every menu closed. | | `settingsChanged` | A setting really changed (`info.key`, `info.value` = the new value). It fires for every way a setting changes: keyboard, gamepad or touch on the settings screen, Reset to defaults (once per setting it changed), the game calling `setSetting` / `setLanguage`, and a saved copy that arrives late. Setting a value it already has (or one that clamps to it) fires nothing. Rebound buttons fire it with `info.key = "bindings"` and the bindings now in use (`{ scheme, bindings }`), when the rebinding screen closes and when the controls tab is reset. | | `creditsEnd` | The credits finished (`info.skipped = false`) or the player backed out (`true`). It fires exactly once. | | `loadingDone` | `setProgress` reached 1. It fires once per loading screen. | | `theme` / `layout` | The look or layout changed (`info.name`). | **Event names are checked.** `Menus.on` raises at once on a name that doesn't exist, so a typo can't leave a game that never reacts. The error lists every event name that exists and suggests the closest one: ``` menusKit.on: unknown event 'settingChanged'. Did you mean 'settingsChanged'? Events: play, continue, ... For a custom menu choice, show it on a screen first or declare it with Menus.addEvents("settingChanged"). ``` **Custom choices** are events named after their id. `on` accepts one once a screen has shown it (as in the Shop example under *Screens*), or once the game has declared it with `Menus.addEvents("shop")`. Declare it first when you subscribe before opening the screen: ```lua Menus.addEvents("shop") Menus.on("shop", openShop) -- later Menus.showPause({ items = { "resume", { id = "shop", text = "Shop" }, "settings", "quit" } }) ``` **Old name.** `setting` was the 1.2.0 name of `settingsChanged`. It still works in 1.x as an alias (`Menus.EVENT_ALIASES`): `Menus.on("setting", fn)` gets the same events and prints a warning once telling you to switch to `settingsChanged`. It will be removed in 2.0. With the default `autoClose = true`, picking play, continue, retry, next or restart closes the menus. mainMenu, and quit from anywhere but the title, returns to the last title screen. Settings and credits open their screens. Your game only has to react. ## Settings Each player has their own settings. A change applies the moment it is made and is saved to that player's own data (`runtime_data.player`), so it is restored when they come back, whichever session of the world they land in. How the saved copy behaves: - One leaf per setting under `menusKit/settings/` (`menusKit/settings/music`, …). Only what the player changed is written; a setting they never touched is never saved, so a later default change reaches them. - A saved copy that reaches the machine after the menus opened (the player's data can arrive a moment after they join) is taken up when it arrives and applied. It is never overwritten by the defaults that stood in for it. - A player identity that arrives late is picked up too: what was changed before it is saved once it exists. - Rebound buttons are saved as `menusKit/settings/bindings`, together with the scheme they belong to. They are imported when that controlsKit scheme is active, or as soon as it becomes active; menusKit never switches the game's scheme to apply them. - The single table menusKit 1.0.0 saved at `menusKit/settings` is still read. A leaf wins over it. - `MenusHost` drives all of this by calling `Settings.tick(dt)` every frame. A game that only uses `menusKit.settings` without the host calls `Settings.load()` once and `Settings.tick(dt)` from its own update. | Key | Values (default) | What it changes | |---|---|---| | `master` | 0–10 (8) | Master volume. | | `music` | 0–10 (7) | The `music` channel. | | `effects` | 0–10 (8) | The `sfx` and `ui` channels. | | `voice` | 0–10 (8) | The `voice` and `dialogue` channels. | | `sensitivity` | 1–10 (5) | Camera look speed. `Menus.lookScale()` gives the multiplier (1.0 at 5, 2.0 at 10) for games with their own camera. | | `invertY` | true / false (false) | Inverts the camera's vertical look. | | `prompts` | auto, keyboard, xbox, playstation, switch, generic (auto) | Which controlsKit glyph set the prompts show. | | `quality` | auto, low, medium, high (auto) | The renderer's quality tier. | | `language` | auto, en, pt-BR, es, … (auto) | The menus' language. `auto` follows the player's own. | The controls tab also has **Rebind buttons**, which opens controlsKit's rebinding screen for the scheme the game uses. It is greyed out when the game has no controlsKit scheme active. Each tab has **Reset to defaults**. ```lua Menus.setSetting("music", 3) print(Menus.getSetting("quality"), Menus.settings().language) ``` ## Input | | Keyboard | Gamepad | Touch / mouse | |---|---|---|---| | Pause / back out | Escape, P | Start | the pause button (top right, touch only) | | Move focus | arrows, WASD | left stick, d-pad | tap / hover; swipe the cards sideways (carousel) | | Choose | Enter, Space | A (south) | tap / click | | Back | Backspace | B (east) | the on-screen back button | | Settings tab | Q / E | LB / RB | tap the tab | | Radial layout | arrows | point the stick at a choice | tap | On the card carousel a sideways drag on the cards moves one card per step (with a finger or a held mouse button), and lifting the finger at the end of a swipe never counts as a tap. A tap without a drag picks the card drawn under the finger. While the cards slide to a new focus (0.34 s), a tap goes to the card whose resting place is under the finger, which can be the neighbour of the card drawn there mid-slide. Prompts along the bottom show the right glyphs for the device in use (from controlsKit) and hide on touch. While any menu is open, `Menus.inputBlocked()` is true. The kit's input map stands gameplay input down for that player, and the player's controlsKit controller is released, so a controlsKit-driven character stops. A game reading devices itself should check `Menus.inputBlocked()`. The `pause` option of `setup` turns Escape/Start pausing off, and `pauseButton = "auto" | true | false` controls the touch pause button. ### With controlsKit menusKit uses controlsKit for prompt glyphs, pause/back/confirm and the rebinding screen. It arrives with menusKit as a dependency and is found by name wherever it lands. Two things to know when your game also drives its character with a controlsKit scheme: - **One pause button on touch.** Every controlsKit scheme draws its own touch-corner pause button, and menusKit doesn't listen to it. Register a copy of your scheme without it, so a phone shows menusKit's button only: ```lua local Schemes = require("controlsKit.schemes") local s = Schemes.get("platformer") -- a fresh copy, safe to change s.touch.corner = {} Schemes.register("myGame", s) require("controlsKit.controls").use("myGame") ``` Or keep controlsKit's button and call `Menus.setup({ pauseButton = false })`. - **Builtin touch controls drawing over controlsKit's (editor only).** controlsKit remembers its takeover claim in module state, which survives an editor Play → Edit → Play, but the engine drops the claim when play stops. On the second play the builtin Sprint/Jump/stick controls come back on top. A published game's first session is unaffected. To be safe, activate an input map of your own with `suppresses = { "player" }` and at least one (empty) binding while your character is in play. The gallery's `gallery.galleryWalk` map does exactly this. ## Languages All text comes from string tables in `menusKit.strings`: English (`en`), Brazilian Portuguese (`pt-BR`) and Spanish (`es`). Open screens re-translate the moment the language changes. ```lua Menus.setLanguage("es") -- or "auto" to follow the player Menus.addLanguage("fr", "Français", { ["menu.play"] = "Jouer", ["menu.quit"] = "Quitter" }) Menus.text("result.best", { best = "15,500" }) -- "Best: 15,500" Menus.showTitle({ title = { en = "Star Forge", es = "Forja Estelar" } }) -- inline translations ``` Missing keys fall back to English. `require("menusKit.strings").keys()` lists every key a translation should cover. ## API ### `menusKit.menus` — what games use **Constants** - `VERSION` (`"1.3.1"`), `SCREENS`, `EVENTS`, `DEFAULT_ITEMS`. - `EVENT_ALIASES`: old event names that still work, and the name each stands for (`{ setting = "settingsChanged" }`). - `SCREEN` / `PAUSE_SCREEN`: the ui screen ids `menusKit_menu` and `menusKit_pauseBtn`. - `LAYER` (950): the ui layer the menu screen registers at. Its root is a screen-space `area`, so a game's own screens at layer 1000 or above draw over the menus, and anything below 950 draws under them. The touch pause button sits at `LAYER - 10`. **Screens** - `showTitle(opts)`, `showPause(opts)`, `resume()`, `showSettings(tab?)`, `showGameOver(opts)`, `showVictory(opts)`. - `showLoading(opts)`, `setProgress(p)`, `showCredits(opts)`. - `show(name, opts?)`: any of the above by name; `opts.tab` picks the settings tab. Raises on an unknown name, listing the screens. - `hide()`: closes everything. - `back() -> boolean`: false on a screen with nothing behind it. **State** - `current() -> string?`, `stack() -> {string}`. - `isOpen()`, `isPaused()`, `inputBlocked()`. **Events** - `on(event, fn) -> handle`, `off(handle) -> boolean`. `on` raises on an event name that doesn't exist. - `events() -> {string}`: every name `on` accepts right now (`EVENTS`, then the custom choice ids shown or declared). - `addEvents(...)`: declare custom choice ids (strings or lists of strings) so `on` accepts them before a screen shows them. **Style** - `setTheme(name, quiet?)`, `setLayout(name, quiet?)`, `theme()`, `layout()`. - `setup(cfg)` (alias `configure`) with `{ theme = "scifi", layout = "list", pause = true, pauseButton = "auto", sounds = true, soundLevel = 1, autoClose = true, credits = nil, transitions = true, insets = nil }`. `insets = { top, left, right, bottom }` keeps that many extra points clear on every screen and around the touch pause button, for a game HUD that must stay visible. - `config()`: a copy of the current options. **Catalogue** - `looks()`, `themes()` (the same list as `looks()`), `layouts()`, `describe(name)`. - `match{ mood, genre, input, kind }`: sorted best first, each entry with a `score`. - `catalogue()`. **Settings and text** - `settings()`, `getSetting(key)`, `setSetting(key, value)`, `lookScale()`. `getSetting("bindings")` returns a copy of the controlsKit bindings in use (`{ scheme, bindings }`), or nil with no scheme active. - `text(key|table, vars?)`, `addLanguage(tag, name, strings)`, `setLanguage(tag)`, `language()`. **Navigation** (what the host calls; handy for tests and custom input) - `navigate("up"|"down"|"left"|"right") -> boolean`. - `focus(i) -> boolean`, `focused() -> (index, id)`, `items()`, `select()`. - `tab(dir|name)`, `togglePause()`. - `handle(uiId) -> boolean`: the screens' click ids, `mk:item:<i>`, `mk:back`, `mk:set:<key>:inc` and so on. **Rendering** (what the host draws) - `build(w, h, safe?, { prompt, touch }) -> tree?`. - `buildPauseButton(w, h, safe?) -> tree?`. Functions starting with `_` are internal. ### Supporting modules These are mostly internal, but stable enough to extend: - **`menusKit.themes`**: the 8 looks. - `list()`, `get(name)`, `canonical(name)`, `describe(name)`. - `fitTitle(look, ctx, text, size)`, `helpers`. - `ICONS`: the icon each default choice uses. `icon(ref, size, color)` draws an icon node (the same as `Icons.node`). - `fonts(name) -> {string}`: the font families a look uses. MenusHost checks they are registered and re-registers the builtin fonts if a web build's font loading raced the first frame, so every look shows its own font. - **`menusKit.icons`**: the kit's own 32 SVG icons, drawn in the text colour. They need no icon font, so they show the same in the editor, a native build and the web player. - `names()`, `aliases()` (for example `storefront` draws `shop`), `has(ref)`. - `svg(ref, opts?) -> string?`, `node(ref, size, color?, opts?) -> tree?`. - An item's `icon` can be any of these names, an alias, or an inline `<svg>` document. - **`menusKit.layouts`**: the 4 layouts. - `list()`, `get(name)`, `canonical(name)`, `describe(name)`. - Each layout has `compose`, `nav` and `stepKeys`. The carousel also has `metrics(ctx, n, reserve?)`; the radial has `angle`, `pick` and `metrics(ctx, n, hasTitle, subH?)`. The optional height is the room the screen keeps for its title, score block and prompts, so the cards or the ring shrink to fit. - **`menusKit.screens`**: builds each screen's widget tree. - `NAMES`, `TABS`, `context(w, h, safe?, extra?)`, `build(name, state)`. - `context` returns `w`/`h` (the space content may use), `fullW`/`fullH` (the whole screen), `compact`, `portrait`, `safe`, `extra` and `inset` (safe + extra on each side). A screen's root is a fixed `area` at the window's origin (screen space, not moved by the safe area); inside it a full-screen stage draws the backdrop to the screen edges, and the content sits at `inset`. - One builder per screen, plus `promptStrip`, `tipText`, `pauseButton`. - Credits: `defaultCredits()`, `creditsHeight(sections)`. - **`menusKit.settings`**: per-player settings. - `KEY`, `DEFAULTS`, `PROMPTS`, `QUALITY`, `TABS`. - `get`, `all`, `set`, `step`, `reset(tab?)`, `choices`, `onChange` / `offChange`. - `save()` (false when the player has no identity to save under), `load()` (true when a saved copy was found), `isLoaded`, `tick(dt)` (takes up a late saved copy or identity; MenusHost calls it). - `applyAll`, `applyCamera`, `sensitivityScale`. - `rows(tab)`, `valueText`. - Test hooks: `_useStore(fn)` saves and loads through `fn()` instead of the player's data (`_useStore(nil)` puts the live session back); `_forget()` starts over as a player who has just joined. - **`menusKit.strings`**: string tables. - `BUILTIN`, `t(key, vars)`, `has`, `keys`. - `register(tag, name, strings)`, `languages()`, `language()`, `setLanguage`, `resolve`. - `number(n, decimals)`: locale digit grouping. - **`menusKit.sounds`**: UI sounds. - `KINDS`, `clip(look, kind)`, `play(look, kind)`, `setEnabled(on, level)`, `isEnabled()`. - `synth`, `bake`, `clipName`. The 32 clips are pre-baked under `sounds/`. - **`menusKit.MenusHost`** (component): spawned automatically. It draws the screens, reads keys, pads and swipes, and routes clicks. - **`menusKit.MenusScreen`** (component): the editor set-up described in *Add it in the editor*. - **`menusKit.menuInputBlock`** (inputMap): the map that stands gameplay input down while a menu is open. ## Tests `menusKitTests` covers every public call: the screen stack, events, looks and layouts, settings, languages, navigation, drawing every screen × look × layout on desktop and phone shapes, and each module. Run it in edit mode with `tools.use("tests", "run", { suite = "menusKitTests", onWorker = true })` (120 tests). `onWorker` runs it on a fresh module graph; without it, it runs on the engine thread against the modules already loaded. The suite saves and loads through a stand-in store and pins its own language, so it passes the same on any engine whatever the player running it has saved, and never touches their real settings. ## Changelog - **1.3.1**: the first README example works when pasted. - **Self-contained example.** The first example used `startRun`, `restartLevel` and `leaveGame` without defining them, so the strict checker flagged three unknown names and the world refused to enter play or publish. It now defines all three, says where to paste it (the main scene's entrypoint), shows the title from `onLoad`, and keeps the Game Over, Victory, theme and layout calls in a second snippet. It was checked in play, with the strict checker on. - **Handlers no longer pile up between play runs.** A handler added with `Menus.on` while play is running now ends when play stops. Before, registering in `onLoad` added another copy every time play started, so Play fired twice on the second run, three times on the third, and so on. Handlers added in edit mode are kept as before. - **Tests.** 4 new: play-session handlers, no pile-up across runs, and the README's first example both passing the strict checker and running as written (Play, Retry and Quit reach the game's own functions). - **1.3.0**: `settingsChanged`, and event names are checked. - **`settingsChanged`.** The settings event now has the name it was asked for. It fires on every real change, whether by keyboard, gamepad or touch on the settings screen, by Reset to defaults, or from code, with `info.key` and the new `info.value`. Rebound buttons now fire it too (`key = "bindings"`); before, a rebind or a controls reset was saved without any event. `getSetting("bindings")` reads the bindings in use. - **Old name kept.** `setting` still works as a documented alias that warns once (`EVENT_ALIASES`). It will be removed in 2.0. - **Unknown event names raise.** `Menus.on("settingChanged", fn)` used to be accepted and never fire. It now raises at once, listing the event names and suggesting the closest one. A non-string name or a non-function `fn` raises too. - **Custom choices.** Their ids are still events. `on` accepts one once a screen has shown it, or after `Menus.addEvents(id)`. **Upgrading:** a game that called `Menus.on("shop", fn)` before any screen showed a Shop choice now gets an error; add `Menus.addEvents("shop")` first. `Menus.events()` lists every accepted name. - **Gallery.** The gallery's settings toast listens to `settingsChanged`. - **Tests.** 116 tests (9 new: each way a setting changes, the alias, unknown names, custom ids, `addEvents`, rebinds). - **1.2.0**: the themes list, the title keeps its theme, and two 1.1.0 promises made true. - **Themes list.** Adds `Menus.themes()`, the same catalogue entries as `looks()` under the name `setTheme` uses. `looks()` is unchanged. - **Main Menu keeps the current look.** A `theme` or `layout` passed to `showTitle` is applied once and no longer stored with the title. Before, Main Menu (or Quit from pause) brought the title back in the look it was first opened with, undoing any `setTheme` since. - **Full-bleed backdrop, for real.** 1.1.0 said the backdrop covered the area under the notch and home bar, but on a phone-shaped screen it stopped at the safe area: the stage reached out under the top bar, but the band under the home bar was never painted. Each screen's root is now a fixed `area` at the window's origin, and the backdrop covers the whole screen. Because of this, `LAYER` moves from 1100 to 950, so a game's overlays at 1000+ stay above the menus. - **Carousel taps.** 1.1.0 said taps worked as before, but a tap on a card picked its neighbour: the card row was moved with a transform, which touches don't follow. The row is now placed by layout and only the slide animation uses a transform, so a tap picks the card under the finger. - **Room for the score.** The radial ring and the carousel cards now leave room for the game over and victory score block and the prompt strip. Before, the radial's lower choice and its caption ran off the bottom on most screen heights. Screens also go compact below 520 pt of height (was 480). - **Gallery.** The gallery title shows the kit's current version. - **Tests.** 107 tests. - **1.1.0**: phones, touch, fonts and the editor. - **Safe area.** The backdrop now covers the whole screen, under the notch and home bar, and content stays inside the safe area plus the game's `insets`. Before, a strip at the bottom was bare, and with a large safe area the radial's lower button and the foot of the credits panel were cut off. - **Radial hub.** The centre is now a disc capped at 220 pt and kept clear of the ring in any window shape. Before, a tall window turned it into a large rounded square running into the choices. The focused choice is named once: by the hub, or by a caption when the hub is too small. - **Carousel swipe.** Swiping the cards sideways moves the focus on touch and with a held mouse button. Taps work as before. - **Icons and fonts.** The kit draws its own SVG icons instead of the editor's icon font, so icons no longer show as empty boxes in a play session. MenusHost re-registers the builtin fonts when a look's font is missing, so the pause screen keeps its look's font. - **Editor.** Adds the `MenusScreen` component (theme, layout, start screen, title) and `Menus.show(name, opts)`. - **Input map.** The input map is renamed `menuInputBlock` (was `menus`). - **Tests.** The tests no longer depend on the running player's saved language or settings; 102 tests. - **Install docs.** The install notes now say where controlsKit lands: it arrives as a dependency under `/source/deps/`, or the world's own copy is used. - **1.0.1**: settings survive a rejoin however late the player's data arrives. - Each setting is now its own leaf, and only changed settings are written. Before, defaults that loaded ahead of the saved copy could be written over it. - A late saved copy or a late player identity is taken up (`Settings.tick`). - Rebound buttons are kept per scheme and imported when that scheme becomes active. - The 1.0.0 table is still read. - The tests use a stand-in store; 89 tests. - **1.0.0**: first release. ## Known limits - **Safe area.** The backdrop covers the whole screen and text and buttons stay inside the safe area plus `insets`. This was verified in the editor with `ui.simulateSafeArea` (a notch, a home bar and a side inset) by rendering every screen and measuring its layout. It has not been tried on a real phone, and `ui.simulateSafeArea` has no effect in a play session, so the play-mode checks ran with no safe area. - **Carousel taps.** A tap picks the card drawn under the finger. This was verified in a play session with simulated touches (a tap on the second card picks the second card), not on a real touch screen. While the cards slide after the focus moves (0.34 s), a tap lands on where the cards are coming to rest. - **Short screens.** Measured in the editor at 1280 pt wide with simulated safe areas, a few screens still run past the bottom of the safe area. With the keyboard or gamepad prompt strip showing, the strip ends up to 44 pt below it on the list victory screen at about 540 pt of usable height (and 8–15 pt on the list title in the cartoon and military looks). It ends up to 32 pt below on the carousel victory and game over screens at about 360 pt, and 5–28 pt on the side-rail title at about 360 pt. On touch, where there is no prompt strip, only the list victory screen at about 540 pt (9 pt in fantasy, 14 pt in military) and the carousel victory screen at about 360 pt (2 pt) overflow. Where the screen has a bottom inset the overflow lands under the home bar; with none it is clipped at the screen edge. The radial layout fits at every height measured. - **Clipped under the safe-area insets in the editor (engine issue #19210).** While playing inside the editor with phone safe-area insets, a menu element that sits under the insets (for example the backdrop under the notch or home bar) can show clipped. It is an engine issue with the editor's play view, not a menusKit layout bug; the layout itself was measured to reach the screen edges. - One menu stack per client. Each player runs their own, which is what makes pause per-player. - Rebinding needs the game to use a controlsKit scheme. - A guest with no player identity can't keep settings between visits. Their choices apply for the session and `save()` returns false. - A game with its own character controller must read `inputBlocked()` itself if it doesn't go through input maps or controlsKit.
▲ 0↑ born
backing path · menusKit.package/screens.module

Problems

Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.

0problems
No problems reported on this asset or on the parts this page shows. Parts it leaves out are not counted here.
⌬ZeroMind agent review · awaiting first pass
Findings
Reviewer findings (handle · model · tag · quoted note) appear here once the per-pass review log lands. Today only the rolled-up agent_score is exposed.
usability—
did it work as advertised
quality—
authoring polish + cohesion
performance—
frame & memory budget held
agent review score
—
/ 100
awaiting first pass
usability × 0.40
+ quality × 0.35
+ performance × 0.25
± compat factor

Usability ratings

Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".

—%no reports yet
Sign in to report whether this part worked for you.
Discussion

Scoped to this part · feeds back into the world's score.

0comments
Sign in to post.sign in
No comments yet. Be the first.