Add tests for scenery surface caching and preview behavior

- Introduced `test_scenery_brick_preview.gd` to validate focused placement and CPU draw-cost for large brick walls.
- Added `test_scenery_surface_cache.gd` to assess texture budget, visual equivalence, and cache invalidation.
- Created `test_scenery_surface_preview.gd` to evaluate adaptive preview selection and placement behavior.
- Implemented various helper functions to measure drawing performance and validate expected behaviors during tests.
- Added unique identifiers for new test files to ensure proper tracking and management.
This commit is contained in:
2026-10-21 11:06:57 -04:00
parent 80ba83b703
commit c1758efd3b
54 changed files with 5941 additions and 526 deletions
@@ -0,0 +1,97 @@
class_name StageDepthChoicePreview
extends Control
## Small fixed-size diagram showing how scenery depth changes draw order.
const DEPTH_FAR := &"far"
const DEPTH_NEAR := &"near"
const DEPTH_FLOOR := &"floor"
var depth_id: StringName = DEPTH_NEAR
var artwork_texture: Texture2D
func configure(depth: StringName, texture: Texture2D, preview_size: Vector2) -> void:
depth_id = depth if depth in [DEPTH_FAR, DEPTH_NEAR, DEPTH_FLOOR] else DEPTH_NEAR
artwork_texture = texture
custom_minimum_size = preview_size
size_flags_horizontal = Control.SIZE_SHRINK_CENTER
size_flags_vertical = Control.SIZE_SHRINK_CENTER
mouse_filter = Control.MOUSE_FILTER_IGNORE
clip_contents = true
queue_redraw()
func set_artwork_texture(texture: Texture2D) -> void:
artwork_texture = texture
queue_redraw()
func _draw() -> void:
var bounds := Rect2(Vector2.ZERO, size)
draw_rect(bounds, Color("#B9D9D9"), true)
_draw_hills()
if depth_id != DEPTH_FAR:
_draw_near_scenery()
if depth_id == DEPTH_FLOOR:
_draw_ground_tiles()
_draw_artwork()
if depth_id == DEPTH_FAR:
_draw_near_scenery()
if depth_id != DEPTH_FLOOR:
_draw_ground_tiles()
draw_rect(bounds, Color("#566273"), false, 1.0)
func _draw_hills() -> void:
var points := PackedVector2Array([
Vector2(0.0, size.y * 0.68),
Vector2(0.0, size.y * 0.52),
Vector2(size.x * 0.18, size.y * 0.39),
Vector2(size.x * 0.38, size.y * 0.54),
Vector2(size.x * 0.62, size.y * 0.36),
Vector2(size.x * 0.84, size.y * 0.49),
Vector2(size.x, size.y * 0.41),
Vector2(size.x, size.y * 0.68),
])
draw_colored_polygon(points, Color("#79AFA4"))
func _draw_near_scenery() -> void:
var canopy := Color("#277447")
var trunk := Color("#84502B")
var canopy_center := Vector2(size.x * 0.49, size.y * 0.56)
var radius := maxf(4.0, size.y * 0.25)
draw_rect(Rect2(Vector2(size.x * 0.49 - 2.0, size.y * 0.46), Vector2(4.0, size.y * 0.23)), trunk, true)
draw_circle(canopy_center, radius, canopy)
draw_circle(canopy_center + Vector2(-radius * 0.52, radius * 0.08), radius * 0.62, Color("#48A84E"))
draw_circle(canopy_center + Vector2(radius * 0.48, radius * 0.02), radius * 0.55, Color("#36934A"))
func _draw_ground_tiles() -> void:
var top: float = size.y * 0.64
var tile_height: float = size.y - top
var tile_count: int = 5
var tile_width: float = size.x / float(tile_count)
for index: int in tile_count:
var tile := Rect2(Vector2(float(index) * tile_width, top), Vector2(tile_width + 0.5, tile_height))
draw_rect(tile, Color("#87512C"), true)
draw_rect(tile, Color("#A96C3B"), false, 1.0)
draw_line(Vector2(float(index) * tile_width + 2.0, top + tile_height * 0.58), Vector2(float(index + 1) * tile_width - 2.0, top + tile_height * 0.58), Color("#C9894B"), 1.0)
draw_rect(Rect2(Vector2(0.0, top - 3.0), Vector2(size.x, 6.0)), Color("#62D34D"), true)
draw_line(Vector2(0.0, top - 3.0), Vector2(size.x, top - 3.0), Color("#D5F27B"), 1.0)
func _draw_artwork() -> void:
if artwork_texture == null:
var center := Vector2(size.x * 0.49, size.y * 0.59)
draw_circle(center, maxf(3.0, minf(size.x, size.y) * 0.20), Color("#F4F1E8"))
draw_circle(center, maxf(3.0, minf(size.x, size.y) * 0.20), Color("#58616D"), false, 1.0)
return
var texture_size := Vector2(artwork_texture.get_size())
if texture_size.x <= 0.0 or texture_size.y <= 0.0:
return
var available := Vector2(size.x * 0.84, size.y * 0.84)
var scale_factor: float = minf(available.x / texture_size.x, available.y / texture_size.y)
var target_size := texture_size * scale_factor
var target := Rect2(Vector2((size.x - target_size.x) * 0.5, size.y * 0.59 - target_size.y * 0.5), target_size)
draw_texture_rect(artwork_texture, target, false)
@@ -0,0 +1 @@
uid://cc5hn0jge7758
@@ -116,6 +116,52 @@ func style_action_button(button: Button, prominent: bool = false) -> void:
_style_action_button(button, prominent)
func style_primary_action_button(button: Button) -> void:
if button == null:
return
var primary := _color("stage_panels.action.primary_color", Color("#087DBA"))
var hover := _color("stage_panels.action.primary_hover_color", Color("#1596D3"))
var pressed := _color("stage_panels.action.primary_pressed_color", Color("#066B9F"))
var accent := _color("mode_colors.edit", Color("#22C6FF"))
button.add_theme_stylebox_override("normal", _make_style(primary, accent.lightened(0.15), 2, 10, 6))
button.add_theme_stylebox_override("hover", _make_style(hover, accent, 2, 10, 6))
button.add_theme_stylebox_override("pressed", _make_style(pressed, accent, 2, 10, 6))
button.add_theme_stylebox_override("hover_pressed", _make_style(pressed.lightened(0.05), accent, 2, 10, 6))
button.add_theme_stylebox_override("disabled", _make_style(_color("stage_panels.choice.disabled_surface", Color("#252B38")), _color("global.border_color", Color("#FFFFFF33")), 1, 10, 6))
button.add_theme_stylebox_override("focus", _make_style(Color(0, 0, 0, 0), accent, 2, 10, 6))
button.add_theme_font_size_override("font_size", _integer("stage_panels.type.body_size", 20, 14, 40))
button.add_theme_color_override("font_color", Color.WHITE)
button.add_theme_color_override("font_hover_color", Color.WHITE)
button.add_theme_color_override("font_pressed_color", Color.WHITE)
if _stage_theme != null:
var ui_font := _stage_theme.font()
if ui_font != null:
button.add_theme_font_override("font", ui_font)
func style_destructive_button(button: Button) -> void:
if button == null:
return
var danger := _color("stage_panels.action.destructive_color", Color("#FF626B"))
var neutral := _color("stage_panels.action.surface_color", Color("#171B24"))
var hover := _color("stage_panels.action.destructive_hover_surface", Color("#3B252C"))
var pressed := _color("stage_panels.action.destructive_pressed_surface", Color("#4A252D"))
var border := _color("global.border_color", Color("#FFFFFF33"))
var accent := _color("mode_colors.edit", Color("#22C6FF"))
button.add_theme_stylebox_override("normal", _make_style(neutral, danger, 2, 10, 6))
button.add_theme_stylebox_override("hover", _make_style(hover, danger, 2, 10, 6))
button.add_theme_stylebox_override("pressed", _make_style(pressed, danger, 2, 10, 6))
button.add_theme_stylebox_override("hover_pressed", _make_style(pressed.lightened(0.06), danger, 2, 10, 6))
button.add_theme_stylebox_override("disabled", _make_style(_color("stage_panels.choice.disabled_surface", Color("#252B38")), border, 1, 10, 6))
button.add_theme_stylebox_override("focus", _make_style(Color(0, 0, 0, 0), accent, 2, 10, 6))
button.add_theme_font_size_override("font_size", _integer("stage_panels.type.body_size", 20, 14, 40))
button.add_theme_color_override("font_color", _color("stage_panels.type.text_color", Color("#F5F7FA")))
if _stage_theme != null:
var ui_font := _stage_theme.font()
if ui_font != null:
button.add_theme_font_override("font", ui_font)
func _build_shell() -> void:
if _body != null:
return
@@ -145,11 +191,12 @@ func _build_shell() -> void:
_title.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_header.add_child(_title)
_close_button = Button.new()
_close_button.text = "✕ Close"
_close_button.text = "Close"
_close_button.custom_minimum_size = Vector2(
float(_integer("stage_panels.action.close_width", 88, 72, 180)),
float(_integer("stage_panels.action.min_target_size", 48, 44, 96))
)
_close_button.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
_close_button.focus_mode = Control.FOCUS_ALL
_close_button.pressed.connect(func() -> void: close_requested.emit())
_header.add_child(_close_button)
@@ -1,5 +1,5 @@
class_name StagePanelChoiceGroup
extends VBoxContainer
extends BoxContainer
## Exclusive, full-width choices with optional decorative cues.
signal choice_selected(id: StringName)
@@ -8,20 +8,43 @@ var _stage_theme: SandboxTheme = null
var _group := ButtonGroup.new()
var _buttons: Dictionary = {}
var _selected_id: StringName = &""
var _vertical_layout: bool = true
var _minimum_height_override: float = -1.0
var _font_size_override: int = -1
var _corner_radius_override: int = -1
var _border_width_override: int = -1
var _selected_surface_override: Color = Color.TRANSPARENT
var _has_selected_surface_override: bool = false
func _ready() -> void:
vertical = _vertical_layout
_group.allow_unpress = true
add_theme_constant_override("separation", _integer("stage_panels.choice.row_gap", 6, 0, 32))
func add_choice(id: StringName, caption: String, cue: Control = null) -> Button:
func configure_layout(vertical_layout: bool, minimum_height: float = -1.0, font_size: int = -1, corner_radius: int = -1, border_width: int = -1, selected_surface: Color = Color.TRANSPARENT) -> void:
_vertical_layout = vertical_layout
vertical = vertical_layout
_minimum_height_override = minimum_height
_font_size_override = font_size
_corner_radius_override = corner_radius
_border_width_override = border_width
_selected_surface_override = selected_surface
_has_selected_surface_override = selected_surface.a > 0.0
for entry: Dictionary in _buttons.values():
_apply_choice_theme(entry["button"] as Button, entry["label"] as Label, entry.get("detail") as Label)
func add_choice(id: StringName, caption: String, cue: Control = null, detail: String = "") -> Button:
var button := Button.new()
button.name = "Choice_%s" % String(id)
button.toggle_mode = true
button.button_group = _group
button.custom_minimum_size.y = float(_integer("stage_panels.choice.minimum_height", 56, 44, 112))
button.custom_minimum_size.y = _minimum_height_override if _minimum_height_override > 0.0 else float(_integer("stage_panels.choice.minimum_height", 56, 44, 112))
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
if not _vertical_layout:
button.size_flags_vertical = Control.SIZE_EXPAND_FILL
button.focus_mode = Control.FOCUS_ALL
button.text = ""
var margin := MarginContainer.new()
@@ -42,16 +65,31 @@ func add_choice(id: StringName, caption: String, cue: Control = null) -> Button:
var label := Label.new()
label.text = caption
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT if not detail.is_empty() else HORIZONTAL_ALIGNMENT_CENTER
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
row.add_child(label)
var detail_label: Label
if detail.is_empty():
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
row.add_child(label)
else:
var text_stack := VBoxContainer.new()
text_stack.add_theme_constant_override("separation", 0)
text_stack.size_flags_horizontal = Control.SIZE_EXPAND_FILL
text_stack.mouse_filter = Control.MOUSE_FILTER_IGNORE
text_stack.add_child(label)
detail_label = Label.new()
detail_label.text = detail
detail_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
detail_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
detail_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
text_stack.add_child(detail_label)
row.add_child(text_stack)
button.pressed.connect(_on_choice_pressed.bind(id, button))
add_child(button)
_buttons[id] = {"button": button, "label": label}
_buttons[id] = {"button": button, "label": label, "detail": detail_label}
if _stage_theme != null:
_apply_choice_theme(button, label)
_apply_choice_theme(button, label, detail_label)
return button
@@ -75,7 +113,7 @@ func apply_theme(stage_theme: SandboxTheme) -> void:
_stage_theme = stage_theme
add_theme_constant_override("separation", _integer("stage_panels.choice.row_gap", 6, 0, 32))
for entry: Dictionary in _buttons.values():
_apply_choice_theme(entry["button"] as Button, entry["label"] as Label)
_apply_choice_theme(entry["button"] as Button, entry["label"] as Label, entry.get("detail") as Label)
func _on_choice_pressed(id: StringName, button: Button) -> void:
@@ -87,26 +125,40 @@ func _on_choice_pressed(id: StringName, button: Button) -> void:
choice_selected.emit(id)
func _apply_choice_theme(button: Button, label: Label) -> void:
func _apply_choice_theme(button: Button, label: Label, detail: Label = null) -> void:
if _stage_theme == null:
return
var font := _stage_theme.font()
if font != null:
label.add_theme_font_override("font", font)
label.add_theme_font_size_override("font_size", _integer("stage_panels.type.body_size", 20, 14, 40))
if not _vertical_layout:
var strong_font := FontVariation.new()
strong_font.base_font = font
strong_font.variation_embolden = 0.25
label.add_theme_font_override("font", strong_font)
else:
label.add_theme_font_override("font", font)
var font_size: int = _font_size_override if _font_size_override > 0 else _integer("stage_panels.type.body_size", 20, 14, 40)
label.add_theme_font_size_override("font_size", font_size)
label.add_theme_color_override("font_color", _color("stage_panels.type.text_color", Color("#F5F7FA")))
if detail != null:
if font != null:
detail.add_theme_font_override("font", font)
detail.add_theme_font_size_override("font_size", maxi(12, font_size - 4))
detail.add_theme_color_override("font_color", _color("stage_panels.type.muted_text_color", Color("#AAB2C0")))
var neutral := _color("stage_panels.choice.neutral_surface", Color("#171B24"))
var hover := _color("stage_panels.choice.hover_surface", Color("#303A4B"))
var selected := _color("stage_panels.choice.selected_surface", Color("#12394A"))
var selected := _selected_surface_override if _has_selected_surface_override else _color("stage_panels.choice.selected_surface", Color("#12394A"))
var disabled := _color("stage_panels.choice.disabled_surface", Color("#252B38"))
var border := _color("stage_panels.choice.border_color", Color("#FFFFFF33"))
var accent := _color("mode_colors.edit", Color("#22C6FF"))
button.add_theme_stylebox_override("normal", _make_style(neutral, border, 1))
button.add_theme_stylebox_override("hover", _make_style(hover, border, 1))
button.add_theme_stylebox_override("pressed", _make_style(selected, accent, 2))
button.add_theme_stylebox_override("hover_pressed", _make_style(selected.lightened(0.06), accent, 2))
button.add_theme_stylebox_override("disabled", _make_style(disabled, border, 1))
button.add_theme_stylebox_override("focus", _make_style(Color(0, 0, 0, 0), accent, 2))
var border_width: int = _border_width_override if _border_width_override >= 0 else 1
var selected_border_width: int = border_width + 1 if _border_width_override >= 0 else 2
button.add_theme_stylebox_override("normal", _make_style(neutral, border, border_width))
button.add_theme_stylebox_override("hover", _make_style(hover, border, border_width))
button.add_theme_stylebox_override("pressed", _make_style(selected, accent, selected_border_width))
button.add_theme_stylebox_override("hover_pressed", _make_style(selected.lightened(0.06), accent, selected_border_width))
button.add_theme_stylebox_override("disabled", _make_style(disabled, border, border_width))
button.add_theme_stylebox_override("focus", _make_style(Color(0, 0, 0, 0), accent, selected_border_width))
func _make_style(bg: Color, border: Color, width: int) -> StyleBoxFlat:
@@ -114,7 +166,8 @@ func _make_style(bg: Color, border: Color, width: int) -> StyleBoxFlat:
style.bg_color = bg
style.border_color = border
style.set_border_width_all(width)
style.set_corner_radius_all(_integer("stage_panels.shell.corner_radius", 10, 0, 32))
var radius: int = _corner_radius_override if _corner_radius_override >= 0 else _integer("stage_panels.shell.corner_radius", 10, 0, 32)
style.set_corner_radius_all(radius)
return style
@@ -17,7 +17,6 @@ var _current_value: Label
var _recent_empty_label: Label
var _recent_swatches: ColorSwatchSelector
var _preset_swatches: ColorSwatchSelector
var _more_button: Button
var _compact_mode: bool = false
@@ -54,60 +53,107 @@ func set_recent_colors(colors: Array[Color]) -> void:
func focus_custom_button() -> void:
if _more_button != null:
_more_button.grab_focus()
if _current_chip != null:
_current_chip.grab_focus()
func _build() -> void:
_built = true
_apply_compact_layout()
var current_heading: Label = _make_heading("Color")
add_child(current_heading)
var current_parent: Control = self
var recent_parent: Control = self
var presets_parent: Control = self
if _compact_mode:
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", _integer("popups.asset_selector.color_group_gap", 8, 4, 32))
row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
add_child(row)
current_parent = _make_compact_section(132.0)
row.add_child(current_parent)
row.add_child(_make_compact_separator())
recent_parent = _make_compact_section(92.0)
row.add_child(recent_parent)
row.add_child(_make_compact_separator())
presets_parent = _make_compact_section(180.0)
presets_parent.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_child(presets_parent)
_build_current_color_section(current_parent)
_build_recent_color_section(recent_parent)
_build_palette_section(presets_parent)
if _stage_theme != null:
var font: Font = _stage_theme.font()
if font != null:
if _compact_mode:
var strong_font := FontVariation.new()
strong_font.base_font = font
strong_font.variation_embolden = 0.2
_current_value.add_theme_font_override("font", strong_font)
else:
_current_value.add_theme_font_override("font", font)
_current_value.add_theme_font_size_override("font_size", _integer("stage_panels.type.body_size", 20, 14, 40))
func _build_current_color_section(parent: Control) -> void:
parent.add_child(_make_heading("Color"))
var current_row := HBoxContainer.new()
current_row.add_theme_constant_override("separation", _compact_gap())
add_child(current_row)
parent.add_child(current_row)
_current_chip = Button.new()
var swatch_size: float = float(_integer("stage_surface_colors.swatch_size", 48, 44, 96))
if _compact_mode:
swatch_size = minf(swatch_size, 44.0)
swatch_size = maxf(swatch_size, 52.0)
_current_chip.custom_minimum_size = Vector2(swatch_size, swatch_size)
_current_chip.focus_mode = Control.FOCUS_NONE
_current_chip.mouse_filter = Control.MOUSE_FILTER_IGNORE
_current_chip.focus_mode = Control.FOCUS_ALL
_current_chip.tooltip_text = "Choose a custom color"
_current_chip.pressed.connect(func() -> void: custom_color_requested.emit())
current_row.add_child(_current_chip)
_current_value = Label.new()
_current_value.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_current_value.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
current_row.add_child(_current_value)
add_child(_make_heading("Recently used"))
func _build_recent_color_section(parent: Control) -> void:
parent.add_child(_make_heading("Recently used"))
_recent_empty_label = Label.new()
_recent_empty_label.text = "Colors you use will appear here"
_recent_empty_label.text = "No colors yet" if _compact_mode else "Colors you use will appear here"
_recent_empty_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_recent_empty_label.add_theme_color_override("font_color", _color("global.muted_text_color", Color("#AAB2C0")))
add_child(_recent_empty_label)
if _compact_mode:
_recent_empty_label.add_theme_font_size_override("font_size", _integer("stage_panels.type.helper_size", 16, 12, 32))
parent.add_child(_recent_empty_label)
_recent_swatches = COLOR_SWATCH_SELECTOR.new() as ColorSwatchSelector
_recent_swatches.size_flags_horizontal = Control.SIZE_EXPAND_FILL
if _compact_mode:
_recent_swatches.set_swatch_layout_override(_integer("popups.asset_selector.compact_swatch_size", 42, 28, 52), 8)
_recent_swatches.color_selected.connect(_on_swatch_selected)
add_child(_recent_swatches)
add_child(_make_heading("Colors"))
parent.add_child(_recent_swatches)
func _build_palette_section(parent: Control) -> void:
parent.add_child(_make_heading("Colors"))
_preset_swatches = COLOR_SWATCH_SELECTOR.new() as ColorSwatchSelector
_preset_swatches.size_flags_horizontal = Control.SIZE_EXPAND_FILL
if _compact_mode:
_preset_swatches.set_swatch_layout_override(_integer("popups.asset_selector.compact_swatch_size", 42, 28, 52), 8)
_preset_swatches.color_selected.connect(_on_swatch_selected)
add_child(_preset_swatches)
_more_button = Button.new()
_more_button.text = "More colors…"
var action_height: float = float(_integer("stage_panels.action.min_target_size", 48, 44, 96))
_more_button.custom_minimum_size.y = minf(action_height, 44.0) if _compact_mode else action_height
_more_button.focus_mode = Control.FOCUS_ALL
_more_button.pressed.connect(func() -> void: custom_color_requested.emit())
add_child(_more_button)
if _stage_theme != null:
var font: Font = _stage_theme.font()
if font != null:
_current_value.add_theme_font_override("font", font)
_more_button.add_theme_font_override("font", font)
_current_value.add_theme_font_size_override("font_size", _integer("stage_panels.type.body_size", 20, 14, 40))
_more_button.add_theme_font_size_override("font_size", _integer("stage_panels.type.body_size", 20, 14, 40))
_more_button.add_theme_color_override("font_color", _color("global.text_color", Color("#F5F7FA")))
_more_button.add_theme_stylebox_override("normal", _stage_theme.stylebox("stage_panels.action"))
_more_button.add_theme_stylebox_override("hover", _stage_theme.stylebox("stage_panels.action").duplicate())
parent.add_child(_preset_swatches)
func _make_compact_section(minimum_width: float) -> VBoxContainer:
var section := VBoxContainer.new()
section.custom_minimum_size.x = minimum_width
section.add_theme_constant_override("separation", 4)
return section
func _make_compact_separator() -> ColorRect:
var separator := ColorRect.new()
separator.color = _color("popups.asset_selector.separator_color", Color("#AAB2C0A0"))
separator.custom_minimum_size = Vector2(2.0, 64.0)
separator.size_flags_vertical = Control.SIZE_SHRINK_CENTER
separator.mouse_filter = Control.MOUSE_FILTER_IGNORE
return separator
func _refresh_visuals() -> void:
@@ -124,11 +170,16 @@ func _refresh_current_color() -> void:
_current_value.text = "#%s" % _selected.to_html(false).to_upper()
var chip_style := StyleBoxFlat.new()
chip_style.bg_color = _selected
chip_style.border_color = _color("stage_panels.choice.border_color", Color("#FFFFFFAA"))
chip_style.set_border_width_all(_integer("stage_surface_colors.swatch_border_width", 2, 0, 8))
chip_style.border_color = _color("mode_colors.edit", Color("#22C6FF"))
chip_style.set_border_width_all(_integer("stage_surface_colors.swatch_border_width", 3, 1, 8))
chip_style.set_corner_radius_all(_integer("stage_surface_colors.swatch_corner_radius", 8, 0, 24))
_current_chip.add_theme_stylebox_override("normal", chip_style)
_current_chip.add_theme_stylebox_override("hover", chip_style.duplicate())
_current_chip.add_theme_stylebox_override("pressed", chip_style.duplicate())
var focus_style: StyleBoxFlat = chip_style.duplicate() as StyleBoxFlat
focus_style.border_color = Color.WHITE
focus_style.set_border_width_all(5)
_current_chip.add_theme_stylebox_override("focus", focus_style)
func _on_swatch_selected(color: Color) -> void:
@@ -143,18 +194,23 @@ func _make_heading(text_value: String) -> Label:
var heading := Label.new()
heading.text = text_value
var heading_size: int = _integer("stage_panels.type.section_size", 20, 12, 40)
if _compact_mode:
heading_size = mini(heading_size, 18)
heading.add_theme_font_size_override("font_size", heading_size)
heading.add_theme_color_override("font_color", _color("stage_panels.type.text_color", Color("#F5F7FA")))
if _stage_theme != null and _stage_theme.font() != null:
heading.add_theme_font_override("font", _stage_theme.font())
var font: Font = _stage_theme.font()
if _compact_mode:
var strong_font := FontVariation.new()
strong_font.base_font = font
strong_font.variation_embolden = 0.2
heading.add_theme_font_override("font", strong_font)
else:
heading.add_theme_font_override("font", font)
return heading
func _apply_compact_layout() -> void:
var section_gap: int = _integer("stage_surface_colors.section_gap", 8, 0, 28)
add_theme_constant_override("separation", mini(section_gap, 4) if _compact_mode else section_gap)
add_theme_constant_override("separation", section_gap)
func _compact_gap() -> int:
@@ -1,22 +1,37 @@
class_name StageSurfaceColorDialog
extends Window
## Bounded custom-color dialog with a live Stage surface preview.
extends PopupPanel
## Rounded, reusable Stage color popup with the shared surface picker view.
signal preview_color_changed(color: Color)
signal color_confirmed(color: Color)
signal cancelled
const SURFACE_PREVIEW := preload("res://scripts/stage/scenery/stage_surface_preview.gd")
const SURFACE_COLOR_PICKER_VIEW := preload("res://scripts/shared/stage_ui/stage_surface_color_picker_view.gd")
const COMPACT_LAYOUT_MAX_HEIGHT := 640
var _surface_kind: StringName = &"wall"
var _finish_id: StringName = &"finish_wall_paint"
var _initial_color: Color = Color.WHITE
var _stage_theme: SandboxTheme
var _color_picker: ColorPicker
var _surface_preview: StageSurfacePreview
var _margin: MarginContainer
var _layout: VBoxContainer
var _heading: Label
var _view_scroll: ScrollContainer
var _picker_view: StageSurfaceColorPickerView
var _actions: HBoxContainer
var _cancel_button: Button
var _use_button: Button
var _built: bool = false
func _ready() -> void:
exclusive = true
_build()
close_requested.connect(_cancel)
get_tree().root.size_changed.connect(_on_root_size_changed)
_apply_visual_style()
func configure(surface_kind: StringName, finish: StringName, color: Color, stage_theme: SandboxTheme) -> void:
_surface_kind = surface_kind
_finish_id = finish
@@ -24,26 +39,20 @@ func configure(surface_kind: StringName, finish: StringName, color: Color, stage
_stage_theme = stage_theme
if not _built:
_build()
_color_picker.color = color
_update_preview(color)
title = "Choose a color"
exclusive = true
transient = true
theme = stage_theme.control_theme() if stage_theme != null else null
if _picker_view != null:
_picker_view.configure(_surface_kind, _finish_id, color, _stage_theme)
theme = _stage_theme.control_theme() if _stage_theme != null else null
_apply_visual_style()
func open_dialog() -> void:
var max_size: Vector2i = _usable_window_size()
var preferred_size := Vector2i(560, 640)
var raw_size: Variant = _stage_theme.value("popups.dialog.surface_color_size", [560, 640]) if _stage_theme != null else [560, 640]
if raw_size is Array and (raw_size as Array).size() == 2:
preferred_size = Vector2i(int((raw_size as Array)[0]), int((raw_size as Array)[1]))
var dialog_size := Vector2i(mini(preferred_size.x, max_size.x), mini(preferred_size.y, max_size.y))
dialog_size.x = maxi(1, dialog_size.x)
dialog_size.y = maxi(1, dialog_size.y)
size = dialog_size
popup_centered(dialog_size)
_color_picker.grab_focus()
if not _built or _picker_view == null:
return
var popup_size: Vector2i = _bounded_dialog_size()
_apply_responsive_layout(popup_size)
size = popup_size
popup_centered(popup_size)
call_deferred("_focus_picker")
func _unhandled_key_input(event: InputEvent) -> void:
@@ -53,98 +62,168 @@ func _unhandled_key_input(event: InputEvent) -> void:
func _build() -> void:
if _built:
return
_built = true
var margin := MarginContainer.new()
margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_margin = MarginContainer.new()
_margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
add_child(_margin)
_layout = VBoxContainer.new()
_margin.add_child(_layout)
_heading = Label.new()
_heading.name = "ChooseColorHeading"
_heading.text = "Choose a color"
_heading.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_heading.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_heading.mouse_filter = Control.MOUSE_FILTER_IGNORE
_layout.add_child(_heading)
_view_scroll = ScrollContainer.new()
_view_scroll.name = "PickerScroll"
_view_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
_view_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_view_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
_layout.add_child(_view_scroll)
_picker_view = SURFACE_COLOR_PICKER_VIEW.new() as StageSurfaceColorPickerView
_picker_view.name = "SurfaceColorPickerView"
_picker_view.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_picker_view.preview_color_changed.connect(_on_preview_color_changed)
_view_scroll.add_child(_picker_view)
_actions = HBoxContainer.new()
_actions.name = "Actions"
_actions.alignment = BoxContainer.ALIGNMENT_END
_actions.add_theme_constant_override("separation", 8)
_layout.add_child(_actions)
_cancel_button = Button.new()
_cancel_button.name = "CancelButton"
_cancel_button.text = "Cancel"
_cancel_button.focus_mode = Control.FOCUS_ALL
_cancel_button.pressed.connect(_cancel)
_actions.add_child(_cancel_button)
_use_button = Button.new()
_use_button.name = "UseColorButton"
_use_button.text = "Use color"
_use_button.focus_mode = Control.FOCUS_ALL
_use_button.pressed.connect(_confirm_color)
_actions.add_child(_use_button)
func _apply_visual_style() -> void:
if _stage_theme == null or not _built:
return
theme = _stage_theme.control_theme()
var radius: int = _integer("popups.asset_selector.corner_radius", 16, 8, 32)
var shell: StyleBoxFlat = _stage_theme.stylebox("popups.asset_selector")
shell.set_corner_radius_all(radius)
shell.content_margin_left = 16.0
shell.content_margin_right = 16.0
shell.content_margin_top = 14.0
shell.content_margin_bottom = 14.0
add_theme_stylebox_override("panel", shell)
var text_color: Color = _color("stage_panels.type.text_color", Color("#F5F7FA"))
var font: Font = _stage_theme.font()
if font != null:
_heading.add_theme_font_override("font", font)
_cancel_button.add_theme_font_override("font", font)
var strong_font := FontVariation.new()
strong_font.base_font = font
strong_font.variation_embolden = 0.55
_use_button.add_theme_font_override("font", strong_font)
_heading.add_theme_font_size_override("font_size", _integer("popups.asset_selector.setup_title_size", 30, 22, 48))
_heading.add_theme_color_override("font_color", text_color)
_apply_action_button(_cancel_button, false)
_apply_action_button(_use_button, true)
func _apply_action_button(button: Button, primary: bool) -> void:
var accent: Color = _color("mode_colors.edit", Color("#22C6FF"))
var text_color: Color = _color("stage_panels.type.text_color", Color("#F5F7FA"))
var neutral: Color = _color("stage_panels.choice.neutral_surface", Color("#171B24"))
var hover: Color = _color("stage_panels.choice.hover_surface", Color("#303A4B"))
var selected: Color = _color("stage_panels.choice.selected_surface", Color("#12394A"))
var border: Color = _color("stage_panels.choice.border_color", Color("#FFFFFF33"))
var blue: Color = _color("popups.asset_selector.action_color", Color("#087DBA"))
var radius: int = _integer("popups.asset_selector.corner_radius", 16, 8, 32)
var border_width: int = _integer("popups.asset_selector.button_border_width", 2, 1, 5)
var normal_color: Color = blue if primary else neutral
var hover_color: Color = blue.lightened(0.12) if primary else hover
var pressed_color: Color = blue.darkened(0.12) if primary else selected
var edge: Color = accent.lightened(0.15) if primary else border
button.add_theme_stylebox_override("normal", _make_button_style(normal_color, edge, border_width, radius))
button.add_theme_stylebox_override("hover", _make_button_style(hover_color, edge, border_width + 1, radius))
button.add_theme_stylebox_override("pressed", _make_button_style(pressed_color, accent, border_width + 1, radius))
button.add_theme_stylebox_override("hover_pressed", _make_button_style(pressed_color.lightened(0.05), accent, border_width + 1, radius))
button.add_theme_stylebox_override("focus", _make_button_style(Color(0, 0, 0, 0), accent, border_width + 1, radius))
button.add_theme_color_override("font_color", Color.WHITE if primary else text_color)
button.add_theme_color_override("font_hover_color", Color.WHITE if primary else text_color)
button.add_theme_color_override("font_pressed_color", Color.WHITE if primary else text_color)
button.add_theme_font_size_override("font_size", _integer("popups.asset_selector.setup_button_font_size", 20, 14, 40))
button.custom_minimum_size.y = float(_integer("popups.dialog.action_min_target_size", 48, 44, 96))
func _apply_responsive_layout(popup_size: Vector2i) -> void:
if not _built:
return
var compact: bool = popup_size.y < COMPACT_LAYOUT_MAX_HEIGHT
var margin_size: int = 8 if compact else 12
for side: String in ["left", "right", "top", "bottom"]:
margin.add_theme_constant_override("margin_" + side, 12)
add_child(margin)
var layout := VBoxContainer.new()
layout.add_theme_constant_override("separation", 8)
margin.add_child(layout)
var preview_heading := Label.new()
preview_heading.text = "Preview"
layout.add_child(preview_heading)
_surface_preview = SURFACE_PREVIEW.new() as StageSurfacePreview
var preview_size := Vector2(420.0, 140.0)
var raw_preview: Variant = _stage_theme.value("popups.dialog.surface_color_preview_size", [420, 140]) if _stage_theme != null else [420, 140]
if raw_preview is Array and (raw_preview as Array).size() == 2:
preview_size = Vector2(float((raw_preview as Array)[0]), float((raw_preview as Array)[1]))
preview_size.x = minf(preview_size.x, maxf(1.0, float(_usable_window_size().x - 56)))
_surface_preview.custom_minimum_size = preview_size
_surface_preview.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_surface_preview.mouse_filter = Control.MOUSE_FILTER_IGNORE
layout.add_child(_surface_preview)
var picker_scroll := ScrollContainer.new()
picker_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
picker_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
picker_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
layout.add_child(picker_scroll)
_color_picker = ColorPicker.new()
_color_picker.name = "ColorPickerControl"
var picker_min_size := Vector2(320.0, 340.0)
var raw_picker_size: Variant = _stage_theme.value("popups.dialog.surface_color_picker_min_size", [320, 340]) if _stage_theme != null else [320, 340]
if raw_picker_size is Array and (raw_picker_size as Array).size() == 2:
picker_min_size = Vector2(float((raw_picker_size as Array)[0]), float((raw_picker_size as Array)[1]))
picker_min_size.x = minf(picker_min_size.x, maxf(1.0, float(_usable_window_size().x - 56)))
_color_picker.custom_minimum_size = picker_min_size
_color_picker.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_color_picker.focus_mode = Control.FOCUS_ALL
_color_picker.color_changed.connect(_on_picker_color_changed)
picker_scroll.add_child(_color_picker)
var actions := HBoxContainer.new()
actions.alignment = BoxContainer.ALIGNMENT_END
actions.add_theme_constant_override("separation", 8)
layout.add_child(actions)
var use_button := Button.new()
use_button.text = "Use color"
var action_width: float = maxf(44.0, minf(120.0, (float(_usable_window_size().x) - 40.0) * 0.5))
use_button.custom_minimum_size = Vector2(action_width, float(_integer("popups.dialog.action_min_target_size", 48, 44, 96)))
use_button.focus_mode = Control.FOCUS_ALL
use_button.pressed.connect(_confirm_color)
actions.add_child(use_button)
var cancel_button := Button.new()
cancel_button.text = "✕ Cancel"
cancel_button.custom_minimum_size = Vector2(action_width, float(_integer("popups.dialog.action_min_target_size", 48, 44, 96)))
cancel_button.focus_mode = Control.FOCUS_ALL
cancel_button.pressed.connect(_cancel)
actions.add_child(cancel_button)
close_requested.connect(_cancel)
if _stage_theme != null:
var font: Font = _stage_theme.font()
if font != null:
preview_heading.add_theme_font_override("font", font)
use_button.add_theme_font_override("font", font)
cancel_button.add_theme_font_override("font", font)
preview_heading.add_theme_font_size_override("font_size", _stage_theme.integer("stage_panels.type.section_size", 20, 12, 40))
use_button.add_theme_stylebox_override("normal", _stage_theme.stylebox("stage_panels.action"))
cancel_button.add_theme_stylebox_override("normal", _stage_theme.stylebox("stage_panels.action"))
use_button.add_theme_color_override("font_color", _stage_theme.color("global.text_color", Color("#F5F7FA")))
cancel_button.add_theme_color_override("font_color", _stage_theme.color("global.text_color", Color("#F5F7FA")))
_margin.add_theme_constant_override("margin_" + side, margin_size)
_layout.add_theme_constant_override("separation", 4 if compact else 8)
_heading.add_theme_font_size_override("font_size", _integer("popups.asset_selector.setup_title_size", 30, 22, 48))
var content_width: float = maxf(1.0, float(popup_size.x - margin_size * 2))
_picker_view.set_available_size(Vector2(content_width, float(popup_size.y)))
_apply_action_layout(content_width)
func _on_picker_color_changed(color: Color) -> void:
_update_preview(color)
func _apply_action_layout(content_width: float) -> void:
if _actions == null or _cancel_button == null or _use_button == null:
return
var stack_buttons: bool = content_width < 344.0
_actions.vertical = stack_buttons
_actions.alignment = BoxContainer.ALIGNMENT_END
var action_height: float = float(_integer("popups.dialog.action_min_target_size", 48, 44, 96))
var button_width: float = content_width if stack_buttons else maxf(112.0, (content_width - 8.0) * 0.5)
_cancel_button.custom_minimum_size = Vector2(button_width, action_height)
_use_button.custom_minimum_size = Vector2(button_width, action_height)
func _on_preview_color_changed(color: Color) -> void:
preview_color_changed.emit(color)
func _update_preview(color: Color) -> void:
if _surface_preview != null:
_surface_preview.configure(_surface_kind, _finish_id, color)
func _confirm_color() -> void:
var confirmed_color: Color = _color_picker.color
if _picker_view == null:
return
var confirmed_color: Color = _picker_view.get_color()
hide()
color_confirmed.emit(confirmed_color)
func _cancel() -> void:
_update_preview(_initial_color)
if _picker_view != null:
_picker_view.set_color_no_signal(_initial_color)
hide()
cancelled.emit()
func _focus_picker() -> void:
if visible and _picker_view != null:
_picker_view.focus_picker()
func _on_root_size_changed() -> void:
if visible:
open_dialog()
func _bounded_dialog_size() -> Vector2i:
var max_size: Vector2i = _usable_window_size()
var preferred_size := Vector2i(560, 640)
var raw_size: Variant = _stage_theme.value("popups.dialog.surface_color_size", [560, 640]) if _stage_theme != null else [560, 640]
if raw_size is Array and (raw_size as Array).size() == 2:
preferred_size = Vector2i(int((raw_size as Array)[0]), int((raw_size as Array)[1]))
return Vector2i(maxi(1, mini(preferred_size.x, max_size.x)), maxi(1, mini(preferred_size.y, max_size.y)))
func _usable_window_size() -> Vector2i:
var available: Vector2i = Vector2i(get_tree().root.size)
var usable: Rect2i = DisplayServer.screen_get_usable_rect(DisplayServer.window_get_current_screen())
@@ -156,5 +235,22 @@ func _usable_window_size() -> Vector2i:
return available
func _make_button_style(background: Color, border: Color, border_width: int, radius: int) -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = background
style.border_color = border
style.set_border_width_all(border_width)
style.set_corner_radius_all(radius)
style.content_margin_left = 12.0
style.content_margin_right = 12.0
style.content_margin_top = 8.0
style.content_margin_bottom = 8.0
return style
func _integer(path: String, fallback: int, minimum: int, maximum: int) -> int:
return fallback if _stage_theme == null else _stage_theme.integer(path, fallback, minimum, maximum)
func _color(path: String, fallback: Color) -> Color:
return fallback if _stage_theme == null else _stage_theme.color(path, fallback)
@@ -0,0 +1,130 @@
class_name StageSurfaceColorPickerView
extends VBoxContainer
## Inline color picker content for the scenery chooser.
const SURFACE_PREVIEW := preload("res://scripts/stage/scenery/stage_surface_preview.gd")
const COMPACT_LAYOUT_MAX_HEIGHT := 640.0
const COMPACT_PREVIEW_HEIGHT := 64.0
const COMPACT_PICKER_MIN_HEIGHT := 260.0
const COMPACT_SV_HEIGHT := 144
const REGULAR_SV_HEIGHT := 224
signal preview_color_changed(color: Color)
var _surface_kind: StringName = &"wall"
var _finish_id: StringName = &"finish_wall_paint"
var _stage_theme: SandboxTheme
var _color_picker: ColorPicker
var _surface_preview: StageSurfacePreview
var _preview_heading: Label
var _preferred_preview_size: Vector2 = Vector2(420.0, 140.0)
var _preferred_preview_height: float = 140.0
var _preferred_picker_min_size: Vector2 = Vector2(320.0, 340.0)
var _built: bool = false
var _suppress_color_signal: bool = false
func configure(surface_kind: StringName, finish: StringName, color: Color, stage_theme: SandboxTheme) -> void:
_surface_kind = surface_kind
_finish_id = finish
_stage_theme = stage_theme
if not _built:
_build()
set_color_no_signal(color)
theme = stage_theme.control_theme() if stage_theme != null else null
_apply_preview_heading_theme()
func get_color() -> Color:
return _color_picker.color if _color_picker != null else Color.WHITE
func set_color_no_signal(color: Color) -> void:
if _color_picker == null:
return
_suppress_color_signal = true
_color_picker.color = color
_suppress_color_signal = false
_update_preview(color)
func set_available_size(available_size: Vector2) -> void:
if not _built:
return
var compact: bool = available_size.y < COMPACT_LAYOUT_MAX_HEIGHT
var preview_size: Vector2 = _surface_preview.custom_minimum_size
preview_size.x = minf(_preferred_preview_size.x, maxf(1.0, available_size.x - 56.0))
preview_size.y = minf(_preferred_preview_height, COMPACT_PREVIEW_HEIGHT) if compact else _preferred_preview_height
_surface_preview.custom_minimum_size = preview_size
var picker_size: Vector2 = _color_picker.custom_minimum_size
picker_size.x = minf(_preferred_picker_min_size.x, maxf(1.0, available_size.x - 56.0))
picker_size.y = minf(_preferred_picker_min_size.y, COMPACT_PICKER_MIN_HEIGHT) if compact else _preferred_picker_min_size.y
_color_picker.custom_minimum_size = picker_size
_color_picker.add_theme_constant_override("sv_height", COMPACT_SV_HEIGHT if compact else REGULAR_SV_HEIGHT)
func focus_picker() -> void:
if _color_picker != null:
_color_picker.grab_focus()
func _build() -> void:
_built = true
add_theme_constant_override("separation", 8)
_surface_preview = SURFACE_PREVIEW.new() as StageSurfacePreview
_preview_heading = Label.new()
_preview_heading.name = "PreviewHeading"
_preview_heading.text = "Preview"
_preview_heading.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_preview_heading)
var preview_size := _preferred_preview_size
var raw_preview: Variant = _stage_theme.value("popups.dialog.surface_color_preview_size", [420, 140]) if _stage_theme != null else [420, 140]
if raw_preview is Array and (raw_preview as Array).size() == 2:
preview_size = Vector2(float((raw_preview as Array)[0]), float((raw_preview as Array)[1]))
_preferred_preview_size = preview_size
_preferred_preview_height = preview_size.y
_surface_preview.custom_minimum_size = preview_size
_surface_preview.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_surface_preview.set_rounded_frame(
float(_stage_theme.integer("popups.asset_selector.surface_corner_radius", 14, 4, 32)) if _stage_theme != null else 14.0,
_stage_theme.color("popups.style_defaults.bg_color", Color("#202633FA")) if _stage_theme != null else Color("#202633FA")
)
add_child(_surface_preview)
_color_picker = ColorPicker.new()
_color_picker.name = "ColorPickerControl"
var picker_size := _preferred_picker_min_size
var raw_picker_size: Variant = _stage_theme.value("popups.dialog.surface_color_picker_min_size", [320, 340]) if _stage_theme != null else [320, 340]
if raw_picker_size is Array and (raw_picker_size as Array).size() == 2:
picker_size = Vector2(float((raw_picker_size as Array)[0]), float((raw_picker_size as Array)[1]))
_preferred_picker_min_size = picker_size
_color_picker.custom_minimum_size = picker_size
_color_picker.presets_visible = false
_color_picker.sampler_visible = false
_color_picker.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_color_picker.focus_mode = Control.FOCUS_ALL
_color_picker.color_changed.connect(_on_picker_color_changed)
add_child(_color_picker)
_apply_preview_heading_theme()
func _apply_preview_heading_theme() -> void:
if _preview_heading == null:
return
if _stage_theme == null:
return
_preview_heading.add_theme_font_size_override("font_size", _stage_theme.integer("stage_panels.type.section_size", 20, 12, 40))
_preview_heading.add_theme_color_override("font_color", _stage_theme.color("stage_panels.type.text_color", Color("#F5F7FA")))
var font: Font = _stage_theme.font()
if font != null:
_preview_heading.add_theme_font_override("font", font)
func _on_picker_color_changed(color: Color) -> void:
_update_preview(color)
if not _suppress_color_signal:
preview_color_changed.emit(color)
func _update_preview(color: Color) -> void:
if _surface_preview != null:
_surface_preview.configure(_surface_kind, _finish_id, color)
@@ -0,0 +1 @@
uid://dbvatqk4a287y
File diff suppressed because it is too large Load Diff
+13 -3
View File
@@ -8,6 +8,15 @@ var _group: ButtonGroup = ButtonGroup.new()
var _buttons: Array[Button] = []
var _stage_theme: SandboxTheme
var _colors: Array[Color] = []
var _swatch_size_override: float = -1.0
var _swatch_gap_override: int = -1
func set_swatch_layout_override(size: float, gap: int) -> void:
_swatch_size_override = size
_swatch_gap_override = gap
if is_inside_tree() and not _colors.is_empty():
configure(_colors, _selected, _stage_theme)
func configure(colors: Array[Color], selected: Color, theme: Variant = null) -> void:
@@ -25,8 +34,9 @@ func configure(colors: Array[Color], selected: Color, theme: Variant = null) ->
for child: Node in get_children():
remove_child(child)
child.queue_free()
add_theme_constant_override("h_separation", _integer("stage_surface_colors.swatch_gap", 8))
add_theme_constant_override("v_separation", _integer("stage_surface_colors.swatch_gap", 8))
var gap: int = _swatch_gap_override if _swatch_gap_override >= 0 else _integer("stage_surface_colors.swatch_gap", 8)
add_theme_constant_override("h_separation", gap)
add_theme_constant_override("v_separation", gap)
var seen: Dictionary = {}
for color: Color in _colors:
var color_key: String = color.to_html(true).to_upper()
@@ -34,7 +44,7 @@ func configure(colors: Array[Color], selected: Color, theme: Variant = null) ->
continue
seen[color_key] = true
var button := Button.new()
var swatch_size: float = float(_integer("stage_surface_colors.swatch_size", 48))
var swatch_size: float = _swatch_size_override if _swatch_size_override > 0.0 else float(_integer("stage_surface_colors.swatch_size", 48))
button.custom_minimum_size = Vector2(swatch_size, swatch_size)
button.toggle_mode = true
button.button_group = _group