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:
@@ -166,8 +166,8 @@ func _build_shell() -> void:
|
||||
|
||||
|
||||
func _fit_to_content() -> void:
|
||||
_fit_pending = false
|
||||
if not is_inside_tree() or _body == null:
|
||||
_fit_pending = false
|
||||
return
|
||||
var rect := _safe_rect
|
||||
if rect.size.x <= 0.0 or rect.size.y <= 0.0:
|
||||
@@ -179,7 +179,16 @@ func _fit_to_content() -> void:
|
||||
size.x = card_width
|
||||
await get_tree().process_frame
|
||||
if not is_inside_tree():
|
||||
_fit_pending = false
|
||||
return
|
||||
# A resize can arrive while the first layout pass is waiting for the body
|
||||
# minimum size. Re-read the safe rectangle so this pass uses the new bounds.
|
||||
rect = _safe_rect
|
||||
if rect.size.x <= 0.0 or rect.size.y <= 0.0:
|
||||
rect = get_viewport_rect()
|
||||
card_width = minf(preferred_width, maxf(1.0, rect.size.x - inset * 2.0))
|
||||
custom_minimum_size.x = card_width
|
||||
size.x = card_width
|
||||
var padding := float(_integer("stage_panels.shell.padding", 12, 4, 28))
|
||||
var gap := float(_integer("stage_panels.shell.header_gap", 8, 0, 32))
|
||||
var header_height := maxf(44.0, _header.get_combined_minimum_size().y)
|
||||
@@ -189,10 +198,13 @@ func _fit_to_content() -> void:
|
||||
var natural_height := fixed_height + body_height
|
||||
var card_height := minf(natural_height, available_height)
|
||||
var scroll_height := maxf(0.0, card_height - fixed_height)
|
||||
position = Vector2(rect.position.x + rect.size.x - inset - card_width, rect.position.y + inset)
|
||||
size = Vector2(card_width, card_height)
|
||||
# Update the scroll minimum before shrinking the PanelContainer. Otherwise its
|
||||
# previous full-content minimum can clamp the new height until another layout.
|
||||
_scroll.custom_minimum_size.y = scroll_height
|
||||
_scroll.update_minimum_size()
|
||||
position = Vector2(rect.position.x + rect.size.x - inset - card_width, rect.position.y + inset)
|
||||
size = Vector2(card_width, card_height)
|
||||
_fit_pending = false
|
||||
|
||||
|
||||
func _style_action_button(button: Button, prominent: bool) -> void:
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
class_name StageSurfaceColorControls
|
||||
extends VBoxContainer
|
||||
## Shared wrapping Color history and preset controls for scenery surfaces.
|
||||
|
||||
signal color_selected(color: Color)
|
||||
signal custom_color_requested
|
||||
|
||||
const COLOR_SWATCH_SELECTOR := preload("res://scripts/shared/ui/color_swatch_selector.gd")
|
||||
|
||||
var _stage_theme: SandboxTheme
|
||||
var _selected: Color = Color.WHITE
|
||||
var _recents: Array[Color] = []
|
||||
var _presets: Array[Color] = []
|
||||
var _built: bool = false
|
||||
var _current_chip: Button
|
||||
var _current_value: Label
|
||||
var _recent_empty_label: Label
|
||||
var _recent_swatches: ColorSwatchSelector
|
||||
var _preset_swatches: ColorSwatchSelector
|
||||
var _more_button: Button
|
||||
|
||||
|
||||
func configure(selected: Color, recents: Array[Color], presets: Array[Color], stage_theme: SandboxTheme) -> void:
|
||||
_stage_theme = stage_theme
|
||||
_selected = selected
|
||||
_recents = recents.duplicate()
|
||||
_presets = presets.duplicate()
|
||||
if not _built:
|
||||
_build()
|
||||
_refresh_visuals()
|
||||
|
||||
|
||||
func set_selected_no_signal(color: Color) -> void:
|
||||
_selected = color
|
||||
_refresh_current_color()
|
||||
_recent_swatches.set_selected(color)
|
||||
_preset_swatches.set_selected(color)
|
||||
|
||||
|
||||
func set_recent_colors(colors: Array[Color]) -> void:
|
||||
if _recents == colors:
|
||||
return
|
||||
_recents = colors.duplicate()
|
||||
_recent_empty_label.visible = _recents.is_empty()
|
||||
_recent_swatches.visible = not _recents.is_empty()
|
||||
_recent_swatches.configure(_recents, _selected, _stage_theme)
|
||||
|
||||
|
||||
func focus_custom_button() -> void:
|
||||
if _more_button != null:
|
||||
_more_button.grab_focus()
|
||||
|
||||
|
||||
func _build() -> void:
|
||||
_built = true
|
||||
add_theme_constant_override("separation", _integer("stage_surface_colors.section_gap", 8, 0, 28))
|
||||
var current_heading: Label = _make_heading("Color")
|
||||
add_child(current_heading)
|
||||
var current_row := HBoxContainer.new()
|
||||
current_row.add_theme_constant_override("separation", _integer("stage_surface_colors.section_gap", 8, 0, 28))
|
||||
add_child(current_row)
|
||||
_current_chip = Button.new()
|
||||
var swatch_size: float = float(_integer("stage_surface_colors.swatch_size", 48, 44, 96))
|
||||
_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_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"))
|
||||
_recent_empty_label = Label.new()
|
||||
_recent_empty_label.text = "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)
|
||||
_recent_swatches = COLOR_SWATCH_SELECTOR.new() as ColorSwatchSelector
|
||||
_recent_swatches.color_selected.connect(_on_swatch_selected)
|
||||
add_child(_recent_swatches)
|
||||
add_child(_make_heading("Colors"))
|
||||
_preset_swatches = COLOR_SWATCH_SELECTOR.new() as ColorSwatchSelector
|
||||
_preset_swatches.color_selected.connect(_on_swatch_selected)
|
||||
add_child(_preset_swatches)
|
||||
_more_button = Button.new()
|
||||
_more_button.text = "More colors…"
|
||||
_more_button.custom_minimum_size.y = float(_integer("stage_panels.action.min_target_size", 48, 44, 96))
|
||||
_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())
|
||||
|
||||
|
||||
func _refresh_visuals() -> void:
|
||||
if not _built:
|
||||
return
|
||||
_refresh_current_color()
|
||||
_recent_empty_label.visible = _recents.is_empty()
|
||||
_recent_swatches.visible = not _recents.is_empty()
|
||||
_recent_swatches.configure(_recents, _selected, _stage_theme)
|
||||
_preset_swatches.configure(_presets, _selected, _stage_theme)
|
||||
|
||||
|
||||
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.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())
|
||||
|
||||
|
||||
func _on_swatch_selected(color: Color) -> void:
|
||||
_selected = color
|
||||
_refresh_current_color()
|
||||
_recent_swatches.set_selected(color)
|
||||
_preset_swatches.set_selected(color)
|
||||
color_selected.emit(color)
|
||||
|
||||
|
||||
func _make_heading(text_value: String) -> Label:
|
||||
var heading := Label.new()
|
||||
heading.text = text_value
|
||||
heading.add_theme_font_size_override("font_size", _integer("stage_panels.type.section_size", 20, 12, 40))
|
||||
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())
|
||||
return heading
|
||||
|
||||
|
||||
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 @@
|
||||
uid://d37coqfbju18u
|
||||
@@ -0,0 +1,160 @@
|
||||
class_name StageSurfaceColorDialog
|
||||
extends Window
|
||||
## Bounded custom-color dialog with a live Stage surface preview.
|
||||
|
||||
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")
|
||||
|
||||
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 _built: bool = false
|
||||
|
||||
|
||||
func configure(surface_kind: StringName, finish: StringName, color: Color, stage_theme: SandboxTheme) -> void:
|
||||
_surface_kind = surface_kind
|
||||
_finish_id = finish
|
||||
_initial_color = color
|
||||
_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
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
func _unhandled_key_input(event: InputEvent) -> void:
|
||||
if visible and event is InputEventKey and (event as InputEventKey).pressed and not (event as InputEventKey).echo and (event as InputEventKey).keycode == KEY_ESCAPE:
|
||||
_cancel()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _build() -> void:
|
||||
_built = true
|
||||
var margin := MarginContainer.new()
|
||||
margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
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")))
|
||||
|
||||
|
||||
func _on_picker_color_changed(color: Color) -> void:
|
||||
_update_preview(color)
|
||||
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
|
||||
hide()
|
||||
color_confirmed.emit(confirmed_color)
|
||||
|
||||
|
||||
func _cancel() -> void:
|
||||
_update_preview(_initial_color)
|
||||
hide()
|
||||
cancelled.emit()
|
||||
|
||||
|
||||
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())
|
||||
if usable.size.x > 0 and usable.size.y > 0:
|
||||
available.x = mini(available.x, usable.size.x)
|
||||
available.y = mini(available.y, usable.size.y)
|
||||
available.x = maxi(1, available.x - 32)
|
||||
available.y = maxi(1, available.y - 32)
|
||||
return available
|
||||
|
||||
|
||||
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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://c56y4dpi7w2pu
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user