Add visual harmonization tests for editor UI components and ground finish chooser
- Implemented `test_editor_visual_harmonization.gd` to validate the Stickman editor and prop editor UI layouts, ensuring proper functionality and visual consistency. - Created `test_editor_visual_theme.gd` to verify the loading and application of visual themes in the editor, including semantic overrides and default values. - Introduced `test_phase19_ground_finish_chooser.gd` to test the ground finish chooser's functionality, including layout responsiveness, selection behavior, and interaction with the stage. - Added new scripts for ground finish preview and chooser UI components to enhance the editor's visual representation of ground finishes.
This commit is contained in:
@@ -0,0 +1,284 @@
|
||||
class_name EditorVisualTheme
|
||||
extends RefCounted
|
||||
## Validated semantic chrome tokens shared by the standalone editors.
|
||||
## Canvas artwork and editor-specific drawing values stay with their owners.
|
||||
|
||||
const SCHEMA_VERSION: String = "1.0"
|
||||
const DEFAULT_PATH: String = "res://editor_visual_theme.json"
|
||||
|
||||
const DEFAULT_CONFIG: Dictionary = {
|
||||
"version": SCHEMA_VERSION,
|
||||
"chrome": {
|
||||
"colors": {
|
||||
"background": "#171B24F2",
|
||||
"surface": "#252B38FA",
|
||||
"surface_hover": "#30394A",
|
||||
"text": "#F5F7FA",
|
||||
"text_muted": "#AAB2C0",
|
||||
"edit": "#22C6FF",
|
||||
"preview": "#22C6FF66",
|
||||
"focus": "#8BD9FF",
|
||||
"action_primary": "#087DBA",
|
||||
"destructive": "#E66B75",
|
||||
"border": "#FFFFFF33",
|
||||
"disabled": "#77808F",
|
||||
"saved": "#8FD6A4",
|
||||
"dirty": "#FFD166",
|
||||
"error": "#FF8A8A",
|
||||
},
|
||||
"fonts": {
|
||||
"body": 18,
|
||||
"helper": 16,
|
||||
"section": 22,
|
||||
"title": 26,
|
||||
"menu": 20,
|
||||
"status": 16,
|
||||
"mode": 24,
|
||||
"prop_name": 20,
|
||||
"context_menu": 20,
|
||||
},
|
||||
"spacing": {"small": 8, "medium": 12, "large": 16},
|
||||
"radii": {"control": 8, "panel": 10},
|
||||
"borders": {"normal": 1, "focus": 2},
|
||||
"targets": {"button_min_height": 44, "row_min_height": 44, "mode_button_min_height": 56},
|
||||
},
|
||||
"editors": {"prop": {}, "stickman": {}},
|
||||
"stickman_canvas": {
|
||||
"grid_color": "#FFFFFF26",
|
||||
"selection_core": "#FFFFFF",
|
||||
"selection_halo": "#000000D9",
|
||||
},
|
||||
}
|
||||
|
||||
const COLOR_ROLE_SECTIONS: Dictionary = {
|
||||
"background": "colors",
|
||||
"surface": "colors",
|
||||
"surface_hover": "colors",
|
||||
"text": "colors",
|
||||
"text_muted": "colors",
|
||||
"edit": "colors",
|
||||
"preview": "colors",
|
||||
"focus": "colors",
|
||||
"action_primary": "colors",
|
||||
"destructive": "colors",
|
||||
"border": "colors",
|
||||
"disabled": "colors",
|
||||
"saved": "colors",
|
||||
"dirty": "colors",
|
||||
"error": "colors",
|
||||
}
|
||||
const SIZE_ROLE_SECTIONS: Dictionary = {
|
||||
"body": "fonts",
|
||||
"helper": "fonts",
|
||||
"section": "fonts",
|
||||
"title": "fonts",
|
||||
"menu": "fonts",
|
||||
"status": "fonts",
|
||||
"mode": "fonts",
|
||||
"prop_name": "fonts",
|
||||
"context_menu": "fonts",
|
||||
"small": "spacing",
|
||||
"medium": "spacing",
|
||||
"large": "spacing",
|
||||
"control": "radii",
|
||||
"panel": "radii",
|
||||
"normal_border": "borders",
|
||||
"focus_border": "borders",
|
||||
"button_min_height": "targets",
|
||||
"row_min_height": "targets",
|
||||
"mode_button_min_height": "targets",
|
||||
}
|
||||
const CONFIG_SIZE_KEYS: Dictionary = {
|
||||
"normal_border": "normal",
|
||||
"focus_border": "focus",
|
||||
}
|
||||
|
||||
var _config: Dictionary = DEFAULT_CONFIG.duplicate(true)
|
||||
var _warned: Dictionary = {}
|
||||
|
||||
|
||||
func load_from(path: String = DEFAULT_PATH) -> bool:
|
||||
_warned.clear()
|
||||
_config = DEFAULT_CONFIG.duplicate(true)
|
||||
if not FileAccess.file_exists(path):
|
||||
_warn_once(path, "EditorVisualTheme: '%s' was not found; using defaults." % path)
|
||||
return false
|
||||
var file: FileAccess = FileAccess.open(path, FileAccess.READ)
|
||||
if file == null:
|
||||
_warn_once(path, "EditorVisualTheme: '%s' could not be read; using defaults." % path)
|
||||
return false
|
||||
var parsed: Variant = JSON.parse_string(file.get_as_text())
|
||||
file.close()
|
||||
if not parsed is Dictionary:
|
||||
_warn_once(path, "EditorVisualTheme: '%s' is malformed; using defaults." % path)
|
||||
return false
|
||||
_config = _deep_merge(_config, parsed as Dictionary)
|
||||
return true
|
||||
|
||||
|
||||
func raw_config() -> Dictionary:
|
||||
return _config.duplicate(true)
|
||||
|
||||
|
||||
func chrome_color(role: StringName, editor: StringName = &"") -> Color:
|
||||
var role_key: String = String(role)
|
||||
if not COLOR_ROLE_SECTIONS.has(role_key):
|
||||
_warn_once("color:" + role_key, "EditorVisualTheme: unknown color role '%s'." % role_key)
|
||||
return Color.MAGENTA
|
||||
var section: String = COLOR_ROLE_SECTIONS[role_key]
|
||||
var raw: Variant = _resolved_value(section, role_key, editor)
|
||||
var fallback_config: Dictionary = DEFAULT_CONFIG["chrome"][section]
|
||||
var fallback: Color = Color.from_string(String(fallback_config[role_key]), Color.WHITE)
|
||||
if not raw is String:
|
||||
_warn_once("color:" + role_key, "EditorVisualTheme: invalid value for color role '%s'." % role_key)
|
||||
return fallback
|
||||
var hex: String = String(raw)
|
||||
if not hex.begins_with("#") or (hex.length() != 7 and hex.length() != 9):
|
||||
_warn_once("color:" + role_key, "EditorVisualTheme: invalid color at '%s'." % role_key)
|
||||
return fallback
|
||||
return Color.from_string(hex, fallback)
|
||||
|
||||
|
||||
func chrome_px(role: StringName, editor: StringName = &"") -> int:
|
||||
var role_key: String = String(role)
|
||||
if not SIZE_ROLE_SECTIONS.has(role_key):
|
||||
_warn_once("size:" + role_key, "EditorVisualTheme: unknown size role '%s'." % role_key)
|
||||
return 0
|
||||
var section: String = SIZE_ROLE_SECTIONS[role_key]
|
||||
var config_key: String = String(CONFIG_SIZE_KEYS.get(role_key, role_key))
|
||||
var raw: Variant = _resolved_value(section, config_key, editor)
|
||||
var fallback: int = int(DEFAULT_CONFIG["chrome"][section][config_key])
|
||||
if not raw is int and not raw is float:
|
||||
_warn_once("size:" + role_key, "EditorVisualTheme: invalid value for size role '%s'." % role_key)
|
||||
return fallback
|
||||
return clampi(int(round(float(raw))), 0, 256)
|
||||
|
||||
|
||||
func stickman_canvas_color(role: StringName) -> Color:
|
||||
var role_key: String = String(role)
|
||||
var fallback_text: String = String(DEFAULT_CONFIG["stickman_canvas"].get(role_key, "#FFFFFF"))
|
||||
var fallback: Color = Color.from_string(fallback_text, Color.WHITE)
|
||||
var raw: Variant = _get_nested(_config, ["stickman_canvas", role_key], fallback_text)
|
||||
if not raw is String:
|
||||
_warn_once("stickman_canvas:" + role_key, "EditorVisualTheme: invalid Stickman canvas color '%s'." % role_key)
|
||||
return fallback
|
||||
var hex: String = String(raw)
|
||||
if not hex.begins_with("#") or (hex.length() != 7 and hex.length() != 9):
|
||||
_warn_once("stickman_canvas:" + role_key, "EditorVisualTheme: invalid Stickman canvas color '%s'." % role_key)
|
||||
return fallback
|
||||
return Color.from_string(hex, fallback)
|
||||
|
||||
|
||||
func control_theme(editor: StringName) -> Theme:
|
||||
var theme: Theme = Theme.new()
|
||||
var surface: Color = chrome_color(&"surface", editor)
|
||||
var background: Color = chrome_color(&"background", editor)
|
||||
var hover: Color = chrome_color(&"surface_hover", editor)
|
||||
var text: Color = chrome_color(&"text", editor)
|
||||
var muted: Color = chrome_color(&"text_muted", editor)
|
||||
var edit: Color = chrome_color(&"edit", editor)
|
||||
var focus: Color = chrome_color(&"focus", editor)
|
||||
var border: Color = chrome_color(&"border", editor)
|
||||
var radius: int = chrome_px(&"control", editor)
|
||||
var normal_width: int = chrome_px(&"normal_border", editor)
|
||||
var focus_width: int = maxi(2, chrome_px(&"focus_border", editor))
|
||||
var normal_box: StyleBoxFlat = _make_box(surface, border, radius, normal_width)
|
||||
var hover_box: StyleBoxFlat = _make_box(hover, edit, radius, normal_width)
|
||||
var pressed_box: StyleBoxFlat = _make_box(surface.darkened(0.12), edit, radius, focus_width)
|
||||
var focus_box: StyleBoxFlat = _make_box(Color(0, 0, 0, 0), focus, radius, focus_width)
|
||||
var disabled_box: StyleBoxFlat = _make_box(background.darkened(0.1), border, radius, normal_width)
|
||||
for type_name: String in ["Button", "OptionButton", "ColorPickerButton", "CheckBox"]:
|
||||
theme.set_stylebox("normal", type_name, normal_box.duplicate())
|
||||
theme.set_stylebox("hover", type_name, hover_box.duplicate())
|
||||
theme.set_stylebox("pressed", type_name, pressed_box.duplicate())
|
||||
theme.set_stylebox("focus", type_name, focus_box.duplicate())
|
||||
theme.set_stylebox("disabled", type_name, disabled_box.duplicate())
|
||||
theme.set_color("font_color", type_name, text)
|
||||
theme.set_color("font_hover_color", type_name, text)
|
||||
theme.set_color("font_pressed_color", type_name, edit)
|
||||
theme.set_color("font_disabled_color", type_name, muted)
|
||||
theme.set_font_size("font_size", type_name, chrome_px(&"body", editor))
|
||||
var input_box: StyleBoxFlat = _make_box(background, border, radius, normal_width)
|
||||
var input_focus: StyleBoxFlat = _make_box(background, focus, radius, focus_width)
|
||||
for type_name: String in ["LineEdit", "SpinBox", "TextEdit"]:
|
||||
theme.set_stylebox("normal", type_name, input_box.duplicate())
|
||||
theme.set_stylebox("focus", type_name, input_focus.duplicate())
|
||||
theme.set_color("font_color", type_name, text)
|
||||
theme.set_color("font_placeholder_color", type_name, muted)
|
||||
theme.set_font_size("font_size", type_name, chrome_px(&"body", editor))
|
||||
var panel_box: StyleBoxFlat = _make_box(surface, border, chrome_px(&"panel", editor), normal_width)
|
||||
theme.set_stylebox("panel", "PanelContainer", panel_box)
|
||||
theme.set_stylebox("normal", "PopupMenu", panel_box.duplicate())
|
||||
theme.set_stylebox("panel", "MenuBar", panel_box.duplicate())
|
||||
theme.set_font_size("font_size", "Label", chrome_px(&"body", editor))
|
||||
theme.set_color("font_color", "Label", text)
|
||||
theme.set_color("font_hover_color", "Label", text)
|
||||
theme.set_font_size("font_size", "PopupMenu", chrome_px(&"menu", editor))
|
||||
theme.set_font_size("font_size", "MenuBar", chrome_px(&"menu", editor))
|
||||
theme.set_color("font_color", "MenuBar", text)
|
||||
return theme
|
||||
|
||||
|
||||
func style_button_role(button: Button, role: StringName, editor: StringName) -> void:
|
||||
button.custom_minimum_size.y = maxf(button.custom_minimum_size.y, float(chrome_px(&"button_min_height", editor)))
|
||||
if role != &"primary" and role != &"destructive":
|
||||
return
|
||||
var fill_role: StringName = &"action_primary" if role == &"primary" else &"destructive"
|
||||
var fill: Color = chrome_color(fill_role, editor)
|
||||
var focus: Color = chrome_color(&"focus", editor)
|
||||
var radius: int = chrome_px(&"control", editor)
|
||||
var border_width: int = chrome_px(&"normal_border", editor)
|
||||
var focus_width: int = maxi(2, chrome_px(&"focus_border", editor))
|
||||
button.add_theme_stylebox_override("normal", _make_box(fill, fill.lightened(0.12), radius, border_width))
|
||||
button.add_theme_stylebox_override("hover", _make_box(fill.lightened(0.12), focus, radius, border_width))
|
||||
button.add_theme_stylebox_override("pressed", _make_box(fill.darkened(0.12), focus, radius, focus_width))
|
||||
button.add_theme_stylebox_override("focus", _make_box(Color(0.0, 0.0, 0.0, 0.0), focus, radius, focus_width))
|
||||
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)
|
||||
|
||||
|
||||
func _resolved_value(section: String, key: String, editor: StringName) -> Variant:
|
||||
if not editor.is_empty():
|
||||
var editor_value: Variant = _get_nested(_config, ["editors", String(editor), section, key], null)
|
||||
if editor_value != null:
|
||||
return editor_value
|
||||
return _get_nested(_config, ["chrome", section, key], null)
|
||||
|
||||
|
||||
func _get_nested(source: Dictionary, keys: Array[String], fallback: Variant) -> Variant:
|
||||
var cursor: Variant = source
|
||||
for key: String in keys:
|
||||
if not cursor is Dictionary or not (cursor as Dictionary).has(key):
|
||||
return fallback
|
||||
cursor = (cursor as Dictionary)[key]
|
||||
return cursor
|
||||
|
||||
|
||||
func _make_box(fill: Color, outline: Color, radius: int, width: int) -> StyleBoxFlat:
|
||||
var box: StyleBoxFlat = StyleBoxFlat.new()
|
||||
box.bg_color = fill
|
||||
box.border_color = outline
|
||||
box.set_border_width_all(width)
|
||||
box.set_corner_radius_all(radius)
|
||||
box.content_margin_left = 8
|
||||
box.content_margin_right = 8
|
||||
box.content_margin_top = 6
|
||||
box.content_margin_bottom = 6
|
||||
return box
|
||||
|
||||
|
||||
func _deep_merge(base: Dictionary, overrides: Dictionary) -> Dictionary:
|
||||
var merged: Dictionary = base.duplicate(true)
|
||||
for key: Variant in overrides:
|
||||
if merged.get(key) is Dictionary and overrides[key] is Dictionary:
|
||||
merged[key] = _deep_merge(merged[key] as Dictionary, overrides[key] as Dictionary)
|
||||
else:
|
||||
merged[key] = overrides[key]
|
||||
return merged
|
||||
|
||||
|
||||
func _warn_once(key: String, message: String) -> void:
|
||||
if not _warned.has(key):
|
||||
_warned[key] = true
|
||||
push_warning(message)
|
||||
@@ -0,0 +1 @@
|
||||
uid://c2mnpqu565na
|
||||
@@ -12,6 +12,9 @@ const DETAIL_META := &"stage_dialog_detail"
|
||||
const ACTIONS_META := &"stage_dialog_actions"
|
||||
const EXTRAS_META := &"stage_dialog_extra_controls"
|
||||
const CARD_META := &"stage_dialog_card"
|
||||
const CARD_WIDTH_META := &"stage_dialog_card_width"
|
||||
const MAX_HEIGHT_RATIO_META := &"stage_dialog_max_height_ratio"
|
||||
const SCREEN_MARGIN_META := &"stage_dialog_screen_margin"
|
||||
const TITLE_META := &"stage_dialog_title"
|
||||
const ICON_META := &"stage_dialog_icon"
|
||||
const ICON_LABEL_META := &"stage_dialog_icon_label"
|
||||
@@ -54,21 +57,13 @@ static func popup(dialog: Window) -> void:
|
||||
dialog.hide()
|
||||
dialog.set_meta(OPEN_META, true)
|
||||
overlay.show()
|
||||
var host_viewport: Viewport = overlay.get_viewport()
|
||||
if host_viewport != null:
|
||||
var viewport_size: Vector2i = Vector2i(host_viewport.get_visible_rect().size)
|
||||
var card: PanelContainer = dialog.get_meta(CARD_META) as PanelContainer
|
||||
if card != null:
|
||||
var card_width: int = active_theme.integer("popups.confirmation.card_width", 560, 320, 720)
|
||||
card.custom_minimum_size.x = minf(float(card_width), maxf(280.0, float(viewport_size.x - 36)))
|
||||
_fit_body_scroll(dialog, viewport_size, active_theme)
|
||||
refit(dialog)
|
||||
var tree: SceneTree = dialog.get_tree()
|
||||
if tree != null:
|
||||
var fit_after_final_layout := func() -> void:
|
||||
if not is_instance_valid(dialog) or not dialog.is_inside_tree() or not is_open(dialog):
|
||||
return
|
||||
var viewport_size: Vector2i = Vector2i(host_viewport.get_visible_rect().size) if host_viewport != null else Vector2i.ZERO
|
||||
_fit_body_scroll(dialog, viewport_size, active_theme)
|
||||
refit(dialog)
|
||||
var fit_after_layout := func() -> void:
|
||||
if is_instance_valid(tree):
|
||||
tree.process_frame.connect(fit_after_final_layout, Object.CONNECT_ONE_SHOT)
|
||||
@@ -91,6 +86,27 @@ static func is_open(dialog: Window) -> bool:
|
||||
return dialog.visible
|
||||
|
||||
|
||||
static func refit(dialog: Window) -> void:
|
||||
if dialog == null or not is_instance_valid(dialog) or not dialog.has_meta(OVERLAY_META):
|
||||
return
|
||||
var overlay: Control = dialog.get_meta(OVERLAY_META) as Control
|
||||
if overlay == null or not is_instance_valid(overlay):
|
||||
return
|
||||
var host_viewport: Viewport = overlay.get_viewport()
|
||||
if host_viewport == null:
|
||||
return
|
||||
var active_theme: SandboxTheme = _theme_for(dialog)
|
||||
var viewport_size: Vector2i = Vector2i(host_viewport.get_visible_rect().size)
|
||||
var card: PanelContainer = dialog.get_meta(CARD_META) as PanelContainer
|
||||
if card != null:
|
||||
var screen_margin: int = int(dialog.get_meta(SCREEN_MARGIN_META, 18))
|
||||
var default_width: int = active_theme.integer("popups.confirmation.card_width", 560, 320, 720)
|
||||
var card_width: int = int(dialog.get_meta(CARD_WIDTH_META, default_width))
|
||||
var available_width: float = maxf(0.0, float(viewport_size.x - screen_margin * 2))
|
||||
card.custom_minimum_size.x = minf(float(card_width), available_width)
|
||||
_fit_body_scroll(dialog, viewport_size, active_theme)
|
||||
|
||||
|
||||
static func is_styled(dialog: Window) -> bool:
|
||||
return dialog != null and is_instance_valid(dialog) and dialog.has_meta(OVERLAY_META)
|
||||
|
||||
@@ -498,7 +514,10 @@ static func _fit_body_scroll(dialog: Window, viewport_size: Vector2i, stage_them
|
||||
var body_width: float = maxf(0.0, card_width - card_padding * 2.0)
|
||||
var preferred_body_height: float = body.get_combined_minimum_size().y
|
||||
var fixed_height: float = header.get_combined_minimum_size().y + actions.get_combined_minimum_size().y + card_padding * 2.0 + content_gap * 2.0
|
||||
var max_body_height: float = maxf(48.0, float(viewport_size.y) - 36.0 - fixed_height)
|
||||
var screen_margin: int = int(dialog.get_meta(SCREEN_MARGIN_META, 18))
|
||||
var max_height_ratio: float = clampf(float(dialog.get_meta(MAX_HEIGHT_RATIO_META, 1.0)), 0.35, 1.0)
|
||||
var available_height: float = maxf(0.0, minf(float(viewport_size.y - screen_margin * 2), float(viewport_size.y) * max_height_ratio))
|
||||
var max_body_height: float = maxf(48.0, available_height - fixed_height)
|
||||
if preferred_body_height > max_body_height:
|
||||
var vertical_bar: VScrollBar = body_scroll.get_v_scroll_bar()
|
||||
if vertical_bar != null:
|
||||
|
||||
@@ -0,0 +1,396 @@
|
||||
class_name StageGroundFinishChooser
|
||||
extends ConfirmationDialog
|
||||
## Modal single-choice gallery for choosing the look of newly painted Ground.
|
||||
|
||||
const DIALOG_STYLER := preload("res://scripts/shared/stage_ui/stage_dialog_styler.gd")
|
||||
const GROUND_PREVIEW := preload("res://scripts/shared/stage_ui/stage_ground_finish_preview.gd")
|
||||
const DEFAULT_THEME := preload("res://scripts/stage/sandbox_theme.gd")
|
||||
|
||||
const DEFAULT_PAINT_TINT: Color = Color(0.25, 0.55, 0.25, 1.0)
|
||||
|
||||
var _stage_theme: SandboxTheme
|
||||
var _ui_font: Font
|
||||
var _finish_ids: Array[StringName] = []
|
||||
var _finish_labels: Array[String] = []
|
||||
var _selected_finish: StringName = &""
|
||||
var _button_group: ButtonGroup
|
||||
var _choice_buttons: Dictionary = {}
|
||||
var _choice_previews: Dictionary = {}
|
||||
var _choice_labels: Dictionary = {}
|
||||
var _content_root: VBoxContainer
|
||||
var _layout_row: HBoxContainer
|
||||
var _choice_grid: GridContainer
|
||||
var _preview_panel: PanelContainer
|
||||
var _selected_preview: StageGroundFinishPreview
|
||||
var _choice_height: int = 96
|
||||
var _choice_preview_height: int = 48
|
||||
var _choice_font_size: int = 18
|
||||
var _preview_height: int = 128
|
||||
var _compact_preview_height: int = 88
|
||||
var _preview_min_width: int = 224
|
||||
var _choice_min_width: int = 104
|
||||
var _grid_gap: int = 8
|
||||
var _compact_choice_height: int = 54
|
||||
var _compact_choice_preview_height: int = 22
|
||||
var _compact_choice_font_size: int = 16
|
||||
var _compact_grid_gap: int = 6
|
||||
var _compact_layout_max_width: int = 600
|
||||
var _compact_layout_max_height: int = 460
|
||||
var _wide_layout_min_width: int = 760
|
||||
var _screen_margin: int = 24
|
||||
var _is_wide_layout: bool = true
|
||||
var _is_compact_layout: bool = false
|
||||
|
||||
|
||||
func configure(
|
||||
stage_theme: SandboxTheme,
|
||||
ui_font: Font,
|
||||
finish_ids: Array[StringName],
|
||||
finish_labels: Array[String]
|
||||
) -> void:
|
||||
_stage_theme = stage_theme if stage_theme != null else DEFAULT_THEME.new() as SandboxTheme
|
||||
_ui_font = ui_font
|
||||
_finish_ids.assign(finish_ids)
|
||||
_finish_labels.assign(finish_labels)
|
||||
if not _finish_ids.is_empty():
|
||||
_selected_finish = _finish_ids[0]
|
||||
_configure_theme_values()
|
||||
|
||||
|
||||
func open_for_finish(finish_id: StringName) -> void:
|
||||
_select_finish(finish_id)
|
||||
_apply_responsive_layout()
|
||||
DIALOG_STYLER.popup(self)
|
||||
call_deferred("_refit_dialog")
|
||||
call_deferred("_focus_selected_choice")
|
||||
|
||||
|
||||
func selected_finish() -> StringName:
|
||||
return _selected_finish
|
||||
|
||||
|
||||
func choice_buttons() -> Dictionary:
|
||||
return _choice_buttons.duplicate()
|
||||
|
||||
|
||||
func selected_preview_finish() -> StringName:
|
||||
return &"" if _selected_preview == null else _selected_preview.finish_id
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
if _stage_theme == null:
|
||||
_stage_theme = DEFAULT_THEME.new() as SandboxTheme
|
||||
_configure_theme_values()
|
||||
title = "Choose Ground"
|
||||
dialog_text = "Stickmen walk here"
|
||||
cancel_button_text = "Close"
|
||||
ok_button_text = "Draw Grass Ground"
|
||||
theme = _stage_theme.control_theme()
|
||||
set_meta(DIALOG_STYLER.CARD_WIDTH_META, _theme_int("card_width", 880, 420, 1200))
|
||||
set_meta(DIALOG_STYLER.MAX_HEIGHT_RATIO_META, _theme_number("max_height_ratio", 0.86, 0.55, 0.96))
|
||||
set_meta(DIALOG_STYLER.SCREEN_MARGIN_META, _screen_margin)
|
||||
_build_content()
|
||||
DIALOG_STYLER.apply(self, _stage_theme, _ui_font, get_ok_button())
|
||||
var icon_badge: Control = get_meta(DIALOG_STYLER.ICON_BADGE_META, null) as Control
|
||||
if icon_badge != null:
|
||||
icon_badge.hide()
|
||||
_apply_responsive_layout()
|
||||
var viewport: Viewport = _stage_viewport()
|
||||
if viewport != null:
|
||||
viewport.size_changed.connect(_on_viewport_size_changed)
|
||||
|
||||
|
||||
func _configure_theme_values() -> void:
|
||||
_choice_font_size = _theme_int("choice_font_size", 18, 14, 26)
|
||||
_choice_height = _theme_int("choice_height", 96, 72, 132)
|
||||
_choice_preview_height = _theme_int("choice_preview_height", 48, 32, 72)
|
||||
_compact_choice_height = _theme_int("compact_choice_height", 50, 48, 72)
|
||||
_compact_choice_preview_height = _theme_int("compact_choice_preview_height", 22, 16, 36)
|
||||
_compact_choice_font_size = _theme_int("compact_choice_font_size", 16, 14, 20)
|
||||
_compact_grid_gap = _theme_int("compact_grid_gap", 6, 4, 12)
|
||||
_compact_layout_max_width = _theme_int("compact_layout_max_width", 600, 440, 760)
|
||||
_compact_layout_max_height = _theme_int("compact_layout_max_height", 460, 360, 640)
|
||||
_preview_height = _theme_int("preview_height", 128, 88, 220)
|
||||
_compact_preview_height = _theme_int("compact_preview_height", 88, 64, 140)
|
||||
_preview_min_width = _theme_int("preview_min_width", 224, 160, 360)
|
||||
_choice_min_width = _theme_int("choice_min_width", 104, 88, 160)
|
||||
_grid_gap = _theme_int("grid_gap", 8, 4, 24)
|
||||
_wide_layout_min_width = _theme_int("wide_layout_min_width", 760, 600, 1100)
|
||||
_screen_margin = _theme_int("screen_margin", 24, 12, 64)
|
||||
|
||||
|
||||
func _build_content() -> void:
|
||||
_button_group = ButtonGroup.new()
|
||||
_button_group.allow_unpress = false
|
||||
_content_root = VBoxContainer.new()
|
||||
_content_root.name = "GroundFinishGallery"
|
||||
_content_root.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_content_root.add_theme_constant_override("separation", _grid_gap)
|
||||
_layout_row = HBoxContainer.new()
|
||||
_layout_row.name = "GalleryAndPreview"
|
||||
_layout_row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_layout_row.add_theme_constant_override("separation", 16)
|
||||
_choice_grid = GridContainer.new()
|
||||
_choice_grid.name = "GroundFinishChoices"
|
||||
_choice_grid.columns = 3
|
||||
_choice_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_choice_grid.add_theme_constant_override("h_separation", _grid_gap)
|
||||
_choice_grid.add_theme_constant_override("v_separation", _grid_gap)
|
||||
_build_choice_buttons()
|
||||
_preview_panel = _build_selected_preview_panel()
|
||||
_layout_row.add_child(_choice_grid)
|
||||
_layout_row.add_child(_preview_panel)
|
||||
_content_root.add_child(_layout_row)
|
||||
add_child(_content_root)
|
||||
|
||||
|
||||
func _build_choice_buttons() -> void:
|
||||
for index: int in mini(_finish_ids.size(), _finish_labels.size()):
|
||||
var finish_id: StringName = _finish_ids[index]
|
||||
var finish_label: String = _finish_labels[index]
|
||||
var button := Button.new()
|
||||
button.name = "GroundFinish_%s" % String(finish_id).trim_prefix("finish_ground_")
|
||||
button.toggle_mode = true
|
||||
button.button_group = _button_group
|
||||
button.text = ""
|
||||
button.custom_minimum_size = Vector2(float(_choice_min_width), float(_choice_height))
|
||||
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
button.focus_mode = Control.FOCUS_ALL
|
||||
_apply_choice_style(button)
|
||||
var contents := VBoxContainer.new()
|
||||
contents.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
contents.add_theme_constant_override("separation", 4)
|
||||
contents.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
var sample := GROUND_PREVIEW.new() as StageGroundFinishPreview
|
||||
sample.name = "FinishSample"
|
||||
sample.custom_minimum_size.y = float(_choice_preview_height)
|
||||
sample.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
sample.configure(finish_id, DEFAULT_PAINT_TINT)
|
||||
contents.add_child(sample)
|
||||
var label := Label.new()
|
||||
label.name = "FinishLabel"
|
||||
label.text = finish_label
|
||||
label.custom_minimum_size.y = 20.0
|
||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
label.add_theme_font_size_override("font_size", _choice_font_size)
|
||||
label.add_theme_color_override("font_color", _theme_color("stage_panels.type.text_color", Color("#F5F7FA")))
|
||||
if _ui_font != null:
|
||||
label.add_theme_font_override("font", _ui_font)
|
||||
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
contents.add_child(label)
|
||||
button.add_child(contents)
|
||||
button.pressed.connect(_on_choice_pressed.bind(finish_id))
|
||||
_choice_grid.add_child(button)
|
||||
_choice_buttons[finish_id] = button
|
||||
_choice_previews[finish_id] = sample
|
||||
_choice_labels[finish_id] = label
|
||||
|
||||
|
||||
func _build_selected_preview_panel() -> PanelContainer:
|
||||
var panel := PanelContainer.new()
|
||||
panel.name = "SelectedGroundPreview"
|
||||
panel.custom_minimum_size = Vector2(float(_preview_min_width), 0.0)
|
||||
panel.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
panel.add_theme_stylebox_override("panel", _preview_panel_style())
|
||||
var stack := VBoxContainer.new()
|
||||
stack.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
stack.add_theme_constant_override("separation", 10)
|
||||
stack.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
var heading := Label.new()
|
||||
heading.name = "GroundPreviewHeading"
|
||||
heading.text = "New Ground will look like this"
|
||||
heading.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
heading.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
heading.add_theme_font_size_override("font_size", 18)
|
||||
heading.add_theme_color_override("font_color", _theme_color("global.text_color", Color("#F5F7FA")))
|
||||
if _ui_font != null:
|
||||
heading.add_theme_font_override("font", _ui_font)
|
||||
stack.add_child(heading)
|
||||
_selected_preview = GROUND_PREVIEW.new() as StageGroundFinishPreview
|
||||
_selected_preview.name = "SelectedGroundFinishSample"
|
||||
_selected_preview.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_selected_preview.custom_minimum_size.y = float(_preview_height)
|
||||
_selected_preview.configure(_selected_finish if not _selected_finish.is_empty() else &"finish_ground_grass", DEFAULT_PAINT_TINT)
|
||||
stack.add_child(_selected_preview)
|
||||
panel.add_child(stack)
|
||||
return panel
|
||||
|
||||
|
||||
func _select_finish(finish_id: StringName) -> void:
|
||||
if not _finish_ids.has(finish_id):
|
||||
if _finish_ids.is_empty():
|
||||
return
|
||||
finish_id = _finish_ids[0]
|
||||
_selected_finish = finish_id
|
||||
for key: Variant in _choice_buttons:
|
||||
var button: Button = _choice_buttons[key] as Button
|
||||
button.set_pressed_no_signal(StringName(key) == finish_id)
|
||||
if _selected_preview != null:
|
||||
_selected_preview.configure(finish_id, DEFAULT_PAINT_TINT)
|
||||
_update_primary_button_text()
|
||||
|
||||
|
||||
func _update_primary_button_text() -> void:
|
||||
var label: String = "Ground"
|
||||
var index: int = _finish_ids.find(_selected_finish)
|
||||
if index >= 0 and index < _finish_labels.size():
|
||||
label = _finish_labels[index]
|
||||
var button: Button = get_ok_button()
|
||||
if button == null:
|
||||
return
|
||||
button.text = "Draw %s Ground" % label
|
||||
var visual_button: Button = DIALOG_STYLER.visual_button(button)
|
||||
if visual_button != null and visual_button != button:
|
||||
visual_button.text = button.text
|
||||
|
||||
|
||||
func _on_choice_pressed(finish_id: StringName) -> void:
|
||||
_select_finish(finish_id)
|
||||
|
||||
|
||||
func _on_viewport_size_changed() -> void:
|
||||
call_deferred("_apply_responsive_layout")
|
||||
call_deferred("_refit_dialog")
|
||||
|
||||
|
||||
func _apply_responsive_layout() -> void:
|
||||
if _content_root == null or _choice_grid == null or _preview_panel == null or _layout_row == null:
|
||||
return
|
||||
var viewport: Viewport = _stage_viewport()
|
||||
if viewport == null:
|
||||
return
|
||||
var viewport_size: Vector2 = viewport.get_visible_rect().size
|
||||
var viewport_width: float = viewport_size.x
|
||||
var card_width: float = minf(
|
||||
float(_theme_int("card_width", 880, 420, 1200)),
|
||||
maxf(0.0, viewport_width - float(_screen_margin * 2))
|
||||
)
|
||||
var body_width: float = maxf(0.0, card_width - 64.0)
|
||||
var wide: bool = viewport_width >= float(_wide_layout_min_width) and body_width >= float(_preview_min_width + _choice_min_width * 3 + _grid_gap * 4)
|
||||
var compact: bool = viewport_width < float(_compact_layout_max_width) or viewport_size.y < float(_compact_layout_max_height)
|
||||
var columns: int = 3 if body_width >= float(_choice_min_width * 3 + _grid_gap * 2) else 2
|
||||
_choice_grid.columns = columns
|
||||
_is_wide_layout = wide
|
||||
_is_compact_layout = compact
|
||||
var card: Control = get_meta(DIALOG_STYLER.CARD_META) as Control
|
||||
if card != null:
|
||||
set_meta(DIALOG_STYLER.MAX_HEIGHT_RATIO_META, _theme_number("compact_height_ratio", 0.89, 0.70, 0.89) if compact else _theme_number("max_height_ratio", 0.86, 0.55, 0.86))
|
||||
_apply_choice_density(compact)
|
||||
if wide:
|
||||
if _choice_grid.get_parent() != _layout_row:
|
||||
if _choice_grid.get_parent() == _content_root:
|
||||
_content_root.remove_child(_choice_grid)
|
||||
if _preview_panel.get_parent() == _content_root:
|
||||
_content_root.remove_child(_preview_panel)
|
||||
_layout_row.add_child(_choice_grid)
|
||||
_layout_row.add_child(_preview_panel)
|
||||
_content_root.add_child(_layout_row)
|
||||
_preview_panel.visible = true
|
||||
_preview_panel.custom_minimum_size.x = float(_preview_min_width)
|
||||
_selected_preview.custom_minimum_size.y = float(_preview_height)
|
||||
else:
|
||||
if _choice_grid.get_parent() != _content_root:
|
||||
if _choice_grid.get_parent() == _layout_row:
|
||||
_layout_row.remove_child(_choice_grid)
|
||||
if _preview_panel.get_parent() == _layout_row:
|
||||
_layout_row.remove_child(_preview_panel)
|
||||
if _layout_row.get_parent() == _content_root:
|
||||
_content_root.remove_child(_layout_row)
|
||||
_content_root.add_child(_preview_panel)
|
||||
_content_root.add_child(_choice_grid)
|
||||
_preview_panel.visible = not compact
|
||||
_preview_panel.custom_minimum_size.x = 0.0
|
||||
_selected_preview.custom_minimum_size.y = float(_compact_preview_height if compact else _preview_height)
|
||||
_preview_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_choice_grid.add_theme_constant_override("v_separation", _compact_grid_gap if compact else _grid_gap)
|
||||
_content_root.queue_sort()
|
||||
|
||||
|
||||
func _refit_dialog() -> void:
|
||||
if DIALOG_STYLER.is_open(self):
|
||||
DIALOG_STYLER.refit(self)
|
||||
|
||||
|
||||
func _stage_viewport() -> Viewport:
|
||||
var overlay: Control = get_meta(DIALOG_STYLER.OVERLAY_META, null) as Control
|
||||
if overlay != null and is_instance_valid(overlay):
|
||||
return overlay.get_viewport()
|
||||
return get_viewport()
|
||||
|
||||
|
||||
func _apply_choice_density(compact: bool) -> void:
|
||||
var target_height: float = float(_compact_choice_height if compact else _choice_height)
|
||||
var target_preview_height: float = float(_compact_choice_preview_height if compact else _choice_preview_height)
|
||||
var target_font_size: int = _compact_choice_font_size if compact else _choice_font_size
|
||||
for finish_id: StringName in _choice_buttons:
|
||||
var button: Button = _choice_buttons[finish_id] as Button
|
||||
button.custom_minimum_size.y = target_height
|
||||
_apply_choice_style(button)
|
||||
var sample: StageGroundFinishPreview = _choice_previews[finish_id] as StageGroundFinishPreview
|
||||
if sample != null:
|
||||
sample.custom_minimum_size.y = target_preview_height
|
||||
var label: Label = _choice_labels[finish_id] as Label
|
||||
if label != null:
|
||||
label.add_theme_font_size_override("font_size", target_font_size)
|
||||
label.custom_minimum_size.y = 18.0 if compact else 20.0
|
||||
|
||||
|
||||
func _focus_selected_choice() -> void:
|
||||
if not is_inside_tree() or not DIALOG_STYLER.is_open(self):
|
||||
return
|
||||
var button: Button = _choice_buttons.get(_selected_finish) as Button
|
||||
if button != null and button.visible:
|
||||
button.grab_focus()
|
||||
|
||||
|
||||
func _apply_choice_style(button: Button) -> void:
|
||||
var neutral: Color = _theme_color("stage_panels.choice.neutral_surface", Color("#171B24"))
|
||||
var hover: Color = _theme_color("stage_panels.choice.hover_surface", Color("#303A4B"))
|
||||
var selected: Color = _theme_color("stage_panels.choice.selected_surface", Color("#12394A"))
|
||||
var border: Color = _theme_color("stage_panels.choice.border_color", Color("#FFFFFF33"))
|
||||
var accent: Color = _theme_color("mode_colors.edit", Color("#22C6FF"))
|
||||
button.add_theme_stylebox_override("normal", _choice_box(neutral, border, 2))
|
||||
button.add_theme_stylebox_override("hover", _choice_box(hover, accent, 2))
|
||||
button.add_theme_stylebox_override("pressed", _choice_box(selected, accent, 3))
|
||||
button.add_theme_stylebox_override("hover_pressed", _choice_box(selected.lightened(0.06), accent, 3))
|
||||
button.add_theme_stylebox_override("focus", _choice_box(Color.TRANSPARENT, Color("#F5F7FA"), 3))
|
||||
|
||||
|
||||
func _choice_box(background: Color, border: Color, border_width: 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(_theme_int("choice_corner_radius", 12, 8, 24))
|
||||
style.content_margin_left = 8.0
|
||||
style.content_margin_right = 8.0
|
||||
var vertical_padding: float = 2.0 if _is_compact_layout else 6.0
|
||||
style.content_margin_top = vertical_padding
|
||||
style.content_margin_bottom = vertical_padding
|
||||
return style
|
||||
|
||||
|
||||
func _preview_panel_style() -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = _theme_color("preview_surface", Color("#151C28"))
|
||||
style.border_color = _theme_color("preview_border", Color("#39465B"))
|
||||
style.set_border_width_all(1)
|
||||
style.set_corner_radius_all(_theme_int("preview_corner_radius", 12, 8, 24))
|
||||
style.content_margin_left = 12.0
|
||||
style.content_margin_right = 12.0
|
||||
style.content_margin_top = 12.0
|
||||
style.content_margin_bottom = 12.0
|
||||
return style
|
||||
|
||||
|
||||
func _theme_int(key: String, fallback: int, minimum: int, maximum: int) -> int:
|
||||
return fallback if _stage_theme == null else _stage_theme.integer("popups.ground_finish_chooser." + key, fallback, minimum, maximum)
|
||||
|
||||
|
||||
func _theme_number(key: String, fallback: float, minimum: float, maximum: float) -> float:
|
||||
return fallback if _stage_theme == null else _stage_theme.number("popups.ground_finish_chooser." + key, fallback, minimum, maximum)
|
||||
|
||||
|
||||
func _theme_color(key: String, fallback: Color) -> Color:
|
||||
return fallback if _stage_theme == null else _stage_theme.color("popups.ground_finish_chooser." + key, fallback)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dfo24vsra2cye
|
||||
@@ -0,0 +1,30 @@
|
||||
class_name StageGroundFinishPreview
|
||||
extends Control
|
||||
## Render-only Ground finish sample using the same motifs as Stage terrain.
|
||||
|
||||
const SURFACE_RENDERER := preload("res://scripts/stage/scenery/stage_surface_renderer.gd")
|
||||
|
||||
const DEFAULT_PAINT_TINT: Color = Color(0.25, 0.55, 0.25, 1.0)
|
||||
|
||||
var finish_id: StringName = &"finish_ground_grass"
|
||||
var paint_tint: Color = DEFAULT_PAINT_TINT
|
||||
|
||||
|
||||
func configure(p_finish_id: StringName, p_paint_tint: Color = DEFAULT_PAINT_TINT) -> void:
|
||||
finish_id = p_finish_id
|
||||
paint_tint = p_paint_tint
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
var preview_top: float = 2.0
|
||||
var motif_scale := Vector2.ONE
|
||||
if finish_id == &"finish_ground_grass":
|
||||
preview_top = minf(12.0, maxf(2.0, size.y * 0.48))
|
||||
motif_scale.y = maxf(1.0, 12.0 / maxf(1.0, preview_top - 1.0))
|
||||
var rect := Rect2(Vector2(2.0, preview_top), Vector2(size.x - 4.0, size.y - preview_top - 2.0))
|
||||
if rect.size.x <= 0.0 or rect.size.y <= 0.0:
|
||||
return
|
||||
SURFACE_RENDERER.draw_ground(self, rect, finish_id, paint_tint, Vector2.ZERO, motif_scale)
|
||||
draw_rect(rect, Color("#111821"), false, 2.0)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cgh4be2rdultd
|
||||
@@ -150,8 +150,13 @@ func open(
|
||||
entries: Array[Dictionary],
|
||||
current_sky_id: String = "",
|
||||
current_background_kind: String = "sky",
|
||||
plain_color: Color = Color("#F5EFE0")
|
||||
plain_color: Color = Color("#F5EFE0"),
|
||||
browse_state: Dictionary = {}
|
||||
) -> void:
|
||||
var restore_scenery_browse_state: bool = p_kind == "scenery" and not browse_state.is_empty()
|
||||
var restored_gallery_tab: String = String(browse_state.get("gallery_tab", "backgrounds")) if restore_scenery_browse_state else "backgrounds"
|
||||
var restored_filter_id: String = String(browse_state.get("tag_id", "all")) if restore_scenery_browse_state else "all"
|
||||
var restored_scroll: int = int(browse_state.get("scroll_vertical", 0)) if restore_scenery_browse_state else 0
|
||||
_cancel_plain_picker_preview(false)
|
||||
kind = p_kind
|
||||
_setup_kind = &""
|
||||
@@ -164,28 +169,62 @@ func open(
|
||||
_current_sky_id = current_sky_id if kind == "scenery" else ""
|
||||
_current_background_kind = &"plain" if current_background_kind == "plain" else &"sky"
|
||||
_current_plain_color = _opaque_color(plain_color)
|
||||
_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"))
|
||||
_title_label.text = "Choose a Stickman" if kind == "stickman" else ("Choose Scenery" if kind == "scenery" else ("Choose a starter scene" if kind == "starters" else "Choose a Prop"))
|
||||
_backgrounds_back_button.visible = false
|
||||
_apply_chooser_visual_style()
|
||||
_tabs.visible = kind == "scenery"
|
||||
_filter_chips.visible = false
|
||||
_make_artwork_button.visible = false
|
||||
_edit_scenery_button.visible = false
|
||||
_browse_button.visible = kind == "stickman" or kind == "prop" or kind == "scenery"
|
||||
_refresh_button.visible = kind == "stickman" or kind == "prop" or kind == "scenery"
|
||||
_empty_label.text = "No scenery found. Choose Make artwork to draw a new piece." if kind == "scenery" else ("No props found. Browse for a .prop file." if kind == "prop" else ("No starter scenes are available." if kind == "starters" else "No stickmen found."))
|
||||
_gallery_tab = "backgrounds"
|
||||
_tag_id = "all"
|
||||
_backgrounds_tab.set_pressed_no_signal(true)
|
||||
_artwork_tab.set_pressed_no_signal(false)
|
||||
_browse_button.visible = false
|
||||
_refresh_button.visible = false
|
||||
_empty_label.text = "No scenery found. Choose Make artwork to draw a new piece." if kind == "scenery" else ("No props in the library yet." if kind == "prop" else ("No starter scenes are available." if kind == "starters" else "No stickmen in the library yet."))
|
||||
_gallery_tab = restored_gallery_tab
|
||||
_tag_id = restored_filter_id
|
||||
_backgrounds_tab.set_pressed_no_signal(_gallery_tab == "backgrounds")
|
||||
_artwork_tab.set_pressed_no_signal(_gallery_tab == "artwork")
|
||||
set_entries(entries)
|
||||
if kind == "scenery":
|
||||
_set_gallery_tab("backgrounds")
|
||||
_set_gallery_tab(_gallery_tab)
|
||||
popup_centered(size)
|
||||
call_deferred("_settle_popup_after_open")
|
||||
if restore_scenery_browse_state:
|
||||
call_deferred("_restore_browse_scroll_after_open", restored_scroll, restored_gallery_tab)
|
||||
|
||||
|
||||
func capture_browse_state() -> Dictionary:
|
||||
if kind != "scenery":
|
||||
return {}
|
||||
return {
|
||||
"gallery_tab": _gallery_tab,
|
||||
"tag_id": _tag_id,
|
||||
"scroll_vertical": _grid_scroll.scroll_vertical if _grid_scroll != null else 0,
|
||||
}
|
||||
|
||||
|
||||
func _restore_browse_scroll_after_open(scroll_vertical: int, gallery_tab: String) -> void:
|
||||
await get_tree().process_frame
|
||||
if not is_inside_tree() or not visible or kind != "scenery" or _gallery_tab != gallery_tab:
|
||||
return
|
||||
_grid_scroll.scroll_vertical = maxi(0, scroll_vertical)
|
||||
|
||||
|
||||
func _settle_popup_after_open() -> void:
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
if is_inside_tree() and visible:
|
||||
# PopupPanel can resize after the first native layout pass. Reapply the
|
||||
# content-sized width once child minimum sizes and DPI scaling have settled.
|
||||
var size_before_settle: Vector2i = size
|
||||
_update_popup_layout(true)
|
||||
if size != size_before_settle:
|
||||
popup_centered(size)
|
||||
|
||||
|
||||
func set_entries(entries: Array[Dictionary]) -> void:
|
||||
_entries = entries
|
||||
if _grid_scroll != null:
|
||||
_grid_scroll.scroll_vertical = 0
|
||||
if kind == "scenery":
|
||||
_rebuild_artwork_filter_chips()
|
||||
_thumbnails.clear()
|
||||
@@ -881,8 +920,8 @@ func _set_gallery_tab(tab: String) -> void:
|
||||
_filter_chips.visible = kind == "scenery" and tab == "artwork"
|
||||
_make_artwork_button.visible = kind == "scenery" and tab == "artwork"
|
||||
_edit_scenery_button.visible = kind == "scenery"
|
||||
_browse_button.visible = kind == "stickman" or kind == "prop"
|
||||
_refresh_button.visible = kind == "stickman" or kind == "prop"
|
||||
_browse_button.visible = false
|
||||
_refresh_button.visible = false
|
||||
_empty_label.text = "No artwork here yet." if tab == "artwork" else "Choose a Sky look or wall finish."
|
||||
_page = 0
|
||||
_update_popup_layout(true)
|
||||
@@ -953,6 +992,7 @@ func _update_popup_layout(keep_visible_position: bool = false) -> void:
|
||||
var in_subview: bool = in_setup or in_color_picker or in_plain_choices
|
||||
var scenery_background_gallery: bool = _is_scenery_background_gallery() and not in_subview
|
||||
var scenery_artwork_gallery: bool = _is_scenery_artwork_gallery() and not in_subview
|
||||
var asset_scrolling_gallery: bool = _uses_scrolling_asset_gallery() and not in_subview
|
||||
var filtered: Array[Dictionary] = _filtered_entries()
|
||||
_tabs.visible = kind == "scenery" and not in_subview
|
||||
_filter_chips.visible = scenery_artwork_gallery
|
||||
@@ -972,17 +1012,17 @@ func _update_popup_layout(keep_visible_position: bool = false) -> void:
|
||||
_empty_label.visible = filtered.is_empty() and not in_subview
|
||||
_grid.visible = not filtered.is_empty() and not in_subview
|
||||
_grid_scroll.visible = not filtered.is_empty() or in_subview
|
||||
_footer.alignment = BoxContainer.ALIGNMENT_BEGIN if scenery_artwork_gallery or in_subview else BoxContainer.ALIGNMENT_CENTER
|
||||
_footer.alignment = BoxContainer.ALIGNMENT_BEGIN if scenery_artwork_gallery or asset_scrolling_gallery or in_subview else BoxContainer.ALIGNMENT_CENTER
|
||||
_footer.add_theme_constant_override(
|
||||
"separation",
|
||||
_theme_integer("popups.asset_selector.footer_gap", 16, 8, 32) if in_subview or kind == "scenery" else 8
|
||||
)
|
||||
var available: Vector2 = _usable_screen_size()
|
||||
var width_ratio: float = 0.98 if in_subview else (0.96 if kind == "starters" else 0.94)
|
||||
var width_ratio: float = 0.98 if in_subview or asset_scrolling_gallery else (0.96 if kind == "starters" else 0.94)
|
||||
var max_width: float = maxf(1.0, minf(available.x * width_ratio, available.x - POPUP_SAFE_SCREEN_MARGIN))
|
||||
var height_ratio: float = _theme_number("popups.asset_selector.starter_viewport_height_ratio", 0.96, 0.7, 0.98) if kind == "starters" else 0.86
|
||||
var max_height: float = maxf(1.0, minf(available.y * height_ratio, available.y - POPUP_SAFE_SCREEN_MARGIN * 2.0))
|
||||
var preferred_width: float = CELL_MIN_SIZE.x * (2 if kind == "starters" else COLUMNS) + GRID_H_SEPARATION * ((2 if kind == "starters" else COLUMNS) - 1) + POPUP_SIDE_PADDING
|
||||
var preferred_width: float = _cell_min_size.x * (2 if kind == "starters" else COLUMNS) + GRID_H_SEPARATION * ((2 if kind == "starters" else COLUMNS) - 1) + POPUP_SIDE_PADDING
|
||||
var preferred_height: float = max_height
|
||||
if kind == "starters":
|
||||
preferred_width = _theme_number("popups.asset_selector.starter_dialog_width", 1160.0, 440.0, 2400.0)
|
||||
@@ -993,14 +1033,14 @@ func _update_popup_layout(keep_visible_position: bool = false) -> void:
|
||||
var popup_width: float = minf(preferred_width, max_width)
|
||||
_position_footer(popup_width)
|
||||
var grid_width: float = maxf(1.0, popup_width - POPUP_SIDE_PADDING)
|
||||
var minimum_card_width: float = CELL_MIN_SIZE.x
|
||||
var minimum_card_width: float = _cell_min_size.x
|
||||
if kind == "starters":
|
||||
minimum_card_width = _theme_number("popups.asset_selector.starter_card_min_width", 340.0, 220.0, 800.0)
|
||||
_active_columns = clampi(floori((grid_width + GRID_H_SEPARATION) / (minimum_card_width + GRID_H_SEPARATION)), 1, 2 if kind == "starters" else COLUMNS)
|
||||
_page_capacity = maxi(1, filtered.size()) if scenery_background_gallery or scenery_artwork_gallery or kind == "starters" else _active_columns * ROWS
|
||||
_page_capacity = maxi(1, filtered.size()) if scenery_background_gallery or scenery_artwork_gallery or asset_scrolling_gallery or kind == "starters" else _active_columns * ROWS
|
||||
_grid.columns = _active_columns
|
||||
var available_card_width: float = (grid_width - GRID_H_SEPARATION * (_active_columns - 1)) / float(_active_columns)
|
||||
var card_width: float = available_card_width if kind == "starters" else minf(CELL_MIN_SIZE.x, available_card_width)
|
||||
var card_width: float = available_card_width if kind == "starters" else minf(_cell_min_size.x, available_card_width)
|
||||
var card_height_limit: float = 248.0 if kind != "starters" else INF
|
||||
var card_height: float
|
||||
if kind == "starters":
|
||||
@@ -1128,6 +1168,7 @@ func _rebuild() -> void:
|
||||
var in_subview: bool = in_setup or in_color_picker or in_plain_choices
|
||||
var scenery_background_gallery: bool = _is_scenery_background_gallery() and not in_subview
|
||||
var scenery_artwork_gallery: bool = _is_scenery_artwork_gallery() and not in_subview
|
||||
var asset_scrolling_gallery: bool = _uses_scrolling_asset_gallery() and not in_subview
|
||||
_position_footer(float(size.x))
|
||||
_sync_stage_preview_dimming()
|
||||
_close_button.visible = not in_setup and not in_color_picker
|
||||
@@ -1144,8 +1185,8 @@ func _rebuild() -> void:
|
||||
_color_use_button.visible = in_color_picker
|
||||
_edit_scenery_button.visible = kind == "scenery" and not in_subview
|
||||
_make_artwork_button.visible = scenery_artwork_gallery
|
||||
_browse_button.visible = (kind == "stickman" or kind == "prop") and not in_setup and not in_color_picker
|
||||
_refresh_button.visible = (kind == "stickman" or kind == "prop") and not in_setup and not in_color_picker
|
||||
_browse_button.visible = false
|
||||
_refresh_button.visible = false
|
||||
_footer_spacer.visible = scenery_artwork_gallery
|
||||
_empty_label.visible = filtered.is_empty() and not in_subview
|
||||
_grid.visible = not filtered.is_empty() and not in_subview
|
||||
@@ -1255,6 +1296,10 @@ func _is_scenery_artwork_gallery() -> bool:
|
||||
return kind == "scenery" and _gallery_tab == "artwork"
|
||||
|
||||
|
||||
func _uses_scrolling_asset_gallery() -> bool:
|
||||
return kind == "prop" or kind == "stickman"
|
||||
|
||||
|
||||
func _count_scene_background_entries(entries: Array[Dictionary]) -> int:
|
||||
var count: int = 0
|
||||
for entry: Dictionary in entries:
|
||||
@@ -1324,7 +1369,7 @@ func _build_cell(entry: Dictionary, requested_size: Vector2 = Vector2.ZERO) -> C
|
||||
card_payload["status"] = "Current background"
|
||||
card_payload["status_color"] = _theme_color("mode_colors.edit", Color("#22C6FF"))
|
||||
cell.configure(card_payload, _thumbnails.get(key, null), null)
|
||||
if kind == "scenery" or kind == "starters":
|
||||
if kind == "scenery" or kind == "starters" or _uses_scrolling_asset_gallery():
|
||||
var card_button: Button = cell as Button
|
||||
var card_border_width: int = _theme_integer("popups.asset_selector.button_border_width", 2, 1, 5)
|
||||
var card_corner_radius: int = _theme_integer("popups.asset_selector.corner_radius", 16, 8, 32)
|
||||
@@ -1351,13 +1396,12 @@ func _build_cell(entry: Dictionary, requested_size: Vector2 = Vector2.ZERO) -> C
|
||||
card_hover_border_width,
|
||||
card_corner_radius
|
||||
))
|
||||
if kind == "starters":
|
||||
card_button.add_theme_stylebox_override("hover_pressed", _make_rounded_style(
|
||||
card_selected_surface.lightened(0.05),
|
||||
_theme_color("mode_colors.edit", Color("#22C6FF")),
|
||||
card_hover_border_width,
|
||||
card_corner_radius
|
||||
))
|
||||
card_button.add_theme_stylebox_override("hover_pressed", _make_rounded_style(
|
||||
card_selected_surface.lightened(0.05),
|
||||
_theme_color("mode_colors.edit", Color("#22C6FF")),
|
||||
card_hover_border_width,
|
||||
card_corner_radius
|
||||
))
|
||||
var focus_color: Color = _theme_color("stage_panels.type.text_color", Color("#F5F7FA")) if kind == "starters" else _theme_color("mode_colors.edit", Color("#22C6FF"))
|
||||
var focus_style: StyleBoxFlat = _make_rounded_style(
|
||||
Color(0, 0, 0, 0),
|
||||
@@ -1447,7 +1491,7 @@ func _apply_chooser_visual_style() -> void:
|
||||
frame_style.set_corner_radius_all(_theme_integer("popups.asset_selector.surface_corner_radius", 14, 4, 32))
|
||||
preview_frame.add_theme_stylebox_override("panel", frame_style)
|
||||
if _title_label != null:
|
||||
var title_size: int = _theme_integer("popups.asset_selector.setup_title_size", 30, 22, 48) if kind == "scenery" else (_theme_integer("popups.asset_selector.starter_title_size", 30, 22, 48) if kind == "starters" else 24)
|
||||
var title_size: int = _theme_integer("popups.asset_selector.starter_title_size", 30, 22, 48) if kind == "starters" else _theme_integer("popups.asset_selector.setup_title_size", 30, 22, 48)
|
||||
_title_label.add_theme_font_size_override("font_size", title_size)
|
||||
_title_label.add_theme_color_override("font_color", text_color)
|
||||
_apply_emphasis_font(_title_label)
|
||||
|
||||
Reference in New Issue
Block a user