136 lines
5.3 KiB
GDScript
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)
|