@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")