Implement Phase 17a Prop Roles UI/UX Specification and associated functionality

- Added UI/UX specification for prop roles and set down mechanics in phase 17a.
- Created `PropStageRoles` script to manage normalized behavior for props, including depth, motion, and collision settings.
- Introduced `StagePhysicsPolicy` for shared physics configurations related to props and actors.
- Developed comprehensive tests for prop roles, including default settings, legacy migration, serialization, and runtime behavior.
- Ensured proper handling of set down actions, including clearance and orientation preservation.
This commit is contained in:
2026-10-09 23:11:34 -04:00
parent 3f66cc3195
commit 1d4a71525a
52 changed files with 20051 additions and 109 deletions
@@ -0,0 +1,238 @@
class_name StageInspectorPanel
extends PanelContainer
## Reusable shell for an in-stage selection inspector.
signal close_requested
var _stage_theme: SandboxTheme = null
var _safe_rect := Rect2()
var _fit_pending: bool = false
var _margin: MarginContainer
var _layout: VBoxContainer
var _header: HBoxContainer
var _icon: Label
var _title: Label
var _close_button: Button
var _scroll: ScrollContainer
var _body: VBoxContainer
func _ready() -> void:
_build_shell()
if _stage_theme != null:
apply_theme(_stage_theme)
request_fit()
func _unhandled_key_input(event: InputEvent) -> void:
if not visible or not event is InputEventKey:
return
var key_event := event as InputEventKey
if key_event.pressed and not key_event.echo and key_event.keycode == KEY_ESCAPE:
close_requested.emit()
get_viewport().set_input_as_handled()
func apply_theme(stage_theme: SandboxTheme) -> void:
_stage_theme = stage_theme
if not is_node_ready():
return
theme = stage_theme.control_theme() if stage_theme != null else null
var shell_bg := _color("stage_panels.shell.surface_color", Color("#252B38FA"))
var shell_border := _color("stage_panels.shell.border_color", Color("#FFFFFF33"))
var panel_style := _make_style(shell_bg, shell_border, 1, 0, 0)
panel_style.set_corner_radius_all(_integer("stage_panels.shell.corner_radius", 10, 0, 32))
add_theme_stylebox_override("panel", panel_style)
var padding := _integer("stage_panels.shell.padding", 12, 4, 28)
for side: String in ["left", "right", "top", "bottom"]:
_margin.add_theme_constant_override("margin_" + side, padding)
_layout.add_theme_constant_override("separation", _integer("stage_panels.shell.header_gap", 8, 0, 32))
_header.add_theme_constant_override("separation", _integer("stage_panels.shell.header_gap", 8, 0, 32))
_close_button.custom_minimum_size = Vector2(
float(_integer("stage_panels.action.close_width", 88, 72, 180)),
float(_integer("stage_panels.action.min_target_size", 48, 44, 96))
)
_title.add_theme_font_size_override("font_size", _integer("stage_panels.type.title_size", 24, 12, 48))
_title.add_theme_color_override("font_color", _color("stage_panels.type.text_color", Color("#F5F7FA")))
if stage_theme != null:
var ui_font := stage_theme.font()
if ui_font != null:
_title.add_theme_font_override("font", ui_font)
_close_button.add_theme_font_override("font", ui_font)
var emoji_font := stage_theme.emoji_font()
if emoji_font != null:
_icon.add_theme_font_override("font", emoji_font)
_close_button.add_theme_font_size_override("font_size", _integer("stage_panels.type.body_size", 20, 14, 40))
_style_action_button(_close_button, false)
request_fit()
func set_header(title: String, icon_text: String = "") -> void:
if _title == null:
return
_title.text = title
_title.tooltip_text = title
_icon.text = icon_text
_icon.visible = not icon_text.is_empty()
request_fit()
func body() -> VBoxContainer:
return _body
func set_safe_rect(rect: Rect2) -> void:
_safe_rect = rect
request_fit()
func request_fit() -> void:
if _fit_pending:
return
_fit_pending = true
call_deferred("_fit_to_content")
func reset_scroll() -> void:
if _scroll != null:
_scroll.scroll_vertical = 0
func focus_first_control() -> void:
if _body == null:
return
for node: Node in _body.find_children("*", "Control", true, false):
var control := node as Control
if control == null or not control.visible or not control.is_visible_in_tree():
continue
if control.focus_mode == Control.FOCUS_NONE or control is Button and (control as Button).disabled:
continue
control.grab_focus()
return
_close_button.grab_focus()
func style_action_button(button: Button, prominent: bool = false) -> void:
_style_action_button(button, prominent)
func _build_shell() -> void:
if _body != null:
return
set_anchors_and_offsets_preset(Control.PRESET_TOP_LEFT)
var safe := MarginContainer.new()
add_child(safe)
_margin = safe
_layout = VBoxContainer.new()
_layout.add_theme_constant_override("separation", 8)
safe.add_child(_layout)
_header = HBoxContainer.new()
_header.add_theme_constant_override("separation", 8)
_layout.add_child(_header)
_icon = Label.new()
_icon.custom_minimum_size.x = 28.0
_icon.add_theme_font_size_override("font_size", 24)
_icon.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_icon.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
_header.add_child(_icon)
_title = Label.new()
_title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_title.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_title.max_lines_visible = 2
_title.mouse_filter = Control.MOUSE_FILTER_IGNORE
_title.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_header.add_child(_title)
_close_button = Button.new()
_close_button.text = "✕ Close"
_close_button.custom_minimum_size = Vector2(
float(_integer("stage_panels.action.close_width", 88, 72, 180)),
float(_integer("stage_panels.action.min_target_size", 48, 44, 96))
)
_close_button.focus_mode = Control.FOCUS_ALL
_close_button.pressed.connect(func() -> void: close_requested.emit())
_header.add_child(_close_button)
_scroll = ScrollContainer.new()
_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_layout.add_child(_scroll)
_body = VBoxContainer.new()
_body.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_body.add_theme_constant_override("separation", 8)
_scroll.add_child(_body)
func _fit_to_content() -> void:
_fit_pending = false
if not is_inside_tree() or _body == null:
return
var rect := _safe_rect
if rect.size.x <= 0.0 or rect.size.y <= 0.0:
rect = get_viewport_rect()
var inset := float(_integer("stage_panels.shell.edge_inset", 12, 0, 48))
var preferred_width := float(_integer("stage_panels.shell.preferred_width", 380, 220, 720))
var card_width := minf(preferred_width, maxf(1.0, rect.size.x - inset * 2.0))
custom_minimum_size.x = card_width
size.x = card_width
await get_tree().process_frame
if not is_inside_tree():
return
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)
var body_height := _body.get_combined_minimum_size().y
var fixed_height := header_height + gap + padding * 2.0
var available_height := maxf(72.0, rect.size.y - inset * 2.0)
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)
_scroll.custom_minimum_size.y = scroll_height
_scroll.update_minimum_size()
func _style_action_button(button: Button, prominent: bool) -> void:
if button == null:
return
var accent := _color("mode_colors.edit", Color("#22C6FF"))
var neutral := _color("stage_panels.action.surface_color", Color("#171B24"))
var hover := _color("stage_panels.action.hover_surface", Color("#303A4B"))
var selected := _color("stage_panels.action.selected_surface", Color("#12394A"))
var border := _color("global.border_color", Color("#FFFFFF33"))
button.add_theme_stylebox_override("normal", _make_style(selected if prominent else neutral, accent if prominent else border, 1, 10, 6))
button.add_theme_stylebox_override("hover", _make_style(selected.lightened(0.06) if prominent else hover, accent, 2, 10, 6))
button.add_theme_stylebox_override("pressed", _make_style(selected, accent, 2, 10, 6))
button.add_theme_stylebox_override("hover_pressed", _make_style(selected.lightened(0.06), accent, 2, 10, 6))
button.add_theme_stylebox_override("disabled", _make_style(_color("stage_panels.choice.disabled_surface", Color("#252B38")), border, 1, 10, 6))
button.add_theme_stylebox_override("focus", _make_style(Color(0, 0, 0, 0), accent, 2, 0, 0))
button.add_theme_font_size_override("font_size", _integer("stage_panels.type.body_size", 20, 14, 40))
button.add_theme_color_override("font_color", _color("stage_panels.type.text_color", Color("#F5F7FA")))
if _stage_theme != null:
var ui_font := _stage_theme.font()
if ui_font != null:
button.add_theme_font_override("font", ui_font)
func _make_style(bg: Color, border: Color, border_width: int, hpad: int, vpad: int) -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = bg
style.border_color = border
style.set_border_width_all(border_width)
style.set_corner_radius_all(_integer("stage_panels.shell.corner_radius", 10, 0, 32))
style.content_margin_left = hpad
style.content_margin_right = hpad
style.content_margin_top = vpad
style.content_margin_bottom = vpad
return style
func _integer(path: String, fallback: int, minimum: int = -2147483648, maximum: int = 2147483647) -> 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://dxpgiyw4mxljt
@@ -0,0 +1,126 @@
class_name StagePanelChoiceGroup
extends VBoxContainer
## Exclusive, full-width choices with optional decorative cues.
signal choice_selected(id: StringName)
var _stage_theme: SandboxTheme = null
var _group := ButtonGroup.new()
var _buttons: Dictionary = {}
var _selected_id: StringName = &""
func _ready() -> void:
_group.allow_unpress = true
add_theme_constant_override("separation", _integer("stage_panels.choice.row_gap", 6, 0, 32))
func add_choice(id: StringName, caption: String, cue: Control = null) -> Button:
var button := Button.new()
button.name = "Choice_%s" % String(id)
button.toggle_mode = true
button.button_group = _group
button.custom_minimum_size.y = float(_integer("stage_panels.choice.minimum_height", 56, 44, 112))
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
button.focus_mode = Control.FOCUS_ALL
button.text = ""
var margin := MarginContainer.new()
margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
margin.add_theme_constant_override("margin_left", 10)
margin.add_theme_constant_override("margin_right", 10)
margin.add_theme_constant_override("margin_top", 4)
margin.add_theme_constant_override("margin_bottom", 4)
margin.mouse_filter = Control.MOUSE_FILTER_IGNORE
button.add_child(margin)
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 8)
row.mouse_filter = Control.MOUSE_FILTER_IGNORE
margin.add_child(row)
if cue != null:
cue.mouse_filter = Control.MOUSE_FILTER_IGNORE
row.add_child(cue)
var label := Label.new()
label.text = caption
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
row.add_child(label)
button.pressed.connect(_on_choice_pressed.bind(id, button))
add_child(button)
_buttons[id] = {"button": button, "label": label}
if _stage_theme != null:
_apply_choice_theme(button, label)
return button
func select_no_signal(id: StringName) -> void:
if not _buttons.has(id):
clear_no_signal()
return
_selected_id = id
for key: Variant in _buttons:
var button: Button = _buttons[key]["button"]
button.set_pressed_no_signal(StringName(key) == id)
func clear_no_signal() -> void:
_selected_id = &""
for entry: Dictionary in _buttons.values():
(entry["button"] as Button).set_pressed_no_signal(false)
func apply_theme(stage_theme: SandboxTheme) -> void:
_stage_theme = stage_theme
add_theme_constant_override("separation", _integer("stage_panels.choice.row_gap", 6, 0, 32))
for entry: Dictionary in _buttons.values():
_apply_choice_theme(entry["button"] as Button, entry["label"] as Label)
func _on_choice_pressed(id: StringName, button: Button) -> void:
if not button.button_pressed:
if _buttons.has(_selected_id):
(_buttons[_selected_id]["button"] as Button).set_pressed_no_signal(true)
return
_selected_id = id
choice_selected.emit(id)
func _apply_choice_theme(button: Button, label: Label) -> void:
if _stage_theme == null:
return
var font := _stage_theme.font()
if font != null:
label.add_theme_font_override("font", font)
label.add_theme_font_size_override("font_size", _integer("stage_panels.type.body_size", 20, 14, 40))
label.add_theme_color_override("font_color", _color("stage_panels.type.text_color", Color("#F5F7FA")))
var neutral := _color("stage_panels.choice.neutral_surface", Color("#171B24"))
var hover := _color("stage_panels.choice.hover_surface", Color("#303A4B"))
var selected := _color("stage_panels.choice.selected_surface", Color("#12394A"))
var disabled := _color("stage_panels.choice.disabled_surface", Color("#252B38"))
var border := _color("stage_panels.choice.border_color", Color("#FFFFFF33"))
var accent := _color("mode_colors.edit", Color("#22C6FF"))
button.add_theme_stylebox_override("normal", _make_style(neutral, border, 1))
button.add_theme_stylebox_override("hover", _make_style(hover, border, 1))
button.add_theme_stylebox_override("pressed", _make_style(selected, accent, 2))
button.add_theme_stylebox_override("hover_pressed", _make_style(selected.lightened(0.06), accent, 2))
button.add_theme_stylebox_override("disabled", _make_style(disabled, border, 1))
button.add_theme_stylebox_override("focus", _make_style(Color(0, 0, 0, 0), accent, 2))
func _make_style(bg: Color, border: Color, width: int) -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = bg
style.border_color = border
style.set_border_width_all(width)
style.set_corner_radius_all(_integer("stage_panels.shell.corner_radius", 10, 0, 32))
return style
func _integer(path: String, fallback: int, minimum: int = -2147483648, maximum: int = 2147483647) -> 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://du3kqlsq5t10
@@ -0,0 +1,65 @@
class_name StagePanelSwitchVisual
extends Control
## Draw-only switch. Input belongs to its containing toggle row.
var _enabled: bool = false
var _unavailable: bool = false
var _stage_theme: SandboxTheme = null
var _track_size := Vector2(64.0, 34.0)
var _knob_size: float = 26.0
func _ready() -> void:
mouse_filter = Control.MOUSE_FILTER_IGNORE
_apply_metrics()
queue_redraw()
func set_state(enabled: bool, unavailable: bool) -> void:
_enabled = enabled
_unavailable = unavailable
queue_redraw()
func apply_theme(stage_theme: SandboxTheme) -> void:
_stage_theme = stage_theme
_track_size.x = float(_integer("stage_panels.toggle.switch_width", 64, 48, 112))
_track_size.y = float(_integer("stage_panels.toggle.switch_height", 34, 28, 64))
_knob_size = float(_integer("stage_panels.toggle.knob_size", 26, 20, 52))
_apply_metrics()
queue_redraw()
func _draw() -> void:
if size.x <= 0.0 or size.y <= 0.0:
return
var on_color := _color("stage_panels.toggle.switch_on_color", Color("#22C6FF"))
var off_color := _color("stage_panels.toggle.switch_off_color", Color("#4A5362"))
var disabled_color := _color("stage_panels.toggle.switch_disabled_color", Color("#3B414D"))
var knob_color := _color("stage_panels.toggle.knob_color", Color("#F5F7FA"))
var track_color := disabled_color if _unavailable else (on_color if _enabled else off_color)
var center_y := size.y * 0.5
var radius := center_y
draw_rect(Rect2(radius, 0.0, maxf(0.0, size.x - radius * 2.0), size.y), track_color)
draw_circle(Vector2(radius, center_y), radius, track_color)
draw_circle(Vector2(size.x - radius, center_y), radius, track_color)
var knob_radius := minf(_knob_size, size.y - 4.0) * 0.5
var knob_center := Vector2(size.x - radius, center_y) if _enabled else Vector2(radius, center_y)
draw_circle(knob_center, knob_radius, knob_color)
if _enabled and not _unavailable:
var check_color := on_color.darkened(0.25)
draw_line(knob_center + Vector2(-4.5, 0.0), knob_center + Vector2(-1.0, 3.5), check_color, 2.0, true)
draw_line(knob_center + Vector2(-1.0, 3.5), knob_center + Vector2(5.0, -4.0), check_color, 2.0, true)
func _apply_metrics() -> void:
custom_minimum_size = _track_size
size_flags_vertical = Control.SIZE_SHRINK_BEGIN
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://d24xh8pdqpyyn
@@ -0,0 +1,189 @@
class_name StagePanelToggleRow
extends Button
## Full-row toggle with stable switch placement and optional help.
signal preferred_height_changed
const SWITCH_VISUAL := preload("res://scripts/shared/stage_ui/stage_panel_switch_visual.gd")
var _stage_theme: SandboxTheme = null
var _caption: String = ""
var _normal_help: String = ""
var _disabled_reason: String = ""
var _caption_label: Label
var _help_label: Label
var _label_stack: VBoxContainer
var _state_label: Label
var _switch_visual: StagePanelSwitchVisual
var _content_margin: MarginContainer
func _ready() -> void:
text = ""
toggle_mode = true
focus_mode = Control.FOCUS_ALL
_build_content()
toggled.connect(_on_toggled)
if _stage_theme != null:
apply_theme(_stage_theme)
_sync_text()
_sync_switch()
func configure(caption: String) -> void:
_caption = caption
_sync_text()
func set_help_text(text_value: String) -> void:
_normal_help = text_value
_sync_text()
func set_disabled_state(value: bool, reason: String = "") -> void:
if value:
_disabled_reason = reason
else:
_disabled_reason = ""
disabled = value
_sync_text()
_sync_switch()
func set_checked_no_signal(value: bool) -> void:
set_pressed_no_signal(value)
_sync_switch()
func apply_theme(stage_theme: SandboxTheme) -> void:
_stage_theme = stage_theme
if _caption_label == null:
return
var font := stage_theme.font() if stage_theme != null else null
if font != null:
for label: Label in [_caption_label, _help_label, _state_label]:
label.add_theme_font_override("font", font)
_caption_label.add_theme_font_size_override("font_size", _integer("stage_panels.type.body_size", 20, 14, 40))
_help_label.add_theme_font_size_override("font_size", _integer("stage_panels.type.helper_size", 16, 12, 32))
_state_label.add_theme_font_size_override("font_size", _integer("stage_panels.type.helper_size", 16, 12, 32))
_content_margin.add_theme_constant_override("margin_left", _integer("stage_panels.toggle.row_padding", 12, 4, 28))
_content_margin.add_theme_constant_override("margin_right", _integer("stage_panels.toggle.row_padding", 12, 4, 28))
_switch_visual.apply_theme(stage_theme)
var neutral := _color("stage_panels.toggle.neutral_surface", Color("#202633"))
var hover := _color("stage_panels.toggle.hover_surface", Color("#2B3442"))
var selected := _color("stage_panels.toggle.selected_surface", Color("#173443"))
var disabled_surface := _color("stage_panels.toggle.disabled_surface", Color("#252B38"))
var border := _color("stage_panels.toggle.border_color", Color("#FFFFFF33"))
var accent := _color("mode_colors.edit", Color("#22C6FF"))
add_theme_stylebox_override("normal", _make_style(neutral, border, 1))
add_theme_stylebox_override("hover", _make_style(hover, border, 1))
add_theme_stylebox_override("pressed", _make_style(selected, accent, 1))
add_theme_stylebox_override("hover_pressed", _make_style(selected.lightened(0.06), accent, 1))
add_theme_stylebox_override("disabled", _make_style(disabled_surface, border, 1))
add_theme_stylebox_override("focus", _make_style(Color(0, 0, 0, 0), accent, 2))
_sync_text()
func _build_content() -> void:
var margin := MarginContainer.new()
margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
margin.add_theme_constant_override("margin_left", 12)
margin.add_theme_constant_override("margin_right", 12)
margin.add_theme_constant_override("margin_top", 6)
margin.add_theme_constant_override("margin_bottom", 6)
margin.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(margin)
_content_margin = margin
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 8)
row.mouse_filter = Control.MOUSE_FILTER_IGNORE
margin.add_child(row)
_label_stack = VBoxContainer.new()
_label_stack.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_label_stack.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
_label_stack.add_theme_constant_override("separation", 2)
_label_stack.mouse_filter = Control.MOUSE_FILTER_IGNORE
row.add_child(_label_stack)
_caption_label = Label.new()
_caption_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_caption_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_caption_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
_label_stack.add_child(_caption_label)
_help_label = Label.new()
_help_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_help_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
_label_stack.add_child(_help_label)
_switch_visual = SWITCH_VISUAL.new()
_switch_visual.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
row.add_child(_switch_visual)
_state_label = Label.new()
_state_label.custom_minimum_size.x = 28.0
_state_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
_state_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_state_label.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
_state_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
row.add_child(_state_label)
# Connect after all labels exist: adding the first label can trigger a resize
# synchronously, before the help and state labels have been constructed.
_caption_label.resized.connect(_update_preferred_height)
_help_label.resized.connect(_update_preferred_height)
func _sync_text() -> void:
var help := _disabled_reason if disabled and not _disabled_reason.is_empty() else _normal_help
var base_height := float(_integer("stage_panels.toggle.minimum_height", 64, 48, 120))
var help_height := float(_integer("stage_panels.toggle.help_minimum_height", 72, 56, 144))
if _caption_label == null or _help_label == null or _label_stack == null or _state_label == null:
custom_minimum_size.y = help_height if not help.is_empty() else base_height
return
_caption_label.text = _caption
_help_label.text = help
_help_label.visible = not help.is_empty()
_sync_label_colors()
var content_height := _label_stack.get_combined_minimum_size().y
var next_height := maxf(help_height if not help.is_empty() else base_height, content_height + 12.0)
if not is_equal_approx(custom_minimum_size.y, next_height):
custom_minimum_size.y = next_height
update_minimum_size()
preferred_height_changed.emit()
func _update_preferred_height() -> void:
_sync_text()
func _sync_switch() -> void:
if _switch_visual == null or _state_label == null:
return
_switch_visual.set_state(button_pressed, disabled)
_state_label.text = "ON" if button_pressed else "OFF"
_sync_label_colors()
func _sync_label_colors() -> void:
if _caption_label == null or _help_label == null or _state_label == null:
return
_caption_label.add_theme_color_override("font_color", _color("stage_panels.type.text_color", Color("#F5F7FA")))
_help_label.add_theme_color_override("font_color", _color("stage_panels.type.muted_text_color", Color("#AAB2C0")))
_state_label.add_theme_color_override("font_color", _color("stage_panels.type.muted_text_color", Color("#AAB2C0")))
func _on_toggled(_value: bool) -> void:
_sync_switch()
func _make_style(bg: Color, border: Color, width: int) -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = bg
style.border_color = border
style.set_border_width_all(width)
style.set_corner_radius_all(_integer("stage_panels.shell.corner_radius", 10, 0, 32))
return style
func _integer(path: String, fallback: int, minimum: int, maximum: int) -> int:
return fallback if _stage_theme == null else _stage_theme.integer(path, fallback, minimum, maximum)
func _color(path: String, fallback: Color) -> Color:
return fallback if _stage_theme == null else _stage_theme.color(path, fallback)
@@ -0,0 +1 @@
uid://4so64df7s3if