Files
settled-reach/client/ui/implant/implant_panel.gd
T
jpmschweitzer dd13760d62 fix(client): cold-start round 2 — DERIVING TERRAIN state, legend re-fit, release server for make atlas
Item 1: while zero tiles have arrived, the viewer draws a centered
screen-space 'DERIVING TERRAIN…' label (text_dim role, no new hue),
dropping the instant the first tile lands — a cold wait now reads as
loading, not broken. New has_any_tile_arrived() predicate (distinct
from has_pending_tiles(): both true mid-arrival, tested exactly there).

Item 2: legend re-fit root-caused empirically, two plausible fixes
disproven by trace before the real one: a manually-positioned Control's
size NEVER tracks a shrinking minimum in this parenting shape, and
RichTextLabel.fit_content reports degenerate minimums until laid out at
real width once — so reset must be DEFERRED and run after refill, not
inside clear(). ImplantPanel.reset_to_content_size() (call_deferred),
wired into both legend refresh()es. The load-bearing test compares
size.y to get_minimum_size().y — a size-to-size comparison passed
trivially with both numbers equally stuck (caught on first draft).

Item 3: make atlas now builds the RELEASE server and passes
SR_SERVER_BIN (a cold DEBUG server delivers zero tiles for >10s on a
new body — live-measured — vs 210ms warm; release serves cold in well
under a second). atlas_standalone._server_binary_path() honors the env
override per the SR_PORT two-tier precedent, debug path unchanged
when unset.

All fixes revert-verified; nine suites green collateral-checked;
gdlint clean.
2026-07-22 19:51:12 +02:00

118 lines
4.5 KiB
GDScript

@tool
class_name ImplantPanel
extends PanelContainer
## Root container for any implant UI overlay (D-169).
## Add ImplantHeader, ImplantSeparator, ImplantDataRow, ImplantTextBlock
## as children — they auto-style from the shared theme.
@export var theme_resource: ImplantTheme:
set(value):
theme_resource = value
_apply_theme()
var _vbox: VBoxContainer
func _ready() -> void:
# Constrain panel to its custom_minimum_size width
size_flags_horizontal = Control.SIZE_SHRINK_END
clip_contents = true
_vbox = VBoxContainer.new()
_vbox.name = "Content"
add_child(_vbox)
_apply_theme()
func _apply_theme() -> void:
if not theme_resource:
return
# Panel background
var style := StyleBoxFlat.new()
style.bg_color = theme_resource.panel_bg
style.border_color = theme_resource.separator
style.border_width_top = int(theme_resource.border_width)
style.border_width_bottom = int(theme_resource.border_width)
style.border_width_left = int(theme_resource.border_width)
style.border_width_right = int(theme_resource.border_width)
style.content_margin_top = theme_resource.padding
style.content_margin_bottom = theme_resource.padding
style.content_margin_left = theme_resource.padding
style.content_margin_right = theme_resource.padding
add_theme_stylebox_override("panel", style)
# VBox spacing
if _vbox:
_vbox.add_theme_constant_override("separation", 0)
# Propagate theme to existing children
for child in get_implant_children():
if child.has_method("apply_implant_theme"):
child.apply_implant_theme(theme_resource)
## Add an implant component to the panel.
func add_component(component: Control) -> void:
if not _vbox:
_vbox = VBoxContainer.new()
_vbox.name = "Content"
add_child(_vbox)
_vbox.add_child(component)
if theme_resource and component.has_method("apply_implant_theme"):
component.apply_implant_theme(theme_resource)
## Get all implant component children.
func get_implant_children() -> Array:
if not _vbox:
return []
return _vbox.get_children()
## Clear all components. Frees IMMEDIATELY (remove_child() + free()), not
## via queue_free() — a caller that re-enters refresh()-shaped clear()+
## add_component() more than once in the SAME frame (confirmed live: the
## Atlas regional-window legend stacking ~10x around cold-start entry, PR
## #192 cold-start dossier) would otherwise see get_implant_children() still
## returning the STALE children (queue_free() only removes them at end of
## frame), so add_component()'s new ones pile up alongside instead of
## replacing them. Immediate removal makes clear() genuinely synchronous —
## the panel can never observe more than one refresh() worth of content, no
## matter how many times it's called before the next frame.
func clear() -> void:
if not _vbox:
return
for child in _vbox.get_children():
_vbox.remove_child(child)
child.free()
## PR #192 cold-start round 2: call once at the END of a refresh() — AFTER
## clear() + every add_component() for the new content. A Control's `size`
## GROWS to fit a rising minimum size automatically (normal Container
## behavior) but never shrinks back down on its own once a taller stack of
## content has pushed it up (confirmed live: the same stacking window that
## caused the duplicate-children bug above ALSO left the panel's rect
## oversized — 260x2343px, ~10 legends tall — even after the children-count
## fix landed). This panel is manually positioned (not itself inside a
## parent Container), so nothing else forces that re-measure.
##
## DEFERRED, not a direct reset_size() call — confirmed directly
## (instrumented and reverted through several shapes before landing here):
## get_minimum_size() is momentarily WRONG for a frame or more after
## add_component() runs (RichTextLabel.fit_content's minimum-height
## computation is width-dependent and the control hasn't been laid out with
## its real width yet — the classic Godot "fit_content measures huge before
## its first real layout pass" gap), and — critically — a DIRECT reset_size()
## call that captures that transitional wrong value does NOT self-correct
## later even once get_minimum_size() itself settles down on its own:
## `size` stays stuck at whatever the premature reset_size() captured,
## because nothing re-triggers a growth pass for an already-`_ready()`'d
## panel afterward. call_deferred() runs at the END of the current frame's
## idle processing — after layout has had its chance to update
## get_minimum_size() — so by the time this actually executes, it reads the
## real, settled value.
func reset_to_content_size() -> void:
call_deferred("reset_size")