Add Stage Surface Color Dialog and Background Ground Functionality

- Implemented StageSurfaceColorDialog for custom color selection with live preview.
- Created StageBackgroundGround class for non-walkable scenery behind the set.
- Introduced StageRectSurface for shared rectangular geometry and tint preview.
- Added StageSurfacePreview for rendering surface previews.
- Developed comprehensive tests for surface color selection, rendering, and serialization.
This commit is contained in:
2026-10-17 21:00:03 -04:00
parent a0510e6405
commit 26dd9c64e1
39 changed files with 3118 additions and 142 deletions
+1 -1
View File
@@ -50,7 +50,7 @@ func configure(payload: Dictionary, icon_texture: Texture2D = null, theme: Varia
if not description.is_empty():
_description = Label.new()
_description.text = description
_description.custom_minimum_size = Vector2(0.0, 34.0)
_description.custom_minimum_size = Vector2(0.0, float(_payload.get("description_min_height", 34.0)))
_description.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_description.vertical_alignment = VERTICAL_ALIGNMENT_TOP
_description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
+354 -10
View File
@@ -7,16 +7,27 @@ signal browse_requested()
signal refresh_requested()
signal make_artwork_requested()
signal edit_scenery_requested()
signal surface_color_confirmed(color: Color)
const COLUMNS := 4
const ROWS := 3
const PAGE_SIZE := COLUMNS * ROWS
const GRID_H_SEPARATION: float = 8.0
const GRID_V_SEPARATION: float = 8.0
const POPUP_SIDE_PADDING: float = 40.0
const POPUP_VERTICAL_PADDING: float = 32.0
const POPUP_SAFE_SCREEN_MARGIN: float = 24.0
const CELL_MIN_SIZE := Vector2(190.0, 220.0)
const THUMB_SIZE := Vector2(156.0, 156.0)
const ASSET_CARD := preload("res://scripts/shared/ui/asset_card.gd")
const BACKGROUND_CARD_PREVIEW := preload("res://scripts/stage/scenery/stage_background_card_preview.gd")
const STARTER_CARD_PREVIEW := preload("res://scripts/stage/scenery/stage_starter_card_preview.gd")
const SURFACE_RENDERER := preload("res://scripts/stage/scenery/stage_surface_renderer.gd")
const SURFACE_PREVIEW := preload("res://scripts/stage/scenery/stage_surface_preview.gd")
const SURFACE_COLOR_CONTROLS := preload("res://scripts/shared/stage_ui/stage_surface_color_controls.gd")
const SURFACE_COLOR_DIALOG := preload("res://scripts/shared/stage_ui/stage_surface_color_dialog.gd")
const STAGE_PANEL_CHOICE_GROUP := preload("res://scripts/shared/stage_ui/stage_panel_choice_group.gd")
var kind: String = ""
@@ -32,9 +43,24 @@ var _gallery_tab: String = "backgrounds"
var _tag_id: String = "all"
var _current_sky_id: String = ""
var _sky_card_group: ButtonGroup
var _active_columns: int = COLUMNS
var _page_capacity: int = PAGE_SIZE
var _effective_cell_size: Vector2 = CELL_MIN_SIZE
var _surface_color_history: Array[Color] = []
var _setup_view: VBoxContainer
var _setup_kind: StringName = &""
var _setup_finish: StringName = &""
var _setup_color: Color = Color.WHITE
var _setup_original_color: Color = Color.WHITE
var _setup_color_chosen: bool = false
var _setup_preview: StageSurfacePreview
var _setup_color_controls: StageSurfaceColorControls
var _setup_color_dialog: StageSurfaceColorDialog
@onready var _title_label: Label = %TitleLabel
@onready var _grid: GridContainer = %GridContainer
@onready var _content_stack: VBoxContainer = %ContentStack
@onready var _grid_scroll: ScrollContainer = %GridScroll
@onready var _empty_label: Label = %EmptyLabel
@onready var _page_label: Label = %PageLabel
@onready var _prev_button: Button = %PrevButton
@@ -48,10 +74,13 @@ var _sky_card_group: ButtonGroup
@onready var _tag_filter: OptionButton = %TagFilter
@onready var _make_artwork_button: Button = %MakeArtworkButton
@onready var _edit_scenery_button: Button = %EditSceneryButton
@onready var _title_bar: Control = $MarginContainer/VBoxContainer/TitleBar
@onready var _footer: Control = $MarginContainer/VBoxContainer/Footer
func _ready() -> void:
exclusive = true
_build_surface_setup_view()
if _tag_filter.item_count == 0:
for label: String in ["All artwork", "Nature", "Buildings", "Walls", "Roads", "Objects"]:
_tag_filter.add_item(label)
@@ -70,11 +99,15 @@ func _ready() -> void:
func _on_root_size_changed() -> void:
if visible:
_update_popup_layout()
_rebuild()
popup_centered()
func open(p_kind: String, entries: Array[Dictionary], current_sky_id: String = "") -> void:
kind = p_kind
_setup_kind = &""
_setup_view.visible = false
_current_sky_id = current_sky_id if kind == "scenery" else ""
_title_label.text = "Choose Your Stickman" if kind == "stickman" else ("Choose Scenery" if kind == "scenery" else ("Choose a starter scene" if kind == "starters" else "Choose a Prop"))
_tabs.visible = kind == "scenery"
@@ -100,9 +133,16 @@ func set_entries(entries: Array[Dictionary]) -> void:
_thumbnails.clear()
_cell_texrects.clear()
_page = 0
_update_popup_layout()
_rebuild()
func set_surface_color_history(colors: Array[Color]) -> void:
_surface_color_history = colors.duplicate()
if _setup_color_controls != null and is_instance_valid(_setup_color_controls):
_setup_color_controls.set_recent_colors(_surface_color_history)
func set_thumbnail(entry: Dictionary, tex: Texture2D) -> void:
var key := _entry_key(entry)
_thumbnails[key] = tex
@@ -144,6 +184,221 @@ func apply_theme(theme: Variant) -> void:
var thumbnail_size: Variant = theme.value("popups.asset_selector.thumbnail_size", THUMB_SIZE)
if thumbnail_size is Array and (thumbnail_size as Array).size() == 2:
_thumbnail_size = Vector2(float(thumbnail_size[0]), float(thumbnail_size[1]))
var wall_choices: StagePanelChoiceGroup = get_meta("wall_look_group", null) as StagePanelChoiceGroup
var ground_choices: StagePanelChoiceGroup = get_meta("ground_look_group", null) as StagePanelChoiceGroup
if wall_choices != null:
wall_choices.apply_theme(theme as SandboxTheme)
if ground_choices != null:
ground_choices.apply_theme(theme as SandboxTheme)
if _setup_color_controls != null:
_setup_color_controls.configure(_setup_color, _surface_color_history, _preset_surface_colors(), theme as SandboxTheme)
func _build_surface_setup_view() -> void:
_setup_view = VBoxContainer.new()
_setup_view.name = "SurfaceSetupView"
_setup_view.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_setup_view.add_theme_constant_override("separation", _theme_integer("popups.asset_selector.setup_gap", 10, 0, 32))
_setup_view.visible = false
_content_stack.add_child(_setup_view)
var heading_row := HBoxContainer.new()
heading_row.add_theme_constant_override("separation", 8)
_setup_view.add_child(heading_row)
var back_button := Button.new()
back_button.text = "‹ Back"
back_button.custom_minimum_size = Vector2(float(_theme_integer("stage_panels.action.close_width", 92, 72, 180)), float(_theme_integer("stage_panels.action.min_target_size", 48, 44, 96)))
back_button.focus_mode = Control.FOCUS_ALL
back_button.pressed.connect(_back_from_surface_setup)
heading_row.add_child(back_button)
var heading := Label.new()
heading.name = "SetupHeading"
heading.size_flags_horizontal = Control.SIZE_EXPAND_FILL
heading.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
heading.add_theme_font_size_override("font_size", _theme_integer("stage_panels.type.title_size", 24, 12, 48))
heading_row.add_child(heading)
var look_heading := Label.new()
look_heading.text = "Look"
look_heading.add_theme_font_size_override("font_size", _theme_integer("stage_panels.type.section_size", 20, 12, 40))
_setup_view.add_child(look_heading)
var wall_choices: StagePanelChoiceGroup = STAGE_PANEL_CHOICE_GROUP.new() as StagePanelChoiceGroup
wall_choices.name = "WallLookChoices"
wall_choices.add_choice(&"finish_wall_paint", "Paint", _make_look_cue(&"wall", &"finish_wall_paint"))
wall_choices.add_choice(&"finish_wall_brick", "Brick", _make_look_cue(&"wall", &"finish_wall_brick"))
wall_choices.add_choice(&"finish_wall_wood", "Wood", _make_look_cue(&"wall", &"finish_wall_wood"))
wall_choices.choice_selected.connect(_on_setup_look_choice)
_setup_view.add_child(wall_choices)
var ground_choices: StagePanelChoiceGroup = STAGE_PANEL_CHOICE_GROUP.new() as StagePanelChoiceGroup
ground_choices.name = "BackgroundGroundLookChoices"
ground_choices.add_choice(&"finish_background_grass", "Grass", _make_look_cue(&"background_ground", &"finish_background_grass"))
ground_choices.add_choice(&"finish_background_concrete", "Concrete", _make_look_cue(&"background_ground", &"finish_background_concrete"))
ground_choices.add_choice(&"finish_background_carpet", "Carpet", _make_look_cue(&"background_ground", &"finish_background_carpet"))
ground_choices.choice_selected.connect(_on_setup_look_choice)
_setup_view.add_child(ground_choices)
set_meta("wall_look_group", wall_choices)
set_meta("ground_look_group", ground_choices)
var preview_frame := PanelContainer.new()
preview_frame.name = "SurfacePreviewFrame"
var preview_size: Vector2 = _theme_vector2("popups.asset_selector.surface_preview_size", Vector2(420.0, 132.0))
preview_frame.custom_minimum_size.y = preview_size.y + 10.0
preview_frame.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_setup_preview = SURFACE_PREVIEW.new() as StageSurfacePreview
_setup_preview.custom_minimum_size = preview_size
_setup_preview.size_flags_horizontal = Control.SIZE_EXPAND_FILL
preview_frame.add_child(_setup_preview)
_setup_view.add_child(preview_frame)
_setup_color_controls = SURFACE_COLOR_CONTROLS.new() as StageSurfaceColorControls
_setup_color_controls.name = "SurfaceColorControls"
_setup_color_controls.color_selected.connect(_on_setup_color_selected)
_setup_color_controls.custom_color_requested.connect(_open_setup_color_dialog)
_setup_view.add_child(_setup_color_controls)
var helper := Label.new()
helper.name = "SurfaceRoleHelper"
helper.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
helper.add_theme_color_override("font_color", _theme_color("global.muted_text_color", Color("#AAB2C0")))
_setup_view.add_child(helper)
var draw_button := Button.new()
draw_button.name = "DrawSurfaceButton"
draw_button.custom_minimum_size.y = float(_theme_integer("stage_panels.choice.minimum_height", 56, 44, 112))
draw_button.focus_mode = Control.FOCUS_ALL
draw_button.pressed.connect(_draw_selected_surface)
_setup_view.add_child(draw_button)
set_meta("setup_heading", heading)
set_meta("setup_helper", helper)
set_meta("setup_draw_button", draw_button)
_apply_font_to(back_button)
_apply_font_to(heading)
_apply_font_to(look_heading)
_apply_font_to(helper)
_apply_font_to(draw_button)
if _sandbox_theme != null:
draw_button.add_theme_stylebox_override("normal", _sandbox_theme.stylebox("stage_panels.action"))
draw_button.add_theme_stylebox_override("hover", _sandbox_theme.stylebox("stage_panels.action"))
draw_button.add_theme_color_override("font_color", _theme_color("stage_panels.type.text_color", Color("#F5F7FA")))
draw_button.add_theme_font_size_override("font_size", _theme_integer("stage_panels.type.body_size", 20, 14, 40))
for choice: StagePanelChoiceGroup in [wall_choices, ground_choices]:
choice.apply_theme(_sandbox_theme as SandboxTheme)
_apply_font_to(choice)
func _make_look_cue(kind: StringName, finish: StringName) -> Control:
var preview: StageSurfacePreview = SURFACE_PREVIEW.new() as StageSurfacePreview
preview.custom_minimum_size = _theme_vector2("popups.asset_selector.look_cue_size", Vector2(80.0, 42.0))
preview.configure(kind, finish, SURFACE_RENDERER.default_tint(finish))
return preview
func _open_surface_setup(kind: StringName) -> void:
_setup_kind = kind
_setup_color_chosen = false
_setup_finish = &"finish_wall_paint" if kind == &"wall" else &"finish_background_grass"
_setup_color = SURFACE_RENDERER.default_tint(_setup_finish)
_setup_original_color = _setup_color
var wall_choices: StagePanelChoiceGroup = get_meta("wall_look_group") as StagePanelChoiceGroup
var ground_choices: StagePanelChoiceGroup = get_meta("ground_look_group") as StagePanelChoiceGroup
wall_choices.visible = kind == &"wall"
ground_choices.visible = kind == &"background_ground"
wall_choices.select_no_signal(_setup_finish)
ground_choices.select_no_signal(_setup_finish)
(get_meta("setup_heading") as Label).text = "Wall panel" if kind == &"wall" else "Background ground"
(get_meta("setup_helper") as Label).text = "Looks like a wall · Stickmen can pass through." if kind == &"wall" else "Decorative only · Stickmen don’t walk here."
(get_meta("setup_draw_button") as Button).text = "Draw wall panel" if kind == &"wall" else "Draw background ground"
_setup_color_controls.visible = _supports_setup_color()
_setup_color_controls.configure(_setup_color, _surface_color_history, _preset_surface_colors(), _sandbox_theme as SandboxTheme)
_update_setup_preview()
_page = 0
_update_popup_layout()
_rebuild()
back_button_focus()
func back_button_focus() -> void:
var back_button: Button = _setup_view.get_child(0).get_child(0) as Button
if back_button != null:
back_button.grab_focus()
func _back_from_surface_setup() -> void:
_setup_kind = &""
_setup_view.visible = false
_update_popup_layout()
_rebuild()
_close_button.grab_focus()
func _on_setup_look_choice(id: StringName) -> void:
var supported: bool = SURFACE_RENDERER.is_wall_finish(id) if _setup_kind == &"wall" else SURFACE_RENDERER.is_background_finish(id)
if not supported:
return
_setup_finish = id
if not _setup_color_chosen:
_setup_color = SURFACE_RENDERER.default_tint(_setup_finish)
_setup_color_controls.visible = _supports_setup_color()
_setup_color_controls.set_selected_no_signal(_setup_color)
_update_setup_preview()
_update_popup_layout()
func _supports_setup_color() -> bool:
return _setup_kind == &"wall" or _setup_finish == &"finish_background_carpet"
func _on_setup_color_selected(color: Color) -> void:
_setup_color = Color(color.r, color.g, color.b, 1.0)
_setup_color_chosen = true
_update_setup_preview()
surface_color_confirmed.emit(_setup_color)
func _update_setup_preview() -> void:
if _setup_preview == null:
return
_setup_preview.configure(_setup_kind, _setup_finish, _setup_color)
_setup_color_controls.set_selected_no_signal(_setup_color)
func _preset_surface_colors() -> Array[Color]:
if _sandbox_theme == null:
return [Color.WHITE, Color.BLACK, Color("#4C9AFF"), Color("#29A36A"), Color("#FFD84D"), Color("#D94F70")]
return (_sandbox_theme as SandboxTheme).swatches()
func _open_setup_color_dialog() -> void:
if _setup_color_dialog == null or not is_instance_valid(_setup_color_dialog):
_setup_color_dialog = SURFACE_COLOR_DIALOG.new() as StageSurfaceColorDialog
add_child(_setup_color_dialog)
_setup_color_dialog.preview_color_changed.connect(_on_setup_color_preview)
_setup_color_dialog.color_confirmed.connect(_on_setup_color_confirmed)
_setup_color_dialog.cancelled.connect(_on_setup_color_cancelled)
_setup_original_color = _setup_color
_setup_color_dialog.configure(_setup_kind, _setup_finish, _setup_original_color, _sandbox_theme as SandboxTheme)
_setup_color_dialog.open_dialog()
func _on_setup_color_preview(color: Color) -> void:
_setup_color = Color(color.r, color.g, color.b, 1.0)
_update_setup_preview()
func _on_setup_color_confirmed(color: Color) -> void:
_setup_color = Color(color.r, color.g, color.b, 1.0)
_setup_color_chosen = true
_update_setup_preview()
surface_color_confirmed.emit(_setup_color)
_setup_color_controls.focus_custom_button()
func _on_setup_color_cancelled() -> void:
_setup_color = _setup_original_color
_setup_color_controls.set_selected_no_signal(_setup_color)
_update_setup_preview()
_setup_color_controls.focus_custom_button()
func _draw_selected_surface() -> void:
if _setup_kind.is_empty():
return
var background_kind: String = "wall" if _setup_kind == &"wall" else "background_ground"
item_selected.emit({"background_kind": background_kind, "finish_id": String(_setup_finish), "color": _setup_color})
var _cell_min_size: Vector2 = CELL_MIN_SIZE
var _thumbnail_size: Vector2 = THUMB_SIZE
@@ -176,10 +431,13 @@ func _on_tag_filter_selected(index: int) -> void:
var ids: Array[String] = ["all", "nature", "buildings", "walls", "roads", "objects"]
_tag_id = ids[clampi(index, 0, ids.size() - 1)]
_page = 0
_update_popup_layout()
_rebuild()
func _set_gallery_tab(tab: String) -> void:
_setup_kind = &""
_setup_view.visible = false
_gallery_tab = tab
_backgrounds_tab.set_pressed_no_signal(tab == "backgrounds")
_artwork_tab.set_pressed_no_signal(tab == "artwork")
@@ -190,6 +448,7 @@ func _set_gallery_tab(tab: String) -> void:
_refresh_button.visible = kind == "stickman" or kind == "prop" or (kind == "scenery" and tab == "artwork")
_empty_label.text = "No artwork in this group yet." if tab == "artwork" else "Choose a Sky look or wall finish."
_page = 0
_update_popup_layout()
_rebuild()
@@ -211,12 +470,14 @@ func _filtered_entries() -> Array[Dictionary]:
func _on_prev() -> void:
if _page > 0:
_page -= 1
_update_popup_layout()
_rebuild()
func _on_next() -> void:
if (_page + 1) * PAGE_SIZE < _filtered_entries().size():
if (_page + 1) * _page_capacity < _filtered_entries().size():
_page += 1
_update_popup_layout()
_rebuild()
@@ -224,7 +485,55 @@ func _page_count() -> int:
var entries: Array[Dictionary] = _filtered_entries()
if entries.is_empty():
return 1
return int(ceil(float(entries.size()) / float(PAGE_SIZE)))
return int(ceil(float(entries.size()) / float(_page_capacity)))
func _usable_screen_size() -> Vector2:
var root_window: Window = get_tree().root
var available: Vector2 = Vector2(root_window.size)
var screen_index: int = DisplayServer.window_get_current_screen()
var usable_rect: Rect2i = DisplayServer.screen_get_usable_rect(screen_index)
if usable_rect.size.x > 0 and usable_rect.size.y > 0:
available.x = minf(available.x, float(usable_rect.size.x))
available.y = minf(available.y, float(usable_rect.size.y))
return available
func _update_popup_layout() -> void:
var available: Vector2 = _usable_screen_size()
var max_width: float = maxf(1.0, minf(available.x * 0.94, available.x - POPUP_SAFE_SCREEN_MARGIN))
var max_height: float = maxf(1.0, minf(available.y * 0.90, available.y - POPUP_SAFE_SCREEN_MARGIN))
var preferred_width: float = CELL_MIN_SIZE.x * COLUMNS + GRID_H_SEPARATION * (COLUMNS - 1) + POPUP_SIDE_PADDING
var popup_width: float = minf(preferred_width, max_width)
var grid_width: float = maxf(1.0, popup_width - POPUP_SIDE_PADDING)
_active_columns = clampi(floori((grid_width + GRID_H_SEPARATION) / (CELL_MIN_SIZE.x + GRID_H_SEPARATION)), 1, COLUMNS)
_page_capacity = _active_columns * ROWS
_grid.columns = _active_columns
var card_width: float = minf(CELL_MIN_SIZE.x, (grid_width - GRID_H_SEPARATION * (_active_columns - 1)) / float(_active_columns))
var card_height_limit: float = 248.0 if kind == "starters" else CELL_MIN_SIZE.y
var card_height: float = minf(card_height_limit, card_width + (58.0 if kind == "starters" else 30.0))
_effective_cell_size = Vector2(maxf(96.0, card_width), maxf(180.0, card_height))
var filtered_count: int = _filtered_entries().size()
var page_count: int = maxi(1, int(ceil(float(filtered_count) / float(_page_capacity))))
_page = clampi(_page, 0, page_count - 1)
var page_start: int = _page * _page_capacity
var page_items: int = clampi(filtered_count - page_start, 0, _page_capacity)
var grid_rows: int = int(ceil(float(page_items) / float(_active_columns))) if page_items > 0 else 0
var body_height: float
if not _setup_kind.is_empty():
body_height = maxf(180.0, _setup_view.get_combined_minimum_size().y)
else:
body_height = maxf(120.0, float(grid_rows) * _effective_cell_size.y + float(maxi(0, grid_rows - 1)) * GRID_V_SEPARATION)
if page_items == 0:
body_height = maxf(body_height, _empty_label.get_combined_minimum_size().y)
var fixed_height: float = _title_bar.get_combined_minimum_size().y + _footer.get_combined_minimum_size().y + POPUP_VERTICAL_PADDING
var visible_sections: int = 3
if _tabs.visible:
fixed_height += _tabs.get_combined_minimum_size().y
visible_sections += 1
fixed_height += float(visible_sections - 1) * 8.0
var popup_height: float = minf(fixed_height + body_height, max_height)
size = Vector2i(maxi(1, roundi(popup_width)), maxi(1, roundi(popup_height)))
func _rebuild() -> void:
@@ -233,19 +542,30 @@ func _rebuild() -> void:
c.queue_free()
_cell_texrects.clear()
var filtered: Array[Dictionary] = _filtered_entries()
_empty_label.visible = filtered.is_empty()
_grid.visible = not filtered.is_empty()
var in_setup: bool = not _setup_kind.is_empty()
_setup_view.visible = in_setup
_empty_label.visible = filtered.is_empty() and not in_setup
_grid.visible = not filtered.is_empty() and not in_setup
_grid_scroll.visible = not filtered.is_empty() or in_setup
var total := _page_count()
var single := total <= 1
_prev_button.visible = not single
_next_button.visible = not single
_page_label.visible = not single
_prev_button.disabled = _page <= 0
_next_button.disabled = (_page + 1) * PAGE_SIZE >= filtered.size()
_next_button.disabled = (_page + 1) * _page_capacity >= filtered.size()
_page_label.text = "Page %d/%d" % [_page + 1, total]
if in_setup:
_prev_button.visible = false
_next_button.visible = false
_page_label.visible = false
_browse_button.visible = false
_refresh_button.visible = false
_make_artwork_button.visible = false
return
if filtered.is_empty():
return
var bounds := page_bounds(filtered.size(), _page)
var bounds := page_bounds(filtered.size(), _page, _page_capacity)
for i: int in range(int(bounds["start"]), int(bounds["end"])):
_grid.add_child(_build_cell(filtered[i]))
@@ -257,12 +577,16 @@ func _build_cell(entry: Dictionary) -> Control:
# opening a selector never implies that one asset is pre-selected.
var categories: Array = entry.get("tags", []) as Array
var category: String = ", ".join(PackedStringArray(categories)) if not categories.is_empty() else String(entry.get("category", entry.get("material_label", "")))
var card_size: Vector2 = _cell_min_size
var thumb_size: Vector2 = _thumbnail_size
var card_size: Vector2 = _effective_cell_size
var is_background_ground_setup: bool = String(entry.get("background_kind", "")) == "background_ground_setup"
var thumb_height_budget: float = card_size.y - (74.0 if is_background_ground_setup else 64.0)
var thumb_edge: float = minf(_thumbnail_size.x, minf(card_size.x - 20.0, thumb_height_budget))
var thumb_size := Vector2.ONE * maxf(72.0, thumb_edge)
if kind == "starters":
card_size.y = maxf(card_size.y, 248.0)
thumb_size = Vector2(128.0, 128.0)
thumb_size = Vector2.ONE * minf(128.0, maxf(72.0, thumb_edge))
var card_payload: Dictionary = {"placement_id": _entry_key(entry), "label": String(entry.get("name", entry.get("id", ""))), "icon": String(entry.get("icon", "")), "category": category, "description": String(entry.get("description", "")), "thumbnail_size": thumb_size}
if is_background_ground_setup:
card_payload["description_min_height"] = 42.0
if entry.has("background_kind"):
card_payload["background_kind"] = entry["background_kind"]
card_payload["finish_id"] = entry.get("finish_id", "")
@@ -301,7 +625,27 @@ func _theme_color(path: String, fallback: Color) -> Color:
return fallback
func _theme_integer(path: String, fallback: int, minimum: int, maximum: int) -> int:
return fallback if _sandbox_theme == null else _sandbox_theme.integer(path, fallback, minimum, maximum)
func _theme_vector2(path: String, fallback: Vector2) -> Vector2:
if _sandbox_theme == null:
return fallback
var raw: Variant = _sandbox_theme.value(path, [fallback.x, fallback.y])
if not raw is Array or (raw as Array).size() != 2:
return fallback
return Vector2(float((raw as Array)[0]), float((raw as Array)[1]))
func _on_cell_pressed(entry: Dictionary) -> void:
var background_kind: String = String(entry.get("background_kind", ""))
if background_kind == "wall_setup":
_open_surface_setup(&"wall")
return
if background_kind == "background_ground_setup":
_open_surface_setup(&"background_ground")
return
item_selected.emit(entry)
+114 -9
View File
@@ -1,17 +1,122 @@
class_name ColorSwatchSelector
extends HBoxContainer
extends FlowContainer
signal color_selected(color: Color)
var _selected: Color = Color.WHITE
var _group: ButtonGroup = ButtonGroup.new()
var _buttons: Array[Button] = []
var _stage_theme: SandboxTheme
var _colors: Array[Color] = []
func configure(colors: Array[Color], selected: Color, theme: Variant = null) -> void:
for child: Node in get_children(): child.queue_free()
_stage_theme = theme as SandboxTheme if theme is SandboxTheme else null
_selected = selected
for color: Color in colors:
var button := Button.new(); button.custom_minimum_size = Vector2(44,44); button.toggle_mode = true; button.button_group = _group; button.tooltip_text = color.to_html(true)
var box := StyleBoxFlat.new(); box.bg_color = color; box.set_corner_radius_all(22); button.add_theme_stylebox_override("normal", box)
button.pressed.connect(func() -> void: set_selected(color, true)); add_child(button)
button.set_pressed_no_signal(color.is_equal_approx(selected))
_colors = colors.duplicate()
var focus_key: String = ""
var focus_owner: Control = get_viewport().gui_get_focus_owner() if is_inside_tree() else null
if focus_owner != null and focus_owner.get_parent() == self:
var focused_color: Color = focus_owner.get_meta("surface_color", Color.WHITE)
focus_key = "#" + focused_color.to_html(false).to_upper()
_group = ButtonGroup.new()
_group.allow_unpress = true
_buttons.clear()
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 seen: Dictionary = {}
for color: Color in _colors:
var color_key: String = color.to_html(true).to_upper()
if seen.has(color_key):
continue
seen[color_key] = true
var button := Button.new()
var swatch_size: float = 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
button.focus_mode = Control.FOCUS_ALL
button.tooltip_text = "Color #%s" % color.to_html(false).to_upper()
button.set_meta("surface_color", color)
button.pressed.connect(_on_pressed.bind(button))
add_child(button)
_buttons.append(button)
_apply_button_style(button, color)
_sync_pressed_state()
if not focus_key.is_empty():
call_deferred("_restore_color_focus", focus_key)
func set_selected(color: Color, emit_signal: bool = false) -> void:
_selected = color
if emit_signal: color_selected.emit(color)
func get_selected() -> Color: return _selected
_sync_pressed_state()
if emit_signal:
color_selected.emit(color)
func get_selected() -> Color:
return _selected
func _restore_color_focus(color_key: String) -> void:
for button: Button in _buttons:
var button_color: Color = button.get_meta("surface_color", Color.WHITE)
if "#" + button_color.to_html(false).to_upper() == color_key:
button.grab_focus()
return
func _on_pressed(button: Button) -> void:
var color: Color = button.get_meta("surface_color", Color.WHITE)
set_selected(color, true)
func _sync_pressed_state() -> void:
for button: Button in _buttons:
var color: Color = button.get_meta("surface_color", Color.WHITE)
button.set_pressed_no_signal(color.to_html(true) == _selected.to_html(true))
button.text = "✓" if color.to_html(true) == _selected.to_html(true) else ""
var label_color: Color = Color("#111821") if color.get_luminance() > 0.58 else Color("#FFFFFF")
button.add_theme_color_override("font_color", label_color)
button.add_theme_color_override("font_hover_color", label_color)
button.add_theme_color_override("font_pressed_color", label_color)
func _apply_button_style(button: Button, color: Color) -> void:
var border: Color = _color("stage_surface_colors.swatch_border", Color("#FFFFFFB8"))
var accent: Color = _color("mode_colors.edit", Color("#22C6FF"))
var corner: int = _integer("stage_surface_colors.swatch_corner_radius", 8)
var normal := StyleBoxFlat.new()
normal.bg_color = color
normal.border_color = border
normal.set_border_width_all(_integer("stage_surface_colors.swatch_border_width", 2))
normal.set_corner_radius_all(corner)
var selected := normal.duplicate() as StyleBoxFlat
selected.border_color = accent
selected.set_border_width_all(_integer("stage_surface_colors.selected_border_width", 4))
var hover := normal.duplicate() as StyleBoxFlat
hover.border_color = Color.WHITE
hover.set_border_width_all(_integer("stage_surface_colors.focus_border_width", 3))
button.add_theme_stylebox_override("normal", normal)
button.add_theme_stylebox_override("hover", hover)
button.add_theme_stylebox_override("pressed", selected)
button.add_theme_stylebox_override("hover_pressed", selected.duplicate())
var focus := normal.duplicate() as StyleBoxFlat
focus.draw_center = false
focus.border_color = _color("stage_surface_colors.swatch_focus_border", Color.WHITE)
focus.set_border_width_all(_integer("stage_surface_colors.focus_border_width", 3))
button.add_theme_stylebox_override("focus", focus)
if _stage_theme != null and _stage_theme.font() != null:
button.add_theme_font_override("font", _stage_theme.font())
button.add_theme_font_size_override("font_size", _integer("stage_surface_colors.swatch_check_size", 24))
func _integer(path: String, fallback: int) -> int:
return fallback if _stage_theme == null else _stage_theme.integer(path, fallback, 0, 64)
func _color(path: String, fallback: Color) -> Color:
return fallback if _stage_theme == null else _stage_theme.color(path, fallback)