Skip to content
Open
31 changes: 25 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,15 +203,21 @@ player aims by turning, clicks with the attack key, and leaves by sneaking. An e
```kotlin
class MyLobby(player: Player) : LobbyFullscreen(player) {
private var name by state("${player.username}'s Foundry")
private var draft: String? by state(null)
private val worlds = list(cells = 3, key = { it: String -> it }, step = 1) { WORLDS }

override fun foundryName(): Component = Component.text(name)
override fun foundryName(): Component = Component.text(draft?.let { "${it}_" } ?: 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.
// A click lets the player type the name in place, until they press Escape.
@OptIn(ExperimentalFullscreenApi::class)
override fun onRename(click: FullscreenClick) {
promptText(Component.text("Name your foundry"), name) { text -> text?.let { name = it } }
draft = name
promptText(name, onEdit = { draft = it }) { text ->
draft = null
text?.let { name = it }
}
}

override fun onScroll(steps: Int) = repeat(abs(steps)) { if (steps > 0) worlds.next() else worlds.previous() }
Expand All @@ -225,8 +231,16 @@ MyLobby(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 `<Repeater fixed>`'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.
- Two text prompts share one slot: opening either replaces an open prompt, whose callback gets `null`, as it does when a
server container opens over the prompt or the window closes. While one is open, canvas input is ignored and the
hovered control's tooltip is hidden.
- `promptText(initial, onEdit) { text -> }`, experimental (`@OptIn(ExperimentalFullscreenApi::class)`), lets the
player type while the canvas stays in view. Nothing of it is drawn: draw the text from `onEdit`, which re-renders at
once. Escape finishes with the text, and a click on a slot or outside the anvil may finish it too; there is no
cancel. The text holds at most 50 characters, and `initial` is cut to fit.
- `promptTextDialog(title, initial) { text -> }` shows a native dialog over the canvas with a text field and Confirm
and Cancel buttons, and gets `null` on Cancel. It holds up to 256 characters, and Escape does not dismiss it. Use it
when the player must be able to back out, or to move the caret.
- 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:
Expand All @@ -237,7 +251,8 @@ While the window is open, the Minestom host:
(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.
opened. While the keyboard prompt is open, the age sits in a second negative band in which the shaders also hide the
untitled anvil that captures the typing.
- 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.

Expand All @@ -248,6 +263,10 @@ Limitations:
- 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.
- The keyboard prompt is experimental. The server never sees the caret, so draw it at the end of the text. The anvil
holds at most 50 characters. While it is open, GUI text and fills in the anvil's rename field and label bands, such as
chat lines crossing them, are hidden, and the system mouse pointer shows. Each edit takes a round trip to reach
`onEdit`. Without the pack's shaders, the player sees a plain vanilla anvil.

## Supported versions

Expand Down
1 change: 1 addition & 0 deletions crates/window-core/src/codegen/entries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ pub(super) fn fullscreen_canvas_entry_expr(canvas: &FullscreenCanvasEntry, level
.arg("backdrop", kt_string(&canvas.backdrop))
.arg("overlayMarker", kt_string(&canvas.overlay_marker))
.arg("hudHiddenWorldAge", format!("{}L", canvas.hud_hidden_world_age))
.arg("promptWorldAge", format!("{}L", canvas.prompt_world_age))
.arg("hudStripHeight", canvas.hud_strip_height)
.arg("hudStripHalfWidth", canvas.hud_strip_half_width)
.arg("hudMaxHealth", canvas.hud_max_health);
Expand Down
10 changes: 10 additions & 0 deletions crates/window-core/src/fullscreen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,16 @@ pub const MAX_COLORS: usize = 1 << 10;
/// 24000.
pub const HUD_HIDDEN_WORLD_AGE: i64 = -1_000_000_001;

/// The world age the server sends while the keyboard text prompt's anvil is open: 204800 ticks above
/// [`HUD_HIDDEN_WORLD_AGE`], so `GameTime` is about -0.13 rather than -0.67. Both bands hide the HUD, and the prompt
/// band also hides what the anvil draws. Clouds scroll by the world age modulo 400 times the cloud texture's width,
/// and the gap is a multiple of that for widths up to 512 (vanilla's is 256), so the clouds hold still when the anvil
/// opens or closes.
pub const PROMPT_WORLD_AGE: i64 = HUD_HIDDEN_WORLD_AGE + 204_800;

/// The `GameTime` between the HUD band, below it, and the prompt band, above it.
pub const PROMPT_TIME_SPLIT: f64 = -0.4;

/// The height of the HUD strip in GUI pixels, from the bottom of the screen: the hotbar, XP bar, hearts, armour, and
/// hunger.
pub const HUD_STRIP_HEIGHT: u32 = 56;
Expand Down
14 changes: 8 additions & 6 deletions crates/window-core/src/hud/profile.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use std::fmt;

use super::renderer::{
render_dynamic_text_intensity_fragment, render_position_tex_color_fragment, render_position_tex_color_vertex,
render_v150_dynamic_text_background_shader, render_v150_dynamic_text_shader, render_v150_text_background_shader,
render_v150_text_intensity_fragment, render_v150_text_shader, render_v330_define_variant_text_background_shader,
render_v330_define_variant_text_fragment, render_v330_define_variant_text_shader,
render_v330_text_background_shader, render_v330_text_shader,
render_dynamic_text_intensity_fragment, render_gui_fragment, render_gui_vertex, render_position_tex_color_fragment,
render_position_tex_color_vertex, render_v150_dynamic_text_background_shader, render_v150_dynamic_text_shader,
render_v150_text_background_shader, render_v150_text_intensity_fragment, render_v150_text_shader,
render_v330_define_variant_text_background_shader, render_v330_define_variant_text_fragment,
render_v330_define_variant_text_shader, render_v330_text_background_shader, render_v330_text_shader,
};
use super::rule::HudShaderRule;

Expand Down Expand Up @@ -64,14 +64,16 @@ impl ShaderProfile {
Self::Pack85To88DefineVariants => {
let mut sources = vec![
("text.vsh", render_v330_define_variant_text_shader(rules, hover, fullscreen)),
("text.fsh", render_v330_define_variant_text_fragment(hover)),
("text.fsh", render_v330_define_variant_text_fragment(hover, fullscreen)),
];
if !rules.is_empty() {
sources.push(("text_background.vsh", render_v330_define_variant_text_background_shader(rules)));
}
if fullscreen {
sources.push(("position_tex_color.vsh", render_position_tex_color_vertex()));
sources.push(("position_tex_color.fsh", render_position_tex_color_fragment()));
sources.push(("gui.vsh", render_gui_vertex()));
sources.push(("gui.fsh", render_gui_fragment()));
}
sources
}
Expand Down
16 changes: 11 additions & 5 deletions crates/window-core/src/hud/renderer/fragment.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::hover::{HOVER_FRAGMENT_DRAW, HOVER_FRAGMENT_INPUTS};
use super::prompt::{PROMPT_TEXT_FRAGMENT_CLIP, render_prompt_text_fragment_helpers};

const INTENSITY_SAMPLE_FUNCTION: &str = r#"
vec4 window_hud_intensity_sample(sampler2D sampler, vec2 coord) {
Expand All @@ -10,10 +11,15 @@ vec4 window_hud_intensity_sample(sampler2D sampler, vec2 coord) {
}
"#;

/// Renders `core/text.fsh` for every define variant; `IS_GRAYSCALE` departs from vanilla, and with `hover` so does
/// `IS_GUI`.
pub(in crate::hud) fn render_v330_define_variant_text_fragment(hover: bool) -> String {
/// Renders `core/text.fsh` for every define variant; `IS_GRAYSCALE` departs from vanilla, and with `hover` or
/// `fullscreen` so does `IS_GUI`.
pub(in crate::hud) fn render_v330_define_variant_text_fragment(hover: bool, fullscreen: bool) -> String {
let (hover_inputs, hover_draw) = if hover { (HOVER_FRAGMENT_INPUTS, HOVER_FRAGMENT_DRAW) } else { ("", "") };
let (prompt_inputs, prompt_clip) = if fullscreen {
(render_prompt_text_fragment_helpers(), PROMPT_TEXT_FRAGMENT_CLIP)
} else {
(String::new(), "")
};
format!(
r#"#version 330

Expand All @@ -34,11 +40,11 @@ in vec4 vertexColor;
in vec2 texCoord0;
#ifdef IS_GRAYSCALE
flat in int windowHudTextureMode;
#endif{hover_inputs}
#endif{hover_inputs}{prompt_inputs}

out vec4 fragColor;

void main() {{
void main() {{{prompt_clip}
vec4 texColor = texture(Sampler0, texCoord0);
#ifdef IS_GRAYSCALE
if (windowHudTextureMode != 1) {{
Expand Down
28 changes: 18 additions & 10 deletions crates/window-core/src/hud/renderer/fullscreen.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
//! Full-screen snippets of the pack_format 85-88 `core/text` vertex shader, and the `core/position_tex_color`
//! shaders that hide the HUD and the vanilla crosshair while a full-screen window is open.
//! shaders that hide the HUD and the vanilla crosshair while a full-screen window is open, and the keyboard prompt's
//! anvil while it is open (see [`super::prompt`]).
//!
//! The text shader moves every vertex of a full-screen marker (see [`crate::fullscreen`]) in the world pass straight
//! into clip space on the letterboxed canvas, so the projection, FOV, and view bobbing never move it. Unmarked text
//! takes the vanilla path.

use crate::fullscreen::{CROSSHAIR_MARKER_ALPHA, HUD_STRIP_HALF_WIDTH, HUD_STRIP_HEIGHT, LAYER_DEPTH_STEP};

use super::prompt::{PROMPT_SPRITE_FRAGMENT_CLIP, render_prompt_fragment_helpers, render_prompt_vertex_helpers};

/// Imports the canvas contract into the world variant of the text vertex shader.
pub(super) const FULLSCREEN_VERTEX_IMPORT: &str = "
#if !defined(IS_GUI) && !defined(IS_SEE_THROUGH)
Expand Down Expand Up @@ -92,10 +95,10 @@ pub(super) fn render_fullscreen_vertex_helpers() -> String {
VERTEX_HELPERS.replace("@PLACEMENT@", CANVAS_PLACEMENT).replace("@DEPTH_STEP@", &format!("{LAYER_DEPTH_STEP:?}"))
}

/// Renders `core/position_tex_color.vsh`, vanilla's GUI sprite shader plus the HUD strip inputs. Like vanilla, it
/// declares its uniform blocks inline, since the shader is also compiled before resource packs load.
/// Renders `core/position_tex_color.vsh`, vanilla's GUI sprite shader plus the HUD strip and prompt inputs. Like
/// vanilla, it declares its uniform blocks inline, since the shader is also compiled before resource packs load.
pub(in crate::hud) fn render_position_tex_color_vertex() -> String {
r#"#version 330
let source = r#"#version 330

layout(std140) uniform DynamicTransforms {
mat4 ModelViewMat;
Expand Down Expand Up @@ -124,7 +127,7 @@ out vec2 texCoord0;
out vec4 vertexColor;
out vec2 windowHudPos;
flat out int windowHudHidden;

@PROMPT@
void main() {
gl_Position = ProjMat * ModelViewMat * vec4(Position, 1.0);

Expand All @@ -135,14 +138,17 @@ void main() {
// orthographically, so windowHudPos is in GUI pixels: x from the center, y up from the bottom.
windowHudHidden = GameTime < 0.0 && ProjMat[3][3] == 1.0 ? 1 : 0;
windowHudPos = vec2(gl_Position.x / ProjMat[0][0], (gl_Position.y + 1.0) / -ProjMat[1][1]);

window_prompt_clip(gl_Position);
}
"#
.to_string()
"#;
source.replace("@PROMPT@", &render_prompt_vertex_helpers())
}

/// Renders `core/position_tex_color.fsh`, which while the HUD is hidden discards GUI sprite fragments inside the HUD
/// strip, and texels carrying the crosshair marker alpha, which the pack's crosshair sprite override sets on its
/// opaque texels. Clipping per fragment cuts sprites crossing the strip's edge instead of distorting them.
/// opaque texels. Clipping per fragment cuts sprites crossing the strip's edge instead of distorting them. While the
/// prompt's anvil is open, it also discards GUI sprite fragments inside the anvil's rect.
pub(in crate::hud) fn render_position_tex_color_fragment() -> String {
format!(
r#"#version 330
Expand All @@ -164,10 +170,10 @@ in vec2 texCoord0;
in vec4 vertexColor;
in vec2 windowHudPos;
flat in int windowHudHidden;

{prompt_helpers}
out vec4 fragColor;

void main() {{
void main() {{{prompt_clip}
if (windowHudHidden == 1 && abs(windowHudPos.x) < WINDOW_FS_HUD_STRIP.x && windowHudPos.y < WINDOW_FS_HUD_STRIP.y) {{
discard;
}}
Expand All @@ -185,5 +191,7 @@ void main() {{
half_width = HUD_STRIP_HALF_WIDTH,
height = HUD_STRIP_HEIGHT,
crosshair_alpha = CROSSHAIR_MARKER_ALPHA,
prompt_helpers = render_prompt_fragment_helpers(),
prompt_clip = PROMPT_SPRITE_FRAGMENT_CLIP,
)
}
2 changes: 2 additions & 0 deletions crates/window-core/src/hud/renderer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod fragment;
mod fullscreen;
mod glsl;
mod hover;
mod prompt;
mod text;

use crate::ir::Rgb;
Expand All @@ -18,6 +19,7 @@ pub(super) use fragment::{
render_v330_define_variant_text_fragment,
};
pub(super) use fullscreen::{render_position_tex_color_fragment, render_position_tex_color_vertex};
pub(super) use prompt::{render_gui_fragment, render_gui_vertex};
pub(super) use text::{
render_v150_dynamic_text_shader, render_v150_text_shader, render_v330_define_variant_text_shader,
render_v330_text_shader,
Expand Down
Loading
Loading