feat(ui): implant component library + star map refactor (D-169)
New component library at client/ui/implant/: - ImplantTheme: shared Resource with colors, spacing, font sizes - ImplantPanel: PanelContainer root with themed background/border - ImplantHeader: title + subtitle - ImplantSeparator: themed horizontal rule with above/below spacing - ImplantDataRow: single-line text with optional color override - ImplantTextBlock: RichTextLabel for wrapping narrative text - default_implant.tres: default theme resource Star map info panel refactored from 140 lines of manual draw_string calls to 50 lines of component composition via _rebuild_info_panel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
[gd_resource type="Resource" script_class="ImplantTheme" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://ui/implant/implant_theme.gd" id="1"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
text_primary = Color(0.784, 0.816, 0.878, 1)
|
||||
text_dim = Color(0.4, 0.467, 0.533, 1)
|
||||
accent_active = Color(0.941, 0.816, 0.376, 1)
|
||||
accent_positive = Color(0.267, 0.667, 0.4, 1)
|
||||
accent_negative = Color(0.667, 0.267, 0.267, 1)
|
||||
accent_warning = Color(0.667, 0.533, 0.267, 1)
|
||||
panel_bg = Color(0.05, 0.08, 0.14, 0.92)
|
||||
separator = Color(0.78, 0.82, 0.88, 0.12)
|
||||
hover_bg = Color(0.78, 0.82, 0.88, 0.08)
|
||||
line_height = 18
|
||||
sep_above = 6
|
||||
sep_below = 12
|
||||
padding = 12
|
||||
border_width = 1.0
|
||||
font_header = 15
|
||||
font_body = 11
|
||||
font_small = 10
|
||||
font_caption = 9
|
||||
@@ -0,0 +1,28 @@
|
||||
@tool
|
||||
class_name ImplantDataRow
|
||||
extends Label
|
||||
## Single-line data display — text with optional color override.
|
||||
|
||||
var _color_override: Color = Color.TRANSPARENT # transparent = use theme default
|
||||
|
||||
|
||||
func _init(text: String = "", color: Color = Color.TRANSPARENT) -> void:
|
||||
self.text = text
|
||||
_color_override = color
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
|
||||
func set_content(text_value: String, color: Color = Color.TRANSPARENT) -> void:
|
||||
text = text_value
|
||||
_color_override = color
|
||||
if _color_override.a > 0.0:
|
||||
add_theme_color_override("font_color", _color_override)
|
||||
|
||||
|
||||
func apply_implant_theme(t: ImplantTheme) -> void:
|
||||
add_theme_font_size_override("font_size", t.font_body)
|
||||
if _color_override.a > 0.0:
|
||||
add_theme_color_override("font_color", _color_override)
|
||||
else:
|
||||
add_theme_color_override("font_color", t.text_dim)
|
||||
custom_minimum_size.y = t.line_height
|
||||
@@ -0,0 +1,37 @@
|
||||
@tool
|
||||
class_name ImplantHeader
|
||||
extends VBoxContainer
|
||||
## Panel header — title + optional subtitle.
|
||||
|
||||
var _title_label: Label
|
||||
var _subtitle_label: Label
|
||||
|
||||
|
||||
func _init(title: String = "", subtitle: String = "") -> void:
|
||||
_title_label = Label.new()
|
||||
_title_label.text = title
|
||||
_title_label.name = "Title"
|
||||
|
||||
_subtitle_label = Label.new()
|
||||
_subtitle_label.text = subtitle
|
||||
_subtitle_label.name = "Subtitle"
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
add_theme_constant_override("separation", 0)
|
||||
add_child(_title_label)
|
||||
add_child(_subtitle_label)
|
||||
|
||||
|
||||
func set_content(title: String, subtitle: String = "") -> void:
|
||||
_title_label.text = title
|
||||
_subtitle_label.text = subtitle
|
||||
_subtitle_label.visible = not subtitle.is_empty()
|
||||
|
||||
|
||||
func apply_implant_theme(t: ImplantTheme) -> void:
|
||||
_title_label.add_theme_font_size_override("font_size", t.font_header)
|
||||
_title_label.add_theme_color_override("font_color", t.text_primary)
|
||||
|
||||
_subtitle_label.add_theme_font_size_override("font_size", t.font_small)
|
||||
_subtitle_label.add_theme_color_override("font_color", t.text_dim)
|
||||
@@ -0,0 +1,74 @@
|
||||
@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:
|
||||
_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.
|
||||
func clear() -> void:
|
||||
if not _vbox:
|
||||
return
|
||||
for child in _vbox.get_children():
|
||||
child.queue_free()
|
||||
@@ -0,0 +1,28 @@
|
||||
@tool
|
||||
class_name ImplantSeparator
|
||||
extends Control
|
||||
## Thin horizontal separator line with themed spacing.
|
||||
|
||||
var _theme: ImplantTheme
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
|
||||
func apply_implant_theme(t: ImplantTheme) -> void:
|
||||
_theme = t
|
||||
custom_minimum_size.y = t.sep_above + t.sep_below
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
if not _theme:
|
||||
return
|
||||
var y: float = _theme.sep_above
|
||||
draw_line(
|
||||
Vector2(0, y),
|
||||
Vector2(size.x, y),
|
||||
_theme.separator,
|
||||
1.0
|
||||
)
|
||||
@@ -0,0 +1,32 @@
|
||||
@tool
|
||||
class_name ImplantTextBlock
|
||||
extends RichTextLabel
|
||||
## Multi-line wrapping text block for narrative content (GTTR excerpts, etc.).
|
||||
|
||||
var _max_lines: int = -1
|
||||
var _text_alpha: float = 0.75
|
||||
|
||||
|
||||
func _init(content: String = "", max_lines: int = -1) -> void:
|
||||
_max_lines = max_lines
|
||||
text = content
|
||||
bbcode_enabled = false
|
||||
fit_content = true
|
||||
scroll_active = false
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
if max_lines > 0:
|
||||
visible_characters_behavior = TextServer.VC_GLYPHS_LTR
|
||||
# Godot 4 RichTextLabel doesn't have max_lines directly —
|
||||
# we limit via visible_ratio or custom_minimum_size after layout.
|
||||
|
||||
|
||||
func set_content(content: String, max_lines: int = -1) -> void:
|
||||
text = content.replace("**", "") # strip markdown bold
|
||||
_max_lines = max_lines
|
||||
|
||||
|
||||
func apply_implant_theme(t: ImplantTheme) -> void:
|
||||
add_theme_font_size_override("normal_font_size", t.font_small)
|
||||
add_theme_color_override("default_color",
|
||||
Color(t.text_primary.r, t.text_primary.g, t.text_primary.b, _text_alpha))
|
||||
@@ -0,0 +1,46 @@
|
||||
class_name ImplantTheme
|
||||
extends Resource
|
||||
## Shared styling resource for all implant UI panels (D-169).
|
||||
## Swap this resource at runtime to change the implant's visual identity
|
||||
## (hardware variants, upgrades, faction overlays).
|
||||
|
||||
# ── Colors ───────────────────────────────────────────────────────────────────
|
||||
|
||||
## Primary readable text
|
||||
@export var text_primary: Color = Color("#c8d0e0")
|
||||
## Labels, metadata, captions
|
||||
@export var text_dim: Color = Color("#667788")
|
||||
## Selected, active, interactive highlight
|
||||
@export var accent_active: Color = Color("#f0d060")
|
||||
## Good state, opportunity, profit
|
||||
@export var accent_positive: Color = Color("#44aa66")
|
||||
## Danger, loss, hostile
|
||||
@export var accent_negative: Color = Color("#aa4444")
|
||||
## Caution, degraded, unknown
|
||||
@export var accent_warning: Color = Color("#aa8844")
|
||||
## Panel background
|
||||
@export var panel_bg: Color = Color(0.05, 0.08, 0.14, 0.92)
|
||||
## Panel border and separator lines
|
||||
@export var separator: Color = Color(0.78, 0.82, 0.88, 0.12)
|
||||
## Hover state background
|
||||
@export var hover_bg: Color = Color(0.78, 0.82, 0.88, 0.08)
|
||||
|
||||
# ── Spacing ──────────────────────────────────────────────────────────────────
|
||||
|
||||
## Line height for single-line labels (baseline to baseline)
|
||||
@export var line_height: int = 18
|
||||
## Space from last text baseline to separator line
|
||||
@export var sep_above: int = 6
|
||||
## Space from separator line to next text baseline
|
||||
@export var sep_below: int = 12
|
||||
## Panel internal padding
|
||||
@export var padding: int = 12
|
||||
## Border width
|
||||
@export var border_width: float = 1.0
|
||||
|
||||
# ── Font sizes ───────────────────────────────────────────────────────────────
|
||||
|
||||
@export var font_header: int = 15
|
||||
@export var font_body: int = 11
|
||||
@export var font_small: int = 10
|
||||
@export var font_caption: int = 9
|
||||
Reference in New Issue
Block a user