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:
@@ -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
|
||||
Reference in New Issue
Block a user