spaceship/ui/LoadingScreen.gd
2026-09-02 13:43:56 +02:00

136 lines
5.3 KiB
GDScript

class_name LoadingScreen
extends Control
# Forgejo #169: "create a loading screen after clicking on the main menu
# buttons."
#
# Pressing Play used to do nothing visible for as long as the scene took to
# build - several seconds in a browser - and a button that does nothing reads
# as a button that was not pressed, so the player presses it again. The whole
# job of this is to answer the press immediately: a cover over the menu, the
# name of what is being opened, and a bar that moves.
#
# Two things make the answer immediate rather than merely eventual:
#
# 1. It goes up BEFORE the load starts, and the load waits for it to have
# been on screen for a frame or two (see `frames_shown` and
# MainMenu._process). A screen raised in the same frame as a blocking
# load is a screen nobody ever sees.
# 2. The bar reports nothing it does not know. The load is one blocking call
# (see MainMenu._process for why it is not a threaded one), so there is
# no progress to report and the bar does not pretend there is: it is a
# sweep, which says "working" without claiming to say how far.
#
# A Control built in code rather than a scene: it is one panel of geometry
# with nothing to author, and building it here keeps MainMenu.tscn - which the
# player's own editor touches - out of the change.
const BACKDROP: Color = Color(0.01, 0.02, 0.06, 1.0)
const GLOW: Color = Color(0.42, 0.86, 0.95)
const TEXT: Color = Color(0.82, 0.94, 1.0)
const MUTED: Color = Color(0.52, 0.66, 0.76)
const TITLE_SIZE: int = 44
const SUBTITLE_SIZE: int = 24
const BAR_WIDTH: float = 520.0
const BAR_HEIGHT: float = 16.0
# The travelling band inside the bar, and how much of the bar it covers.
const SWEEP_SECONDS: float = 1.2
const SWEEP_WIDTH: float = 0.22
var title_label: Label
var subtitle_label: Label
# How many frames this has been up and running. MainMenu waits for this before
# it starts the blocking part - see the note above. Counted in _process rather
# than in _draw so the count still advances under the headless renderer, where
# nothing paints and a test would otherwise wait for ever.
var frames_shown: int = 0
var _elapsed: float = 0.0
func _ready() -> void:
# Covers whatever is behind it and swallows nothing: the menu's buttons
# check for a modal themselves (see IconButton._blocked_by_modal), and
# MainMenu refuses a second press while a load is running anyway.
mouse_filter = Control.MOUSE_FILTER_IGNORE
set_anchors_preset(Control.PRESET_FULL_RECT)
_fit_viewport()
get_viewport().size_changed.connect(_fit_viewport)
title_label = _make_label("LOADING", TITLE_SIZE, TEXT)
subtitle_label = _make_label("", SUBTITLE_SIZE, MUTED)
resized.connect(_layout)
_layout()
# A Control parent would resolve full-rect anchors for us. This node sits
# directly under a CanvasLayer, so its first layout pass can otherwise be
# 0x0 - exactly the frame that is supposed to cover the menu before loading.
func _fit_viewport() -> void:
position = Vector2.ZERO
size = get_viewport_rect().size
func _make_label(text_value: String, font_size: int, colour: Color) -> Label:
var label := Label.new()
label.text = text_value
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
label.add_theme_font_size_override("font_size", font_size)
label.add_theme_color_override("font_color", colour)
label.add_theme_color_override("font_outline_color", Color(0.01, 0.02, 0.06, 0.95))
label.add_theme_constant_override("outline_size", 3)
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
add_child(label)
return label
# What is being opened, in the player's words rather than a file name.
func begin(what: String) -> void:
if subtitle_label != null:
subtitle_label.text = what
func _bar_rect() -> Rect2:
var width: float = minf(BAR_WIDTH, maxf(size.x - 120.0, 40.0))
return Rect2(Vector2((size.x - width) * 0.5, (size.y - BAR_HEIGHT) * 0.5),
Vector2(width, BAR_HEIGHT))
func _layout() -> void:
if title_label == null:
return
var bar: Rect2 = _bar_rect()
title_label.position = Vector2(0.0, bar.position.y - float(TITLE_SIZE) * 1.8)
title_label.size = Vector2(size.x, float(TITLE_SIZE) * 1.4)
subtitle_label.position = Vector2(0.0, bar.end.y + 18.0)
subtitle_label.size = Vector2(size.x, float(SUBTITLE_SIZE) * 1.6)
queue_redraw()
func _process(delta: float) -> void:
_elapsed += delta
frames_shown += 1
queue_redraw()
func _draw() -> void:
draw_rect(Rect2(Vector2.ZERO, size), BACKDROP)
var bar: Rect2 = _bar_rect()
draw_rect(bar, Color(GLOW, 0.14))
# A band travelling the length of the bar. It stops moving while the load
# blocks, which is honest - the whole engine has stopped - and it is the
# thing that says "working" on the frames either side of that.
var head: float = fposmod(_elapsed, SWEEP_SECONDS) / SWEEP_SECONDS
var band: float = bar.size.x * SWEEP_WIDTH
var x: float = bar.position.x + (bar.size.x + band) * head - band
var left: float = maxf(x, bar.position.x)
var right: float = minf(x + band, bar.end.x)
if right > left:
draw_rect(Rect2(Vector2(left, bar.position.y),
Vector2(right - left, bar.size.y)), Color(GLOW, 0.8))
NeonDraw.draw_glowing_path(self, PackedVector2Array([
bar.position, Vector2(bar.end.x, bar.position.y), bar.end,
Vector2(bar.position.x, bar.end.y), bar.position,
]), Color(GLOW, 0.8), 0.7, 0.5)