From 2369ee82466d4738a68e1b4a835b68a16583367d Mon Sep 17 00:00:00 2001
From: oglassdev <23040613+oglassdev@users.noreply.github.com>
Date: Thu, 15 Oct 2026 20:18:38 -0400
Subject: [PATCH 1/3] feat(example): add a full-screen lobby and a horse-mode
lookout
The lobby pages a world list with arrows, the scroll wheel, and a fixed
repeater with tooltips, renames the foundry through a native text prompt,
and toggles ready on right click. The lookout shows horse mode with a bar.
/lobby and /lookout open them.
The size budget rises to 1.15 MB: the two windows add 12 text-row fonts
(~182 KB), their generated Kotlin, descriptor entries, and the full-screen
shaders.
---
example/pack/rpp.config.ts | 2 +-
example/pack/src/window/handles.ts | 17 +
example/pack/src/window/index.ts | 9 +
example/pack/src/window/lobby.tsx | 74 ++
example/pack/src/window/lookout.tsx | 30 +
.../com/chunkzero/window/example/Main.kt | 15 +-
.../com/chunkzero/window/example/MyLobby.kt | 66 ++
.../com/chunkzero/window/example/MyLookout.kt | 18 +
.../example/generated/LobbyFullscreen.kt | 83 ++
.../example/generated/LookoutFullscreen.kt | 30 +
.../window/example/generated/WindowFonts.kt | 12 +
.../generated/WindowFullscreenDefinitions.kt | 10 +
.../generated/WindowFullscreenEntries.kt | 717 ++++++++++++++++++
.../example/generated/WindowPackData.kt | 2 +
14 files changed, 1082 insertions(+), 3 deletions(-)
create mode 100644 example/pack/src/window/lobby.tsx
create mode 100644 example/pack/src/window/lookout.tsx
create mode 100644 jvm/example/src/main/kotlin/com/chunkzero/window/example/MyLobby.kt
create mode 100644 jvm/example/src/main/kotlin/com/chunkzero/window/example/MyLookout.kt
create mode 100644 jvm/example/src/main/kotlin/com/chunkzero/window/example/generated/LobbyFullscreen.kt
create mode 100644 jvm/example/src/main/kotlin/com/chunkzero/window/example/generated/LookoutFullscreen.kt
create mode 100644 jvm/example/src/main/kotlin/com/chunkzero/window/example/generated/WindowFullscreenDefinitions.kt
create mode 100644 jvm/example/src/main/kotlin/com/chunkzero/window/example/generated/WindowFullscreenEntries.kt
diff --git a/example/pack/rpp.config.ts b/example/pack/rpp.config.ts
index 262c3856..dce560bc 100644
--- a/example/pack/rpp.config.ts
+++ b/example/pack/rpp.config.ts
@@ -18,7 +18,7 @@ export default defineConfig({
namespace: "window",
coreShaders: { hud: true, hover: true },
vanilla: { anvil: { field: "search_field" }, hud: { experience: false }, slotHighlight: false },
- size: { report: true, budget: 1_000_000 },
+ size: { report: true, budget: 1_150_000 },
previews: { output: "build/previews" },
kotlin: {
package: "com.chunkzero.window.example.generated",
diff --git a/example/pack/src/window/handles.ts b/example/pack/src/window/handles.ts
index c4986abe..7cf5e6d8 100644
--- a/example/pack/src/window/handles.ts
+++ b/example/pack/src/window/handles.ts
@@ -71,3 +71,20 @@ export const amount = selection("amount", ["one", "five", "max"]);
export const rank = sprite("rank", { only: ["rank_1", "rank_2", "rank_3"] });
export const rankName = text("rank_name");
export const openShop = action("open_shop");
+
+// Lobby and lookout, the full-screen windows; `tone` is shared
+export const WORLDS = 3;
+const worlds = { shape: [WORLDS] } as const;
+export const worldName = text("world_name", worlds);
+export const pickWorld = action("pick_world", worlds);
+export const previousWorld = action("previous_world");
+export const nextWorld = action("next_world");
+export const foundryName = text("foundry_name");
+export const rename = action("rename");
+export const lobbyStatus = text("lobby_status");
+export const ready = flag("ready");
+export const play = action("play");
+
+/** How far the lookout's signal has charged. */
+export const signal = fraction("signal");
+export const charge = action("charge");
diff --git a/example/pack/src/window/index.ts b/example/pack/src/window/index.ts
index 3e5219fb..9e8400a1 100644
--- a/example/pack/src/window/index.ts
+++ b/example/pack/src/window/index.ts
@@ -5,6 +5,8 @@ import { art } from "plugin:window/theme/industrial";
import { ranks } from "./art.ts";
import clock, { runtime } from "./clock_hud.ts";
import * as handles from "./handles.ts";
+import lobby from "./lobby.tsx";
+import lookout from "./lookout.tsx";
import search from "./search.tsx";
import shop from "./shop.tsx";
import statusHuds, { bottomCenter, coins, hint, power, rate, topLeft, waveBar, waveCharge } from "./status_hud.tsx";
@@ -33,6 +35,7 @@ export default defineWindows({
sprites: { searchField: art.inputField, ...ranks },
windows: [shop, search, upgrades],
huds: statusHuds,
+ fullscreens: [lobby, lookout],
// `rpp build` writes these to build/previews/, one PNG per preview.
previews: [
preview(shop, [
@@ -76,5 +79,11 @@ export default defineWindows({
{ name: "status_bottom_center_danger", scale: 3 },
),
preview(waveBar, [[waveCharge, 0.6]], { scale: 3 }),
+ preview(lobby, [
+ [handles.foundryName, "Steve's Foundry"],
+ [handles.lobbyStatus, "Ready for Emberly"],
+ [handles.worldName, ["Ironvale", "Emberly", "Ashmoor"]],
+ [handles.ready, true],
+ ]),
],
});
diff --git a/example/pack/src/window/lobby.tsx b/example/pack/src/window/lobby.tsx
new file mode 100644
index 00000000..56bc1e0d
--- /dev/null
+++ b/example/pack/src/window/lobby.tsx
@@ -0,0 +1,74 @@
+import { Case, Fullscreen, Image, Region, Switch, Text, create } from "plugin:window/ui";
+import { Button, Column, Repeater, Row, art } from "plugin:window/theme/industrial";
+
+import { icons } from "./art.ts";
+import {
+ WORLDS,
+ foundryName,
+ lobbyStatus,
+ nextWorld,
+ pickWorld,
+ play,
+ previousWorld,
+ ready,
+ rename,
+ tone,
+ worldName,
+} from "./handles.ts";
+
+const s = create({
+ title: { color: "#ffe2b8" },
+ plate: { frame: art.recess, width: 160, height: 16, justify: "center", align: "center" },
+ arrow: { width: 16, height: 18 },
+ action: { width: 60, height: 18 },
+});
+
+/**
+ * A main menu drawn over the whole screen while the world turns behind it. Click the name to rename the foundry in a
+ * native dialog, pick a world from the paged list (the arrows or the scroll wheel move it), right click anywhere to
+ * ready up, then play.
+ */
+export default (
+
+
+ Foundry Lobby
+
+
+
+
+
+
+
+
+ {(i) => }
+
+
+
+
+
+
+ Ready
+
+
+ Right click when ready
+
+
+
+
+
+
+
+
+);
diff --git a/example/pack/src/window/lookout.tsx b/example/pack/src/window/lookout.tsx
new file mode 100644
index 00000000..1bf9a4b7
--- /dev/null
+++ b/example/pack/src/window/lookout.tsx
@@ -0,0 +1,30 @@
+import { Bar, Fullscreen, Text } from "plugin:window/ui";
+import { Button, Column, Row, art } from "plugin:window/theme/industrial";
+
+import { barTrack, toneFill } from "./art.ts";
+import { charge, signal } from "./handles.ts";
+
+/** A horse-mode window: the view stays locked while the player aims the cursor, and charging fills the signal bar. */
+export default (
+
+
+ Lookout
+
+
+
+
+
+
+
+);
diff --git a/jvm/example/src/main/kotlin/com/chunkzero/window/example/Main.kt b/jvm/example/src/main/kotlin/com/chunkzero/window/example/Main.kt
index b139f2aa..3d884641 100644
--- a/jvm/example/src/main/kotlin/com/chunkzero/window/example/Main.kt
+++ b/jvm/example/src/main/kotlin/com/chunkzero/window/example/Main.kt
@@ -33,8 +33,8 @@ import java.nio.file.Path
* jvm/gradlew -p jvm :example:run
* ```
*
- * Then connect a Minecraft 26.2 client and the shop opens on spawn. Reopen it with `/shop` or the hotbar emerald, and
- * open the workshop with `/upgrades` or the shop's Workshop button.
+ * Then connect a Minecraft 26.2 client and the shop opens on spawn. Reopen it with `/shop` or the hotbar emerald, open
+ * the workshop with `/upgrades` or the shop's Workshop button, and the full-screen windows with `/lobby` and `/lookout`.
*/
fun main() {
val server = MinecraftServer.init()
@@ -50,6 +50,7 @@ fun main() {
registerShopAccess(market)
registerToneCommand()
registerBarCommand()
+ registerFullscreens()
server.start("0.0.0.0", serverPort)
println("Window example server listening on $serverPort — pack served at $packUrl.")
@@ -128,6 +129,16 @@ private fun registerShopAccess(market: Market) {
}
}
+/** `/lobby` opens the full-screen lobby, and `/lookout` the horse-mode lookout; sneak to leave either. */
+private fun registerFullscreens() {
+ val lobby = Command("lobby")
+ lobby.setDefaultExecutor { sender, _ -> (sender as? Player)?.let { MyLobby(it).open() } }
+ MinecraftServer.getCommandManager().register(lobby)
+ val lookout = Command("lookout")
+ lookout.setDefaultExecutor { sender, _ -> (sender as? Player)?.let { MyLookout(it).open() } }
+ MinecraftServer.getCommandManager().register(lookout)
+}
+
// A fresh view per open: each carries its own reactive state.
private fun openShop(
player: Player,
diff --git a/jvm/example/src/main/kotlin/com/chunkzero/window/example/MyLobby.kt b/jvm/example/src/main/kotlin/com/chunkzero/window/example/MyLobby.kt
new file mode 100644
index 00000000..2ad52bb4
--- /dev/null
+++ b/jvm/example/src/main/kotlin/com/chunkzero/window/example/MyLobby.kt
@@ -0,0 +1,66 @@
+package com.chunkzero.window.example
+
+import com.chunkzero.window.FullscreenClick
+import com.chunkzero.window.example.generated.LobbyFullscreen
+import net.kyori.adventure.text.Component
+import net.minestom.server.entity.Player
+import kotlin.math.abs
+
+/**
+ * The full-screen lobby: a paged world list moved by the arrows or the scroll wheel, a foundry name renamed through a
+ * native dialog, and a ready flag flipped by right clicking anywhere.
+ */
+class MyLobby(
+ player: Player,
+) : LobbyFullscreen(player) {
+ private var name by state("${player.username}'s Foundry")
+ private var isReady by state(false)
+ private val worlds = list(cells = WORLD_NAME_SIZE, key = { it: String -> it }, step = 1) { WORLDS }
+
+ override fun foundryName(): Component = fit("foundry_name", Component.text(name))
+
+ override fun lobbyStatus(): Component =
+ Component.text(if (isReady) "Ready for ${worlds.selected}" else "${worlds.selected} selected")
+
+ override fun tone(): Tone {
+ val pinned = pinnedTone(player)
+ if (pinned != null) return Tone.entries.first { it.value == pinned }
+ return if (isReady) Tone.OK else Tone.WARN
+ }
+
+ override fun ready(): Boolean = isReady
+
+ override fun worldName(index: Int): Component = Component.text(worlds.at(index).orEmpty())
+
+ override fun onPickWorld(
+ index: Int,
+ click: FullscreenClick,
+ ) {
+ worlds.selectAt(index)
+ }
+
+ override fun onPreviousWorld(click: FullscreenClick) = worlds.previous()
+
+ override fun onNextWorld(click: FullscreenClick) = worlds.next()
+
+ override fun onScroll(steps: Int) = repeat(abs(steps)) { if (steps > 0) worlds.next() else worlds.previous() }
+
+ override fun onRightClick(click: FullscreenClick) {
+ isReady = !isReady
+ }
+
+ /** Asks for a new name in a native dialog; the lobby is back once the player confirms or cancels. */
+ override fun onRename(click: FullscreenClick) {
+ promptText(Component.text("Name your foundry"), name) { text ->
+ text?.trim()?.takeIf(String::isNotEmpty)?.let { name = it }
+ }
+ }
+
+ override fun onPlay(click: FullscreenClick) {
+ if (!isReady) return
+ close()
+ player.sendMessage(Component.text("$name joins ${worlds.selected}."))
+ }
+}
+
+private val WORLDS = listOf("Ironvale", "Emberly", "Ashmoor", "Slagport", "Rustwood", "Kilnmere", "Coalcrag")
diff --git a/jvm/example/src/main/kotlin/com/chunkzero/window/example/MyLookout.kt b/jvm/example/src/main/kotlin/com/chunkzero/window/example/MyLookout.kt
new file mode 100644
index 00000000..70bc86ce
--- /dev/null
+++ b/jvm/example/src/main/kotlin/com/chunkzero/window/example/MyLookout.kt
@@ -0,0 +1,18 @@
+package com.chunkzero.window.example
+
+import com.chunkzero.window.FullscreenClick
+import com.chunkzero.window.example.generated.LookoutFullscreen
+import net.minestom.server.entity.Player
+
+/** The horse-mode lookout: each Charge click fills a quarter of the signal bar, and a full bar starts over. */
+class MyLookout(
+ player: Player,
+) : LookoutFullscreen(player) {
+ private var charges by state(0)
+
+ override fun signal(): Float = charges / 4f
+
+ override fun onCharge(click: FullscreenClick) {
+ charges = (charges + 1) % 5
+ }
+}
diff --git a/jvm/example/src/main/kotlin/com/chunkzero/window/example/generated/LobbyFullscreen.kt b/jvm/example/src/main/kotlin/com/chunkzero/window/example/generated/LobbyFullscreen.kt
new file mode 100644
index 00000000..c8d099d9
--- /dev/null
+++ b/jvm/example/src/main/kotlin/com/chunkzero/window/example/generated/LobbyFullscreen.kt
@@ -0,0 +1,83 @@
+// Generated by window-codegen — do not edit.
+package com.chunkzero.window.example.generated
+
+import com.chunkzero.window.FullscreenClick
+import com.chunkzero.window.FullscreenScope
+import com.chunkzero.window.FullscreenView
+import com.chunkzero.window.minestom.MinestomFullscreenHost
+import net.kyori.adventure.text.Component
+import net.minestom.server.entity.Player
+
+/** Typed view for the `lobby` full-screen window. Implement the abstract members. */
+public abstract class LobbyFullscreen(protected val player: Player) : FullscreenView(WindowFullscreenDefinitions.lobby, MinestomFullscreenHost(player)) {
+ /** Render the `foundry_name` text. */
+ protected abstract fun foundryName(): Component
+
+ /** Render the `lobby_status` text. */
+ protected abstract fun lobbyStatus(): Component
+
+ /** Handle a `next_world` click. */
+ protected abstract fun onNextWorld(click: FullscreenClick)
+
+ /** Handle a `pick_world` click; `index` is the clicked entry, row-major. */
+ protected abstract fun onPickWorld(index: Int, click: FullscreenClick)
+
+ /** Handle a `play` click. */
+ protected abstract fun onPlay(click: FullscreenClick)
+
+ /** Handle a `previous_world` click. */
+ protected abstract fun onPreviousWorld(click: FullscreenClick)
+
+ /** The `ready` flag. */
+ protected abstract fun ready(): Boolean
+
+ /** Handle a `rename` click. */
+ protected abstract fun onRename(click: FullscreenClick)
+
+ /** The values of `tone`. */
+ public enum class Tone(public val value: String) {
+ OK("ok"),
+ WARN("warn"),
+ DANGER("danger"),
+ }
+
+ /** The palette entry the `tone` tint draws in. */
+ protected abstract fun tone(): Tone
+
+ /** Render the `world_name` text. */
+ protected abstract fun worldName(index: Int): Component
+
+ /** Handle a scroll of the wheel, as the signed steps the hotbar selection moved. */
+ protected open fun onScroll(steps: Int) {}
+
+ /** Handle a right click anywhere on the canvas. */
+ protected open fun onRightClick(click: FullscreenClick) {}
+
+ final override fun FullscreenScope.bind() {
+ slot("foundry_name") { foundryName() }
+ slot("lobby_status") { lobbyStatus() }
+ button("next_world", ::onNextWorld)
+ for (index in 0 until PICK_WORLD_SIZE) {
+ button("pick_world[$index]") { click ->
+ onPickWorld(index, click)
+ }
+ }
+ button("play", ::onPlay)
+ button("previous_world", ::onPreviousWorld)
+ switch("ready") { ready().toString() }
+ button("rename", ::onRename)
+ tint("tone") { tone().value }
+ for (index in 0 until WORLD_NAME_SIZE) {
+ slot("world_name[$index]") { worldName(index) }
+ }
+ scroll(::onScroll)
+ rightClick(::onRightClick)
+ }
+
+ public companion object {
+ /** A shape dimension of `pick_world`. */
+ public const val PICK_WORLD_SIZE: Int = 3
+ /** A shape dimension of `world_name`. */
+ public const val WORLD_NAME_SIZE: Int = 3
+ }
+}
diff --git a/jvm/example/src/main/kotlin/com/chunkzero/window/example/generated/LookoutFullscreen.kt b/jvm/example/src/main/kotlin/com/chunkzero/window/example/generated/LookoutFullscreen.kt
new file mode 100644
index 00000000..9ed24de0
--- /dev/null
+++ b/jvm/example/src/main/kotlin/com/chunkzero/window/example/generated/LookoutFullscreen.kt
@@ -0,0 +1,30 @@
+// Generated by window-codegen — do not edit.
+package com.chunkzero.window.example.generated
+
+import com.chunkzero.window.FullscreenClick
+import com.chunkzero.window.FullscreenScope
+import com.chunkzero.window.FullscreenView
+import com.chunkzero.window.minestom.MinestomFullscreenHost
+import net.minestom.server.entity.Player
+
+/** Typed view for the `lookout` full-screen window. Implement the abstract members. */
+public abstract class LookoutFullscreen(protected val player: Player) : FullscreenView(WindowFullscreenDefinitions.lookout, MinestomFullscreenHost(player)) {
+ /** Handle a `charge` click. */
+ protected abstract fun onCharge(click: FullscreenClick)
+
+ /** How much of the `signal` bar is filled, from 0 to 1. */
+ protected abstract fun signal(): Float
+
+ /** Handle a scroll of the wheel, as the signed steps the hotbar selection moved. */
+ protected open fun onScroll(steps: Int) {}
+
+ /** Handle a right click anywhere on the canvas. */
+ protected open fun onRightClick(click: FullscreenClick) {}
+
+ final override fun FullscreenScope.bind() {
+ button("charge", ::onCharge)
+ bar("signal") { signal() }
+ scroll(::onScroll)
+ rightClick(::onRightClick)
+ }
+}
diff --git a/jvm/example/src/main/kotlin/com/chunkzero/window/example/generated/WindowFonts.kt b/jvm/example/src/main/kotlin/com/chunkzero/window/example/generated/WindowFonts.kt
index 0bb5f0e3..793c4b00 100644
--- a/jvm/example/src/main/kotlin/com/chunkzero/window/example/generated/WindowFonts.kt
+++ b/jvm/example/src/main/kotlin/com/chunkzero/window/example/generated/WindowFonts.kt
@@ -842,5 +842,17 @@ internal object WindowFonts {
"window:small_caps/y88" to fontMetrics0,
"window:small_caps/y96" to fontMetrics0,
"window:small_caps/ym1" to fontMetrics0,
+ "window:y113" to vanillaMetrics,
+ "window:y116" to vanillaMetrics,
+ "window:y136" to vanillaMetrics,
+ "window:y144" to vanillaMetrics,
+ "window:y154" to vanillaMetrics,
+ "window:y156" to vanillaMetrics,
+ "window:y162" to vanillaMetrics,
+ "window:y176" to vanillaMetrics,
+ "window:y195" to vanillaMetrics,
+ "window:y207" to vanillaMetrics,
+ "window:y80" to vanillaMetrics,
+ "window:y98" to vanillaMetrics,
)
}
diff --git a/jvm/example/src/main/kotlin/com/chunkzero/window/example/generated/WindowFullscreenDefinitions.kt b/jvm/example/src/main/kotlin/com/chunkzero/window/example/generated/WindowFullscreenDefinitions.kt
new file mode 100644
index 00000000..45ae4b0e
--- /dev/null
+++ b/jvm/example/src/main/kotlin/com/chunkzero/window/example/generated/WindowFullscreenDefinitions.kt
@@ -0,0 +1,10 @@
+// Generated by window-codegen — do not edit.
+package com.chunkzero.window.example.generated
+
+import com.chunkzero.window.FullscreenDefinition
+
+/** Typed full-screen window definitions of this Window pack. */
+public object WindowFullscreenDefinitions {
+ public val lobby: FullscreenDefinition = FullscreenDefinition(WindowPackData.manifest, "lobby")
+ public val lookout: FullscreenDefinition = FullscreenDefinition(WindowPackData.manifest, "lookout")
+}
diff --git a/jvm/example/src/main/kotlin/com/chunkzero/window/example/generated/WindowFullscreenEntries.kt b/jvm/example/src/main/kotlin/com/chunkzero/window/example/generated/WindowFullscreenEntries.kt
new file mode 100644
index 00000000..c9516ddb
--- /dev/null
+++ b/jvm/example/src/main/kotlin/com/chunkzero/window/example/generated/WindowFullscreenEntries.kt
@@ -0,0 +1,717 @@
+// Generated by window-codegen — do not edit.
+package com.chunkzero.window.example.generated
+
+import com.chunkzero.window.manifest.Align
+import com.chunkzero.window.manifest.ArtEntry
+import com.chunkzero.window.manifest.BarEntry
+import com.chunkzero.window.manifest.ControlEntry
+import com.chunkzero.window.manifest.ControlTooltipEntry
+import com.chunkzero.window.manifest.CursorEntry
+import com.chunkzero.window.manifest.FullscreenCanvasEntry
+import com.chunkzero.window.manifest.FullscreenEntry
+import com.chunkzero.window.manifest.FullscreenMode
+import com.chunkzero.window.manifest.FullscreenSurfaceEntry
+import com.chunkzero.window.manifest.HorseEntry
+import com.chunkzero.window.manifest.LayerEntry
+import com.chunkzero.window.manifest.LayerKind
+import com.chunkzero.window.manifest.SlotEntry
+import com.chunkzero.window.manifest.SwitchCaseEntry
+import com.chunkzero.window.manifest.SwitchEntry
+
+/** Compiled full-screen entries and canvas contract of this Window pack. */
+internal object WindowFullscreenEntries {
+ val all: Map =
+ mapOf(
+ "lobby" to FullscreenEntry(
+ surface = FullscreenSurfaceEntry(
+ kind = "fullscreen",
+ mode = FullscreenMode.UNLOCKED,
+ size = listOf(480, 270),
+ ),
+ static = "\uDB80\uDC11\uDB80\uDC10\uDB80\uDC0F\uDB80\uDC0E\uDB80\uDC0D\uF7CD\uDB80\uDC02\uDB80\uDC04\uDB80\uDC05\uDB80\uDC08\uDB80\uDC0A",
+ staticMarker = "#a00400",
+ slots = mapOf(
+ "foundry_name" to SlotEntry(
+ x = 165,
+ y = 98,
+ width = 150,
+ align = Align.CENTER,
+ font = "window:y98",
+ color = "#ffe2b8",
+ shadow = false,
+ bold = false,
+ italic = false,
+ underlined = false,
+ strikethrough = false,
+ obfuscated = false,
+ text = null,
+ shaderMarker = null,
+ shaderColor = null,
+ ),
+ "label_0" to SlotEntry(
+ x = 203,
+ y = 80,
+ width = 75,
+ align = Align.LEFT,
+ font = "window:y80",
+ color = "#ffe2b8",
+ shadow = false,
+ bold = false,
+ italic = false,
+ underlined = false,
+ strikethrough = false,
+ obfuscated = false,
+ text = "Foundry Lobby",
+ shaderMarker = null,
+ shaderColor = null,
+ ),
+ "label_1" to SlotEntry(
+ x = 146,
+ y = 136,
+ width = 4,
+ align = Align.CENTER,
+ font = "window:y136",
+ color = "#ffffff",
+ shadow = false,
+ bold = false,
+ italic = false,
+ underlined = false,
+ strikethrough = false,
+ obfuscated = false,
+ text = "<",
+ shaderMarker = null,
+ shaderColor = null,
+ ),
+ "label_2" to SlotEntry(
+ x = 330,
+ y = 136,
+ width = 4,
+ align = Align.CENTER,
+ font = "window:y136",
+ color = "#ffffff",
+ shadow = false,
+ bold = false,
+ italic = false,
+ underlined = false,
+ strikethrough = false,
+ obfuscated = false,
+ text = ">",
+ shaderMarker = null,
+ shaderColor = null,
+ ),
+ "label_3" to SlotEntry(
+ x = 196,
+ y = 156,
+ width = 29,
+ align = Align.LEFT,
+ font = "window:y156",
+ color = "#3ddc84",
+ shadow = false,
+ bold = false,
+ italic = false,
+ underlined = false,
+ strikethrough = false,
+ obfuscated = false,
+ text = "Ready",
+ shaderMarker = null,
+ shaderColor = null,
+ ),
+ "label_4" to SlotEntry(
+ x = 185,
+ y = 156,
+ width = 111,
+ align = Align.LEFT,
+ font = "window:y156",
+ color = "#f2c9a0",
+ shadow = false,
+ bold = false,
+ italic = false,
+ underlined = false,
+ strikethrough = false,
+ obfuscated = false,
+ text = "Right click when ready",
+ shaderMarker = null,
+ shaderColor = null,
+ ),
+ "label_5" to SlotEntry(
+ x = 197,
+ y = 176,
+ width = 20,
+ align = Align.CENTER,
+ font = "window:y176",
+ color = "#ffffff",
+ shadow = false,
+ bold = false,
+ italic = false,
+ underlined = false,
+ strikethrough = false,
+ obfuscated = false,
+ text = "Play",
+ shaderMarker = null,
+ shaderColor = null,
+ ),
+ "label_6" to SlotEntry(
+ x = 265,
+ y = 176,
+ width = 17,
+ align = Align.CENTER,
+ font = "window:y176",
+ color = "#ffffff",
+ shadow = false,
+ bold = false,
+ italic = false,
+ underlined = false,
+ strikethrough = false,
+ obfuscated = false,
+ text = "Quit",
+ shaderMarker = null,
+ shaderColor = null,
+ ),
+ "lobby_status" to SlotEntry(
+ x = 132,
+ y = 116,
+ width = 216,
+ align = Align.CENTER,
+ font = "window:y116",
+ color = "#3ddc84",
+ shadow = false,
+ bold = false,
+ italic = false,
+ underlined = false,
+ strikethrough = false,
+ obfuscated = false,
+ text = null,
+ shaderMarker = null,
+ shaderColor = null,
+ tint = "tone",
+ tintMarkers = mapOf(
+ "danger" to "#a01004",
+ "ok" to "#a01002",
+ "warn" to "#a01003",
+ ),
+ ),
+ "world_name[0]" to SlotEntry(
+ x = 162,
+ y = 136,
+ width = 48,
+ align = Align.CENTER,
+ font = "window:y136",
+ color = "#ffffff",
+ shadow = false,
+ bold = false,
+ italic = false,
+ underlined = false,
+ strikethrough = false,
+ obfuscated = false,
+ text = null,
+ shaderMarker = null,
+ shaderColor = null,
+ ),
+ "world_name[1]" to SlotEntry(
+ x = 216,
+ y = 136,
+ width = 48,
+ align = Align.CENTER,
+ font = "window:y136",
+ color = "#ffffff",
+ shadow = false,
+ bold = false,
+ italic = false,
+ underlined = false,
+ strikethrough = false,
+ obfuscated = false,
+ text = null,
+ shaderMarker = null,
+ shaderColor = null,
+ ),
+ "world_name[2]" to SlotEntry(
+ x = 270,
+ y = 136,
+ width = 48,
+ align = Align.CENTER,
+ font = "window:y136",
+ color = "#ffffff",
+ shadow = false,
+ bold = false,
+ italic = false,
+ underlined = false,
+ strikethrough = false,
+ obfuscated = false,
+ text = null,
+ shaderMarker = null,
+ shaderColor = null,
+ ),
+ ),
+ controls = mapOf(
+ "next_world" to ControlEntry(
+ index = 5,
+ x = 324,
+ y = 131,
+ width = 16,
+ height = 18,
+ action = "next_world",
+ defaultAction = null,
+ hover = true,
+ outline = "\uDB80\uDC13\uDB80\uDC11\uDB80\uDC0D\uE3F4\uDB80\uDC02\uDB80\uDC04\uDB80\uDC06\uDB80\uDC08\uDB80\uDC0A",
+ source = "region `next_world`",
+ ),
+ "pick_world[0]" to ControlEntry(
+ index = 2,
+ x = 160,
+ y = 132,
+ width = 52,
+ height = 16,
+ action = "pick_world[0]",
+ defaultAction = null,
+ hover = true,
+ outline = "\uDB80\uDC12\uDB80\uDC10\uED3C\uDB80\uDC03\uDB80\uDC04\uDB80\uDC06\uDB80\uDC08\uDB80\uDC0A",
+ tooltip = ControlTooltipEntry(
+ panel = "\uDB80\uDC12\uDB80\uDC0F\uDB80\uDC0C\uDB80\uDC0B\uED20\uDB80\uDC03\uDB80\uDC04\uDB80\uDC05\uDB80\uDC09",
+ marker = "#a3f000",
+ lines = listOf(
+ SlotEntry(
+ x = 151,
+ y = 154,
+ width = 70,
+ align = Align.LEFT,
+ font = "window:y154",
+ color = "#ffffff",
+ shadow = false,
+ bold = false,
+ italic = false,
+ underlined = false,
+ strikethrough = false,
+ obfuscated = false,
+ text = "Click to select",
+ shaderMarker = "#a3f400",
+ shaderColor = null,
+ source = "region `pick_world[0]`",
+ ),
+ ),
+ ),
+ source = "region `pick_world[0]`",
+ ),
+ "pick_world[1]" to ControlEntry(
+ index = 3,
+ x = 214,
+ y = 132,
+ width = 52,
+ height = 16,
+ action = "pick_world[1]",
+ defaultAction = null,
+ hover = true,
+ outline = "\uDB80\uDC12\uDB80\uDC11\uDB80\uDC0F\uDB80\uDC0D\uDB80\uDC0C\uF465\uDB80\uDC02\uDB80\uDC07\uDB80\uDC09\uDB80\uDC0A",
+ tooltip = ControlTooltipEntry(
+ panel = "\uDB80\uDC12\uDB80\uDC11\uDB80\uDC0E\uDB80\uDC0B\uF030\uDB80\uDC02\uDB80\uDC06\uDB80\uDC07",
+ marker = "#a3f000",
+ lines = listOf(
+ SlotEntry(
+ x = 205,
+ y = 154,
+ width = 70,
+ align = Align.LEFT,
+ font = "window:y154",
+ color = "#ffffff",
+ shadow = false,
+ bold = false,
+ italic = false,
+ underlined = false,
+ strikethrough = false,
+ obfuscated = false,
+ text = "Click to select",
+ shaderMarker = "#a3f400",
+ shaderColor = null,
+ source = "region `pick_world[1]`",
+ ),
+ ),
+ ),
+ source = "region `pick_world[1]`",
+ ),
+ "pick_world[2]" to ControlEntry(
+ index = 4,
+ x = 268,
+ y = 132,
+ width = 52,
+ height = 16,
+ action = "pick_world[2]",
+ defaultAction = null,
+ hover = true,
+ outline = "\uDB80\uDC13\uDB80\uDC0E\uDB80\uDC0D\uE3AB\uDB80\uDC02\uDB80\uDC04\uDB80\uDC0A",
+ tooltip = ControlTooltipEntry(
+ panel = "\uDB80\uDC12\uDB80\uDC11\uDB80\uDC10\uDB80\uDC0F\uDB80\uDC0E\uDB80\uDC0D\uDB80\uDC0C\uDB80\uDC0B\uECBA\uDB80\uDC02\uDB80\uDC04\uDB80\uDC07\uDB80\uDC08\uDB80\uDC09",
+ marker = "#a3f000",
+ lines = listOf(
+ SlotEntry(
+ x = 259,
+ y = 154,
+ width = 70,
+ align = Align.LEFT,
+ font = "window:y154",
+ color = "#ffffff",
+ shadow = false,
+ bold = false,
+ italic = false,
+ underlined = false,
+ strikethrough = false,
+ obfuscated = false,
+ text = "Click to select",
+ shaderMarker = "#a3f400",
+ shaderColor = null,
+ source = "region `pick_world[2]`",
+ ),
+ ),
+ ),
+ source = "region `pick_world[2]`",
+ ),
+ "play" to ControlEntry(
+ index = 6,
+ x = 177,
+ y = 171,
+ width = 60,
+ height = 18,
+ action = "play",
+ defaultAction = null,
+ hover = true,
+ outline = "\uDB80\uDC12\uDB80\uDC10\uDB80\uDC0F\uDB80\uDC0B\uE224\uDB80\uDC03\uDB80\uDC04\uDB80\uDC05\uDB80\uDC07\uDB80\uDC08\uDB80\uDC09",
+ tooltip = ControlTooltipEntry(
+ panel = "\uDB80\uDC11\uDB80\uDC0F\uDB80\uDC0E\uDB80\uDC0B\uE157\uDB80\uDC02\uDB80\uDC04\uDB80\uDC08\uDB80\uDC09\uDB80\uDC0A",
+ marker = "#a3f000",
+ lines = listOf(
+ SlotEntry(
+ x = 93,
+ y = 195,
+ width = 20,
+ align = Align.LEFT,
+ font = "window:y195",
+ color = "#ffffff",
+ shadow = false,
+ bold = false,
+ italic = false,
+ underlined = false,
+ strikethrough = false,
+ obfuscated = false,
+ text = "Play",
+ shaderMarker = "#a3f400",
+ shaderColor = null,
+ source = "region `play`",
+ ),
+ SlotEntry(
+ x = 93,
+ y = 207,
+ width = 229,
+ align = Align.LEFT,
+ font = "window:y207",
+ color = "#aaaaaa",
+ shadow = false,
+ bold = false,
+ italic = false,
+ underlined = false,
+ strikethrough = false,
+ obfuscated = false,
+ text = "Joins the selected world once you are ready",
+ shaderMarker = "#a3f406",
+ shaderColor = null,
+ source = "region `play`",
+ ),
+ ),
+ ),
+ source = "region `play`",
+ ),
+ "previous_world" to ControlEntry(
+ index = 1,
+ x = 140,
+ y = 131,
+ width = 16,
+ height = 18,
+ action = "previous_world",
+ defaultAction = null,
+ hover = true,
+ outline = "\uDB80\uDC12\uDB80\uDC0E\uDB80\uDC0D\uF27E\uDB80\uDC03\uDB80\uDC06\uDB80\uDC07\uDB80\uDC08\uDB80\uDC0A",
+ source = "region `previous_world`",
+ ),
+ "rename" to ControlEntry(
+ index = 0,
+ x = 160,
+ y = 94,
+ width = 160,
+ height = 16,
+ action = "rename",
+ defaultAction = null,
+ hover = true,
+ outline = "\uDB80\uDC12\uDB80\uDC10\uE911\uDB80\uDC02\uDB80\uDC04\uDB80\uDC0A",
+ tooltip = ControlTooltipEntry(
+ panel = "\uDB80\uDC12\uDB80\uDC11\uDB80\uDC0D\uDB80\uDC0C\uEEC4\uDB80\uDC02\uDB80\uDC06\uDB80\uDC07\uDB80\uDC09",
+ marker = "#a3f000",
+ lines = listOf(
+ SlotEntry(
+ x = 202,
+ y = 116,
+ width = 75,
+ align = Align.LEFT,
+ font = "window:y116",
+ color = "#ffffff",
+ shadow = false,
+ bold = false,
+ italic = false,
+ underlined = false,
+ strikethrough = false,
+ obfuscated = false,
+ text = "Click to rename",
+ shaderMarker = "#a3f400",
+ shaderColor = null,
+ source = "region `rename`",
+ ),
+ ),
+ ),
+ source = "region `rename`",
+ ),
+ "window:close" to ControlEntry(
+ index = 7,
+ x = 243,
+ y = 171,
+ width = 60,
+ height = 18,
+ action = "window:close",
+ defaultAction = "window:close",
+ hover = true,
+ outline = "\uDB80\uDC12\uDB80\uDC11\uDB80\uDC10\uDB80\uDC0F\uDB80\uDC0C\uDB80\uDC0B\uF59E\uDB80\uDC02\uDB80\uDC05\uDB80\uDC06",
+ source = "region `window:close`",
+ ),
+ ),
+ switches = mapOf(
+ "ready" to SwitchEntry(
+ cases = listOf(
+ SwitchCaseEntry(
+ value = "true",
+ static = "\uDB80\uDC12\uDB80\uDC10\uDB80\uDC0F\uDB80\uDC0E\uDB80\uDC0B\uF0CB\uDB80\uDC03\uDB80\uDC04\uDB80\uDC09\uDB80\uDC0A",
+ slots = listOf("label_3"),
+ spriteSlots = emptyList(),
+ regions = emptyList(),
+ switches = emptyList(),
+ ),
+ SwitchCaseEntry(
+ value = "false",
+ static = "",
+ slots = listOf("label_4"),
+ spriteSlots = emptyList(),
+ regions = emptyList(),
+ switches = emptyList(),
+ ),
+ ),
+ source = null,
+ ),
+ ),
+ tints = mapOf(
+ "tone" to mapOf(
+ "danger" to "#ff4d4d",
+ "ok" to "#3ddc84",
+ "warn" to "#ffb020",
+ ),
+ ),
+ layers = listOf(
+ LayerEntry(LayerKind.SLOT, "label_0"),
+ LayerEntry(LayerKind.SLOT, "foundry_name"),
+ LayerEntry(LayerKind.SLOT, "lobby_status"),
+ LayerEntry(LayerKind.SLOT, "label_1"),
+ LayerEntry(LayerKind.SLOT, "world_name[0]"),
+ LayerEntry(LayerKind.SLOT, "world_name[1]"),
+ LayerEntry(LayerKind.SLOT, "world_name[2]"),
+ LayerEntry(LayerKind.SLOT, "label_2"),
+ LayerEntry(LayerKind.SWITCH, "ready"),
+ LayerEntry(LayerKind.SLOT, "label_3"),
+ LayerEntry(LayerKind.SLOT, "label_4"),
+ LayerEntry(LayerKind.SLOT, "label_5"),
+ LayerEntry(LayerKind.SLOT, "label_6"),
+ ),
+ markers = mapOf(
+ "foundry_name" to "#a00c01",
+ "label_0" to "#a00801",
+ "label_1" to "#a01400",
+ "label_2" to "#a02400",
+ "label_3" to "#a02c02",
+ "label_4" to "#a03005",
+ "label_5" to "#a03400",
+ "label_6" to "#a03800",
+ "lobby_status" to "#a01002",
+ "ready" to "#a02800",
+ "world_name[0]" to "#a01800",
+ "world_name[1]" to "#a01c00",
+ "world_name[2]" to "#a02000",
+ ),
+ backdropMarker = "#a80007",
+ ),
+ "lookout" to FullscreenEntry(
+ surface = FullscreenSurfaceEntry(
+ kind = "fullscreen",
+ mode = FullscreenMode.HORSE,
+ size = listOf(480, 270),
+ ),
+ static = "\uDB80\uDC12\uDB80\uDC10\uE905\uDB80\uDC02\uDB80\uDC04\uDB80\uDC0A",
+ staticMarker = "#a00400",
+ slots = mapOf(
+ "label_0" to SlotEntry(
+ x = 221,
+ y = 113,
+ width = 38,
+ align = Align.LEFT,
+ font = "window:y113",
+ color = "#ffe2b8",
+ shadow = false,
+ bold = false,
+ italic = false,
+ underlined = false,
+ strikethrough = false,
+ obfuscated = false,
+ text = "Lookout",
+ shaderMarker = null,
+ shaderColor = null,
+ ),
+ "label_1" to SlotEntry(
+ x = 191,
+ y = 144,
+ width = 35,
+ align = Align.CENTER,
+ font = "window:y144",
+ color = "#ffffff",
+ shadow = false,
+ bold = false,
+ italic = false,
+ underlined = false,
+ strikethrough = false,
+ obfuscated = false,
+ text = "Charge",
+ shaderMarker = null,
+ shaderColor = null,
+ ),
+ "label_2" to SlotEntry(
+ x = 258,
+ y = 144,
+ width = 29,
+ align = Align.CENTER,
+ font = "window:y144",
+ color = "#ffffff",
+ shadow = false,
+ bold = false,
+ italic = false,
+ underlined = false,
+ strikethrough = false,
+ obfuscated = false,
+ text = "Leave",
+ shaderMarker = null,
+ shaderColor = null,
+ ),
+ ),
+ controls = mapOf(
+ "charge" to ControlEntry(
+ index = 8,
+ x = 179,
+ y = 140,
+ width = 58,
+ height = 16,
+ action = "charge",
+ defaultAction = null,
+ hover = true,
+ outline = "\uDB80\uDC12\uDB80\uDC10\uDB80\uDC0F\uDB80\uDC0C\uDB80\uDC0B\uEFAF\uDB80\uDC03\uDB80\uDC04\uDB80\uDC05\uDB80\uDC07\uDB80\uDC08\uDB80\uDC09",
+ tooltip = ControlTooltipEntry(
+ panel = "\uDB80\uDC12\uDB80\uDC0F\uDB80\uDC0E\uDB80\uDC0D\uDB80\uDC0B\uF545\uDB80\uDC02\uDB80\uDC08",
+ marker = "#a3f000",
+ lines = listOf(
+ SlotEntry(
+ x = 161,
+ y = 162,
+ width = 94,
+ align = Align.LEFT,
+ font = "window:y162",
+ color = "#ffffff",
+ shadow = false,
+ bold = false,
+ italic = false,
+ underlined = false,
+ strikethrough = false,
+ obfuscated = false,
+ text = "Charges the signal",
+ shaderMarker = "#a3f400",
+ shaderColor = null,
+ source = "region `charge`",
+ ),
+ ),
+ ),
+ source = "region `charge`",
+ ),
+ "window:close" to ControlEntry(
+ index = 9,
+ x = 243,
+ y = 140,
+ width = 58,
+ height = 16,
+ action = "window:close",
+ defaultAction = "window:close",
+ hover = true,
+ outline = "\uDB80\uDC12\uDB80\uDC11\uDB80\uDC10\uDB80\uDC0F\uDB80\uDC0C\uDB80\uDC0B\uE09E\uDB80\uDC02\uDB80\uDC05\uDB80\uDC07\uDB80\uDC08\uDB80\uDC09",
+ source = "region `window:close`",
+ ),
+ ),
+ art = mapOf(
+ "art/0" to ArtEntry(
+ static = "\uDB80\uDC12\uDB80\uDC10\uDB80\uDC0F\uDB80\uDC0D\uEBE7\uDB80\uDC0A\uEF82\uDB80\uDC0A\uE686\uDB80\uDC0A\uE6CD\uDB80\uDC02\uDB80\uDC05\uDB80\uDC07\uDB80\uDC08\uDB80\uDC0A",
+ bar = BarEntry(
+ name = "signal",
+ x = 180,
+ width = 120,
+ levels = listOf("\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD\uEDDD", "\uE548\uE548\uE548\uE548\uE548\uE548\uE548\uE548\uE548\uE548\uE548\uE548\uE548\uE548\uE548\uE548\uE548\uE548\uE548\uE548\uE548\uE548\uE548\uE548\uE548\uE548\uE548\uE548\uE548\uE548", "\uF819\uF819\uF819\uF819\uF819\uF819\uF819\uF819\uF819\uF819\uF819\uF819\uF819\uF819\uF819", "\uE6CD\uE6CD\uE6CD\uE6CD\uE6CD\uE6CD\uE6CD\uE6CD", "\uE686\uE686\uE686\uE686", "\uEF82\uEF82", "\uEBE7"),
+ advances = mapOf(
+ "\uE548" to 3,
+ "\uE686" to 17,
+ "\uE6CD" to 9,
+ "\uEBE7" to 65,
+ "\uEDDD" to 2,
+ "\uEF82" to 33,
+ "\uF819" to 5,
+ ),
+ ),
+ ),
+ ),
+ layers = listOf(
+ LayerEntry(LayerKind.SLOT, "label_0"),
+ LayerEntry(LayerKind.ART, "art/0"),
+ LayerEntry(LayerKind.SLOT, "label_1"),
+ LayerEntry(LayerKind.SLOT, "label_2"),
+ ),
+ markers = mapOf(
+ "art/0" to "#a00c00",
+ "label_0" to "#a00801",
+ "label_1" to "#a01000",
+ "label_2" to "#a01400",
+ ),
+ backdropMarker = "#a80008",
+ ),
+ )
+ val canvas: FullscreenCanvasEntry =
+ FullscreenCanvasEntry(
+ width = 480,
+ height = 270,
+ degreesPerPixel = 0.1875,
+ edgeYaw = 45.0,
+ edgePitch = 25.3125,
+ snapEpsilon = 0.01,
+ hitMargin = 1,
+ displayScale = 256.0,
+ markerAlpha = 253,
+ cursor = CursorEntry(
+ text = "\uE0EB\uDB80\uDC07\uDB80\uDC0A",
+ marker = "#a7fc00",
+ width = 8,
+ height = 12,
+ ),
+ backdrop = "\uE6AF\uDB80\uDC07\uDB80\uDC0A",
+ overlayMarker = "#a3f800",
+ hudHiddenWorldAge = -1000000001L,
+ hudStripHeight = 56,
+ hudStripHalfWidth = 96,
+ hudMaxHealth = 20,
+ horse = HorseEntry(
+ offset = listOf(0.0, -1.75, 3.0),
+ armor = "window:fullscreen/horse_cursor",
+ ),
+ )
+}
diff --git a/jvm/example/src/main/kotlin/com/chunkzero/window/example/generated/WindowPackData.kt b/jvm/example/src/main/kotlin/com/chunkzero/window/example/generated/WindowPackData.kt
index 18f81b7a..27985440 100644
--- a/jvm/example/src/main/kotlin/com/chunkzero/window/example/generated/WindowPackData.kt
+++ b/jvm/example/src/main/kotlin/com/chunkzero/window/example/generated/WindowPackData.kt
@@ -17,5 +17,7 @@ internal object WindowPackData {
sprites = WindowSprites.all,
windows = WindowEntries.all,
huds = WindowHudEntries.all,
+ fullscreens = WindowFullscreenEntries.all,
+ fullscreenCanvas = WindowFullscreenEntries.canvas,
)
}
From 61a798d5a19b7ad72fc3d83a4e28b58df8a13826 Mon Sep 17 00:00:00 2001
From: oglassdev <23040613+oglassdev@users.noreply.github.com>
Date: Thu, 15 Oct 2026 20:18:38 -0400
Subject: [PATCH 2/3] docs: document full-screen windows
Cover authoring (fullscreens, modes, backdrop, costs), the Kotlin API
(FullscreenView, scroll, right click, promptText, list), the Minestom
host's runtime contract, and limitations. Note how to check them in a
real client.
---
README.md | 59 +++++++++++++++++++++++++++++++++++++++++++++-
docs/AUTHORING.md | 34 +++++++++++++++++++++-----
docs/VALIDATION.md | 4 ++++
example/README.md | 45 ++++++++++++++++++++++-------------
4 files changed, 118 insertions(+), 24 deletions(-)
diff --git a/README.md b/README.md
index bd25aec8..376bd71c 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,8 @@ through the inventory title and HUD, and generates a typed Kotlin view for each
server. If you rename a button in the UI, the server code stops compiling instead of breaking at runtime.
Inventory UIs support real items, scrolling collections, paging, toggles, choices, tooltips, and native anvil text
-input. HUDs are positioned with core shaders and rendered by the server whenever it wants to update them.
+input. HUDs are positioned with core shaders and rendered by the server whenever it wants to update them. Full-screen
+windows are drawn over the whole screen with no screen open, and the player aims a cursor by turning.
> [!WARNING]
>
@@ -191,6 +192,62 @@ You own the rest:
- Per-player state and cleanup, such as stopping the resend task when the player leaves.
- Threading: a `HudView` or `HudStack` is not thread-safe, so render it from one thread at a time.
+## Full-screen windows
+
+A full-screen window is authored with [``](docs/AUTHORING.md#full-screen-windows) and generates a typed
+view, such as `LobbyFullscreen`, that extends `FullscreenView`. With the `minestom` target it takes a `Player`; with
+other targets it takes a `FullscreenHost`, which only `window-minestom` implements (`MinestomFullscreenHost`). The
+player aims by turning, clicks with the attack key, and leaves by sneaking:
+
+```kotlin
+class Lobby(player: Player) : LobbyFullscreen(player) {
+ private var name by state("Lobby")
+ private val worlds = list(cells = 3, key = { it: String -> it }) { allWorlds() }
+
+ override fun lobbyName(): Component = Component.text(name)
+
+ override fun worldName(index: Int): Component = Component.text(worlds.at(index).orEmpty())
+
+ // A click opens a native dialog; the canvas is back once the player confirms or cancels.
+ override fun onRename(click: FullscreenClick) {
+ promptText(Component.text("Rename"), name) { text -> text?.let { name = it } }
+ }
+
+ override fun onScroll(steps: Int) = if (steps > 0) worlds.next() else worlds.previous()
+
+ override fun onRightClick(click: FullscreenClick) = close()
+}
+
+Lobby(player).open()
+```
+
+- Handlers receive a `FullscreenClick` with the cursor's canvas position. `onScroll(steps)` gets the scroll wheel as
+ signed steps, and `onRightClick(click)` gets right clicks anywhere on the canvas.
+- `list(...)` pages a list through a ``'s cells, like `WindowView.list(...)`.
+- `promptText(title, initial) { text -> }` shows a native dialog with a text field and Confirm and Cancel buttons. The
+ callback gets the text, or `null` on Cancel or when the window closes. Escape does not dismiss it.
+- Views are single-use, as with windows, and a player has at most one full-screen window open.
+
+While the window is open, the Minestom host:
+
+- keeps the player in spectator mode on the server and tells only their client it is in survival, so clicks, the scroll
+ wheel, and number keys reach the server. Game mode, ability, hotbar, time, and max health updates the server sends
+ meanwhile still reach the client, followed by packets that hide them again. Closing restores the player's game mode
+ (or the one the server set meanwhile), abilities, position, and held slot.
+- hides the HUD by holding the client's world age negative, which the pack's shaders read. The hidden strip at the
+ bottom of the screen can clip the player's own inventory (E) at small GUI heights, since the server is not told it
+ opened.
+- closes the window when the player dies or spawns into another instance. Opening one while the player is dead or
+ respawning throws, so a respawn listener should open it on the next tick.
+
+Limitations:
+
+- First person only: third person (F5) mirrors the camera, so the drawn cursor and the server's hit-test disagree.
+- Shader packs that replace world rendering, such as Iris, are not supported.
+- The client sends at most one held-slot change per tick, so a fast scroll counts as one step, more than 4 notches in
+ one tick read as a scroll the other way, and number keys look like scrolling.
+- Chat and other GUI overlays draw over the canvas, and there are no real items.
+
## Supported versions
Window targets Minecraft 26.2 (resource pack format 88). The hosts and example server use Minestom builds for 26.2, and
diff --git a/docs/AUTHORING.md b/docs/AUTHORING.md
index a57c91d4..43c0bf40 100644
--- a/docs/AUTHORING.md
+++ b/docs/AUTHORING.md
@@ -564,10 +564,11 @@ const raised = (fill: Token) =>
### Roots
-| root | `raw` equivalent | notes |
-| ---------------------------------------------------- | ---------------- | ---------------------------------------------------------------- |
-| `` | `ui(...)` | `frame` is drawn first over the GUI grown by `bleed` |
-| `` | `hud(...)` | children lay out as a column; without a size it fits its content |
+| root | `raw` equivalent | notes |
+| ---------------------------------------------------- | ----------------- | ---------------------------------------------------------------- |
+| `` | `ui(...)` | `frame` is drawn first over the GUI grown by `bleed` |
+| `` | `hud(...)` | children lay out as a column; without a size it fits its content |
+| `` | `fullscreen(...)` | see [Full-screen windows](#full-screen-windows) |
`text={{ ... }}` on a window, HUD, section, box, or switch sets default text style (`color`, `shadow`, `bold`, `italic`,
`underlined`, `strikethrough`, `obfuscated`, `font`, `smallCaps`, `align`) for the text inside it; the nearest setting
@@ -612,8 +613,25 @@ which the client still draws once the shader has moved them.
### Full-screen windows
-`` lays its children out on a 480x270 canvas drawn over the whole screen with no screen
-open; the player aims a cursor by turning. It takes the same elements as a window, except those that need a container:
+`` (`raw.fullscreen(...)`) lays its children out on a 480x270 canvas drawn over the
+whole screen with no screen open; the player aims a cursor by turning. List it in `defineWindows({ fullscreens })`. The
+canvas is letterboxed or pillarboxed to the screen and does not move with FOV. Its children lay out as a column in a
+root box that takes box props such as `align`, `justify`, `text`, and `theme`. A pack with a full-screen window emits
+the core shaders that draw it, with no `coreShaders` option, and needs pack format 85 through 88.
+
+```tsx
+
+
+
+```
+
+- `mode`: `"unlocked"`, the default, keeps the camera on the player, so the world turns behind the canvas as they aim.
+ `"horse"` locks the view in place and draws the cursor about a tick behind the mouse.
+- `backdrop`: a hex color filling the screen behind the canvas, letterbox included. Omit it to show the world.
+
+It takes the same elements as a window, except those that need a container:
- **Supported**: art, static and bound text, switches and conditions, tints, bars, regions and the industrial controls
built on them (with hover outlines), `toggle` and `selection` state, and indexed handles. A ``'s cells are
@@ -629,6 +647,10 @@ open; the player aims a cursor by turning. It takes the same elements as a windo
region's `itemModel`, and text `shadow` are also rejected.
- `span` and `at` count grid tracks, which are slots only in a section. A `` sizes its `cell`s at 18px
per slot; give other industrial controls a size or a grid to fill on a full-screen window.
+- Text costs a [text-row font](#what-things-cost) per distinct row, as in windows, and full-screen rows rarely line up
+ with container rows. Each tooltip line counts as a row.
+
+The server side is in the README's [Full-screen windows](../README.md#full-screen-windows).
## Industrial
diff --git a/docs/VALIDATION.md b/docs/VALIDATION.md
index c10faf39..380dac15 100644
--- a/docs/VALIDATION.md
+++ b/docs/VALIDATION.md
@@ -60,6 +60,10 @@ sprites, container/hotbar item sync, cards that render a real item in one of the
routes clicks to the card's region, left/right/shift/hotbar clicks, reactive title invalidation, anvil input, HUD
diagnostics, GUI scale 2, and clean disconnect.
+It does not cover full-screen windows. Their compiler output, cursor mapping, session, and host behavior are covered by
+the deterministic checks and the `fullscreen` golden case; check how they draw and take input in a real client with the
+example's `/lobby` and `/lookout`, in first person, at a few window sizes and GUI scales.
+
The `just mc-validation` recipe defaults to the sibling `../mc-validation` checkout and passes its absolute path to both
Gradle builds. Override it with:
diff --git a/example/README.md b/example/README.md
index 22d6d344..90096e14 100644
--- a/example/README.md
+++ b/example/README.md
@@ -10,12 +10,14 @@ example/
│ ├── rpp.config.ts # pack metadata, build, and Window plugin configuration
│ ├── src/
│ │ └── window/ # UI sources — stripped from the built pack
-│ │ ├── index.ts # defineWindows entry: the sprite catalog, windows, HUDs, and previews
+│ │ ├── index.ts # defineWindows entry: the sprite catalog, windows, HUDs, full-screen windows, and previews
│ │ ├── handles.ts # typed bindings shared by the windows
│ │ ├── art.ts # inline art: icons, coin, vents, bars, and the rank sprites
│ │ ├── shop.tsx # paged catalog, selections, toggles, and buttons
│ │ ├── search.tsx # linked native-anvil catalog search
│ │ ├── upgrades.tsx # themed workshop: styles, vars, cards with a face per stage, choices, and a hotspot
+│ │ ├── lobby.tsx # full-screen main menu: paged worlds, a rename prompt, tooltips, and a ready switch
+│ │ ├── lookout.tsx # horse-mode full-screen window with a signal bar
│ │ ├── status_hud.tsx # the shader-relocated actionbar HUDs
│ │ └── clock_hud.ts # the clock HUD, written with the function-style raw API
│ ├── build/previews/ # preview PNGs of the windows and HUDs, rewritten by every build
@@ -24,13 +26,14 @@ example/
```
The Minestom server is the Gradle module `jvm/example`, which depends on `window-minestom`. `Main.kt` boots the server,
-`Market.kt` and `Workshop.kt` are a throwaway domain model, and `MyShop.kt` and `MyUpgrades.kt` implement the
-**generated** `ShopView` and `UpgradesView`. `MyStatusHud.kt` implements the **generated** HUD views. The pack uses
-`target: "minestom"`, so a window view takes the `Player` it is shown to: `Main.kt` opens the shop with
-`MyShop(player, market).open()`. Generated HUD views take no player; the handwritten ones that need it take it
-themselves. `Main.kt` joins the status HUDs into one action-bar `HudStack` and resends it with `player.sendHud(stack)`
-every tick, which keeps the wave bar moving smoothly, re-renders the clocks, and stays ahead of the action bar fading
-out. The bindings live under `generated/`, produced by the rpp plugin — committed, do not hand-edit.
+`Market.kt` and `Workshop.kt` are a throwaway domain model, `MyShop.kt` and `MyUpgrades.kt` implement the **generated**
+`ShopView` and `UpgradesView`, and `MyLobby.kt` and `MyLookout.kt` the full-screen `LobbyFullscreen` and
+`LookoutFullscreen`. `MyStatusHud.kt` implements the **generated** HUD views. The pack uses `target: "minestom"`, so a
+window view takes the `Player` it is shown to: `Main.kt` opens the shop with `MyShop(player, market).open()`. Generated
+HUD views take no player; the handwritten ones that need it take it themselves. `Main.kt` joins the status HUDs into one
+action-bar `HudStack` and resends it with `player.sendHud(stack)` every tick, which keeps the wave bar moving smoothly,
+re-renders the clocks, and stays ahead of the action bar fading out. The bindings live under `generated/`, produced by
+the rpp plugin — committed, do not hand-edit.
## Run it
@@ -51,6 +54,12 @@ levels, each card shows its level as text and is marked new, shows a bar, or is
the shared tone. A maxed card takes no clicks, so hovering it shows "Fully upgraded" and no outline. Its levels last
until the server stops.
+`/lobby` opens a full-screen main menu over the world; aim by turning and sneak to leave. Click the foundry name to
+rename it in a native dialog, which returns to the lobby on Confirm or Cancel. The arrows or the scroll wheel move the
+world list, a click selects a world, and right clicking anywhere toggles ready, which switches the line under the list
+and retints the status. Play leaves once you are ready. `/lookout` opens a horse-mode window, which locks the view while
+the cursor follows the mouse; Charge fills its signal bar.
+
The actionbar-backed HUD demo is disabled by default so the inventory examples stay visually focused. Enable it with
`-Dwindow.hud.enabled=true`; use `-Dwindow.hud.spriteDebug=true` to print each HUD's static art to chat. The pack blanks
vanilla's experience bar, and one of the HUDs draws its wave bar there.
@@ -59,6 +68,8 @@ vanilla's experience bar, and one of the HUDs draws its wave bar there.
| -------------------------------- | ---------------------------------------------------------------------- |
| `/shop` | opens the catalog, like the hotbar emerald |
| `/upgrades` | opens the workshop, like the catalog's Workshop button |
+| `/lobby` | opens the full-screen lobby |
+| `/lookout` | opens the horse-mode lookout |
| `/tone ` | pins the tone the HUDs and newly opened windows draw in; `auto` unpins |
| `/bar ` | pins how full the wave bar and a newly opened shop's bar are |
@@ -71,12 +82,12 @@ when testing from another machine.
## The typed contract
The rpp plugin emits `internal` pack data (`WindowPackData`, `WindowFonts`, `WindowSpacers`, and friends), the public
-`WindowDefinitions` and `WindowHudDefinitions`, and one public abstract class for each window/HUD. Each handle in
-`handles.ts` becomes a member of the views that use it: `text` handles such as `balance` and `status` are functions
-returning a `Component`, `flag` handles such as `canBuy` are Boolean functions, `action` handles such as `buy` are
-`onBuy(click)` handlers, `products` is a `WindowCollection` that `MyShop` backs with a `WindowList`, and the
-`category`/`sort` selections and `favorites`/`affordable` toggles are UI-owned `var`s with `on…Changed` hooks. In
-`UpgradesView` the indexed handles take the card's index (`upgradeLevel(index)`, `onUpgrade(click)` with `click.index`),
-the `upgradeStage` value and the `rank` sprite return generated enums, and `upgradeProgress` and `tone` return a bar's
-fraction and a tint's palette entry. `MyStatusHud` implements each HUD's handles. Omit one, rebuild, and the breakage
-surfaces at compile time.
+`WindowDefinitions`, `WindowHudDefinitions`, and `WindowFullscreenDefinitions`, and one public abstract class for each
+window, HUD, and full-screen window. Each handle in `handles.ts` becomes a member of the views that use it: `text`
+handles such as `balance` and `status` are functions returning a `Component`, `flag` handles such as `canBuy` are
+Boolean functions, `action` handles such as `buy` are `onBuy(click)` handlers, `products` is a `WindowCollection` that
+`MyShop` backs with a `WindowList`, and the `category`/`sort` selections and `favorites`/`affordable` toggles are
+UI-owned `var`s with `on…Changed` hooks. In `UpgradesView` the indexed handles take the card's index
+(`upgradeLevel(index)`, `onUpgrade(click)` with `click.index`), the `upgradeStage` value and the `rank` sprite return
+generated enums, and `upgradeProgress` and `tone` return a bar's fraction and a tint's palette entry. `MyStatusHud`
+implements each HUD's handles. Omit one, rebuild, and the breakage surfaces at compile time.
From 75cc5e5073fa06c32255999f3e2b2b393d7be095 Mon Sep 17 00:00:00 2001
From: oglassdev <23040613+oglassdev@users.noreply.github.com>
Date: Thu, 15 Oct 2026 20:23:00 -0400
Subject: [PATCH 3/3] docs: match the full-screen README example to MyLobby
---
README.md | 23 ++++++++++++-----------
1 file changed, 12 insertions(+), 11 deletions(-)
diff --git a/README.md b/README.md
index 376bd71c..a3a6df1d 100644
--- a/README.md
+++ b/README.md
@@ -197,28 +197,29 @@ You own the rest:
A full-screen window is authored with [``](docs/AUTHORING.md#full-screen-windows) and generates a typed
view, such as `LobbyFullscreen`, that extends `FullscreenView`. With the `minestom` target it takes a `Player`; with
other targets it takes a `FullscreenHost`, which only `window-minestom` implements (`MinestomFullscreenHost`). The
-player aims by turning, clicks with the attack key, and leaves by sneaking:
+player aims by turning, clicks with the attack key, and leaves by sneaking. An excerpt of the example's
+[`MyLobby`](jvm/example/src/main/kotlin/com/chunkzero/window/example/MyLobby.kt):
```kotlin
-class Lobby(player: Player) : LobbyFullscreen(player) {
- private var name by state("Lobby")
- private val worlds = list(cells = 3, key = { it: String -> it }) { allWorlds() }
+class MyLobby(player: Player) : LobbyFullscreen(player) {
+ private var name by state("${player.username}'s Foundry")
+ private val worlds = list(cells = 3, key = { it: String -> it }, step = 1) { WORLDS }
- override fun lobbyName(): Component = Component.text(name)
+ override fun foundryName(): Component = Component.text(name)
override fun worldName(index: Int): Component = Component.text(worlds.at(index).orEmpty())
// A click opens a native dialog; the canvas is back once the player confirms or cancels.
override fun onRename(click: FullscreenClick) {
- promptText(Component.text("Rename"), name) { text -> text?.let { name = it } }
+ promptText(Component.text("Name your foundry"), name) { text -> text?.let { name = it } }
}
- override fun onScroll(steps: Int) = if (steps > 0) worlds.next() else worlds.previous()
+ override fun onScroll(steps: Int) = repeat(abs(steps)) { if (steps > 0) worlds.next() else worlds.previous() }
- override fun onRightClick(click: FullscreenClick) = close()
+ // The other generated bindings and handlers are omitted.
}
-Lobby(player).open()
+MyLobby(player).open()
```
- Handlers receive a `FullscreenClick` with the cursor's canvas position. `onScroll(steps)` gets the scroll wheel as
@@ -244,8 +245,8 @@ Limitations:
- First person only: third person (F5) mirrors the camera, so the drawn cursor and the server's hit-test disagree.
- Shader packs that replace world rendering, such as Iris, are not supported.
-- The client sends at most one held-slot change per tick, so a fast scroll counts as one step, more than 4 notches in
- one tick read as a scroll the other way, and number keys look like scrolling.
+- The client sends at most one held-slot change per tick, so `onScroll` gets one update per tick of at most 4 steps
+ either way, more than 4 notches in one tick read as a scroll the other way, and number keys look like scrolling.
- Chat and other GUI overlays draw over the canvas, and there are no real items.
## Supported versions