feat(stage): complete phase 20b action plans

This commit is contained in:
2026-11-04 01:19:57 -05:00
parent 87a33b0abc
commit a9c0886348
70 changed files with 14234 additions and 469 deletions
File diff suppressed because it is too large Load Diff
+7 -1
View File
@@ -37,6 +37,7 @@ var _row_font_size: int = 0
var _title_font_size: int = 0
var _title_font: Font = null
var _use_target_summary_resolver: Callable = Callable()
var _prop_target_name_resolver: Callable = Callable()
func _ready() -> void:
@@ -62,6 +63,11 @@ func set_use_target_summary_resolver(resolver: Callable) -> void:
refresh()
func set_prop_target_name_resolver(resolver: Callable) -> void:
_prop_target_name_resolver = resolver
refresh()
func refresh() -> void:
if _list == null:
return
@@ -151,7 +157,7 @@ func _make_row(index: int, action: Dictionary, status: int) -> PanelContainer:
var summary := Label.new()
summary.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var action_summary: String = ACTION_REGISTRY.summarize(action)
var action_summary: String = ACTION_REGISTRY.summarize(action, _prop_target_name_resolver)
if String(action.get("type", "")) == "use_prop" and _use_target_summary_resolver.is_valid():
action_summary = String(_use_target_summary_resolver.call(
String(action.get("target_prop_id", "")),
File diff suppressed because it is too large Load Diff
@@ -283,7 +283,7 @@ func set_down_target(position: Vector2) -> void:
func show_action_editor() -> void:
if _action_editor != null:
_action_editor.popup_centered()
_action_editor.resume_editor()
func request_action_target_capture() -> void:
@@ -304,7 +304,7 @@ func request_action_target_capture() -> void:
var pace: String = "run" if _action_editor.current_move_style() == ACTION_REGISTRY.RUN else "walk"
_emit_capture_request(&"position", "🚩 Click where they should %s (Esc or RMB to cancel)" % pace, Callable(self, "set_walk_target"), Callable(self, "cancel_target_capture"))
"pick_up_prop":
_emit_capture_request(&"prop", "🫴 Click the prop to pick up (Esc or RMB to cancel)", Callable(self, "accept_prop_target"), Callable(self, "cancel_target_capture"))
_emit_capture_request(&"pickup_prop", "Choose a prop to pick up", Callable(self, "accept_prop_target"), Callable(self, "cancel_target_capture"))
"use_prop":
_emit_capture_request(&"use_prop", "Click a prop, then choose its Use spot", Callable(self, "set_use_target_from_capture"), Callable(self, "cancel_target_capture"))
"throw_prop":
File diff suppressed because it is too large Load Diff
+75 -13
View File
@@ -27,7 +27,7 @@ signal reset_requested
const TOP_BAR_HEIGHT: float = 64.0
const STATUS_BAR_HEIGHT: float = 38.0
const COMPACT_TOP_BAR_WIDTH: float = 900.0
const COMPACT_TOP_BAR_WIDTH: float = 1100.0
var _stage_theme: SandboxTheme
var _current_mode: int = 0
@@ -39,6 +39,7 @@ var _accent_direct: Color = Color("#FFB300")
var _accent_play: Color = Color("#33DD77")
var _status_pill_font_size: int = 16
var _tooltip_font_size: int = 18
var _direct_hint_requested: bool = false
var _mode_buttons: Dictionary = {}
var _palette_buttons: Dictionary = {}
@@ -58,6 +59,7 @@ var _pause_button: Button
var _resume_button: Button
var _reset_button: Button
var _tooltip: Label
var _action_tooltip_avoid_rect: Rect2 = Rect2()
var _grid_check: CheckBox
var _snap_check: CheckBox
var _grid_size_spin: SpinBox
@@ -293,21 +295,76 @@ func set_action_tooltip(visible: bool, text_value: String = "") -> void:
_tooltip.visible = visible
if not visible:
return
_tooltip.text = text_value
_tooltip.text = _capture_tooltip_text(text_value)
var style: StyleBox = _tooltip.get_theme_stylebox("normal")
var extra: Vector2 = style.get_minimum_size() if style != null else Vector2.ZERO
_tooltip.size = _tooltip.get_minimum_size() + extra
var required_size: Vector2 = _tooltip.get_minimum_size() + extra
var tooltip_width: float = clampf(required_size.x, 320.0, 420.0)
_tooltip.size = Vector2(tooltip_width, maxf(required_size.y, 50.0))
var mouse: Vector2 = get_viewport().get_mouse_position()
var size: Vector2 = _tooltip.size
var position: Vector2 = mouse + Vector2(20.0, -20.0)
var viewport_size: Vector2 = get_viewport().get_visible_rect().size
if position.x + size.x > viewport_size.x:
position.x = mouse.x - size.x - 20.0
if position.y + size.y > viewport_size.y:
position.y = mouse.y - size.y - 20.0
if position.y < 0.0:
position.y = mouse.y + 20.0
_tooltip.position = position
var avoid_rects: Array[Rect2] = []
if _action_tooltip_avoid_rect.has_area():
avoid_rects.append(_action_tooltip_avoid_rect)
if _mode_badge != null and _mode_badge.visible:
avoid_rects.append(_mode_badge.get_global_rect().grow(6.0))
if _timestamp_badge != null and _timestamp_badge.visible:
avoid_rects.append(_timestamp_badge.get_global_rect().grow(6.0))
if _palette_dock != null and _palette_dock.visible:
avoid_rects.append(_palette_dock.get_global_rect().grow(6.0))
_tooltip.position = _find_action_tooltip_position(mouse, size, viewport_size, avoid_rects)
func _find_action_tooltip_position(mouse: Vector2, size: Vector2, viewport_size: Vector2, avoid_rects: Array[Rect2]) -> Vector2:
var min_x: float = 8.0
var max_x: float = maxf(min_x, viewport_size.x - size.x - 8.0)
var min_y: float = TOP_BAR_HEIGHT + 8.0
var max_y: float = maxf(min_y, viewport_size.y - size.y - 8.0)
var candidates: Array[Vector2] = [
mouse + Vector2(20.0, -20.0),
mouse + Vector2(-size.x - 20.0, -20.0),
mouse + Vector2(20.0, 20.0),
mouse + Vector2(-size.x - 20.0, 20.0),
]
for avoid_rect: Rect2 in avoid_rects:
candidates.append(Vector2(avoid_rect.position.x - size.x - 12.0, avoid_rect.get_center().y - size.y * 0.5))
candidates.append(Vector2(avoid_rect.end.x + 12.0, avoid_rect.get_center().y - size.y * 0.5))
candidates.append(Vector2(avoid_rect.get_center().x - size.x * 0.5, avoid_rect.position.y - size.y - 12.0))
candidates.append(Vector2(avoid_rect.get_center().x - size.x * 0.5, avoid_rect.end.y + 12.0))
candidates.append(Vector2(min_x, min_y))
candidates.append(Vector2(max_x, min_y))
candidates.append(Vector2(min_x, max_y))
candidates.append(Vector2(max_x, max_y))
for candidate: Vector2 in candidates:
var position: Vector2 = Vector2(clampf(candidate.x, min_x, max_x), clampf(candidate.y, min_y, max_y))
var tooltip_rect: Rect2 = Rect2(position, size)
var overlaps: bool = false
for avoid_rect: Rect2 in avoid_rects:
if tooltip_rect.intersects(avoid_rect):
overlaps = true
break
if not overlaps:
return position
return Vector2(min_x, min_y)
func set_action_tooltip_avoid_rect(rect: Rect2) -> void:
_action_tooltip_avoid_rect = rect
func _capture_tooltip_text(text_value: String) -> String:
var task: String = text_value.strip_edges()
for old_copy: String in ["(Esc or RMB to cancel)", "(Esc or right-click to cancel)", "Esc cancels."]:
task = task.replace(old_copy, "").strip_edges()
while task.ends_with("\n"):
task = task.substr(0, task.length() - 1).strip_edges()
var cancel_hint: String = "Press Escape or right-click to cancel"
if task.is_empty():
return cancel_hint
if task.contains(cancel_hint):
return task
return "%s\n%s" % [task, cancel_hint]
func set_edit_controls_visible(visible: bool) -> void:
@@ -326,8 +383,9 @@ func set_edit_controls_visible(visible: bool) -> void:
func set_direct_controls_visible(visible: bool) -> void:
_direct_hint_requested = visible
if _direct_hint_label != null:
_direct_hint_label.visible = visible
_direct_hint_label.visible = visible and get_viewport().get_visible_rect().size.x > COMPACT_TOP_BAR_WIDTH
func is_stage_frame_position(viewport_pos: Vector2) -> bool:
@@ -506,6 +564,8 @@ func _apply_responsive_top_bar_layout() -> void:
return
var viewport_width: float = get_viewport().get_visible_rect().size.x
var compact: bool = viewport_width <= COMPACT_TOP_BAR_WIDTH
if _direct_hint_label != null:
_direct_hint_label.visible = _direct_hint_requested and not compact
if compact:
_file_menu.custom_minimum_size = Vector2(64.0, 48.0)
_file_menu.add_theme_font_size_override("font_size", 18)
@@ -656,7 +716,7 @@ func _build_timestamp_badge() -> void:
_timestamp_badge.mouse_filter = Control.MOUSE_FILTER_IGNORE
_timestamp_badge.offset_left = -220.0
_timestamp_badge.offset_top = TOP_BAR_HEIGHT + 62.0
_timestamp_badge.offset_right = -12.0
_timestamp_badge.offset_right = -_theme_number("stage_panels.shell.edge_inset", 12.0, 0.0, 48.0)
_timestamp_badge.offset_bottom = TOP_BAR_HEIGHT + 106.0
var style := StyleBoxFlat.new()
style.bg_color = _theme_color("chrome.timestamp_badge.bg_color", Color("#171B24F2"))
@@ -683,6 +743,8 @@ func _build_action_tooltip() -> void:
_tooltip.name = "ActionTooltip"
_tooltip.visible = false
_tooltip.mouse_filter = Control.MOUSE_FILTER_IGNORE
_tooltip.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_tooltip.max_lines_visible = 3
var style := StyleBoxFlat.new()
style.bg_color = Color(0.1, 0.1, 0.1, 0.9)
style.border_color = _accent_direct
@@ -75,7 +75,7 @@ func build_ui(parent: Node, sandbox_theme: SandboxTheme, ui_font: Font, emoji_fo
_action_popup.add_separator()
_action_popup.add_item("⚡ When...", int(action_ids.get("when", 5)))
_action_popup.add_separator()
_action_popup.add_item("📋 Edit Queue…", int(action_ids.get("edit_queue", 7)))
_action_popup.add_item("📋 Step Stack…", int(action_ids.get("edit_queue", 7)))
_action_popup.add_item("⚡ Edit Rules…", int(action_ids.get("edit_rules", 8)))
_action_popup.id_pressed.connect(_on_action_popup_id_pressed)
_action_popup.about_to_popup.connect(_on_action_popup_about_to_popup)
@@ -308,7 +308,7 @@ func refresh_action_popup_items(rules: Array[Dictionary]) -> void:
if _action_popup == null:
return
var valid: bool = context_rig != null and is_instance_valid(context_rig)
_disable_popup_item(_action_popup, 7, not valid or context_rig.get_queue().is_empty())
_disable_popup_item(_action_popup, 7, not valid)
_disable_popup_item(_action_popup, 8, not valid or not rig_has_rules(context_rig, rules))
@@ -316,7 +316,7 @@ func refresh_rig_context_items(rig: StickmanRig, popup: PopupMenu, rules: Array[
if popup == null:
return
var valid: bool = rig != null and is_instance_valid(rig)
_disable_popup_item(popup, 0, not valid or rig.get_queue().is_empty())
_disable_popup_item(popup, 0, not valid)
_disable_popup_item(popup, 1, not valid or not rig_has_rules(rig, rules))
@@ -455,10 +455,13 @@ func update_action_overlay(
if awaiting:
var origin: Vector2 = action_origin(capture_kind, set_down_rig, throw_rig, throw_cache, _action_queue.panel_rig if _action_queue != null else null)
var target: Vector2 = _camera.get_global_mouse_position() if _camera != null else Vector2.ZERO
var movement_target_capture: bool = capture_kind == "position" or pending_walk_target or (_rule_authoring != null and _rule_authoring.step == RULE_STEP_ACTION_POSITION)
if snap_enabled and (capture_kind in ["position", "throw", "set_down"] or (_rule_authoring != null and _rule_authoring.step == RULE_STEP_ACTION_POSITION)):
if grid_size > 0.0:
target = Vector2(roundf(target.x / grid_size) * grid_size, roundf(target.y / grid_size) * grid_size)
if capture_kind == "set_down":
if capture_kind in ["plan_place", "plan_prop"]:
_placement_overlay.set_action_marker(target)
elif capture_kind == "set_down":
var known: bool = bool(set_down_cache.get("known", false))
var valid: bool = bool(set_down_cache.get("valid", false))
if known:
@@ -487,11 +490,15 @@ func update_action_overlay(
throw_preview["start_position"], target, throw_preview["landing_position"], target_reachable,
trajectory_points, effective_target, collision_position, preview_planned, preview_uncertain
)
elif movement_target_capture:
_placement_overlay.set_grounded_action_trajectory(origin, target, is_target_valid(target))
else:
_placement_overlay.set_action_trajectory(origin, target, is_target_valid(target))
# Object/actor/rule target selection needs a cursor marker, not a
# movement-looking line from the actor to the hovered target.
_placement_overlay.set_action_marker(target)
elif direct_move_target.is_finite() and context_rig != null and is_instance_valid(context_rig):
var origin: Vector2 = context_rig.global_position - STICKMAN_LOCOMOTION.FOOT_OFFSET
_placement_overlay.set_action_trajectory(origin, direct_move_target, true)
_placement_overlay.set_grounded_action_trajectory(origin, direct_move_target, true)
else:
_placement_overlay.clear_action()
if _chrome != null:
@@ -0,0 +1,287 @@
class_name StageInspectorCaptureOverlay
extends Control
## Keeps an active world-target choice visible beside the inspector while
## dimming and blocking the inspector controls underneath it.
signal capture_action_requested(action_id: StringName)
const CAPTURE_STRIP_HEIGHT: float = 72.0
const CAPTURE_MULTI_ACTION_STRIP_HEIGHT: float = 124.0
const CAPTURE_BUTTON_HEIGHT: float = 48.0
const CAPTURE_CANCEL_WIDTH: float = 84.0
const CAPTURE_ACTION_WIDTH: float = 112.0
var _stage_theme: SandboxTheme
var _inspector: Control
var _inspector_scrim: Panel
var _capture_strip: PanelContainer
var _content_column: VBoxContainer
var _title_row: HBoxContainer
var _caption: Label
var _caption_base_font_size: int = 17
var _actions_row: HBoxContainer
var _action_buttons: Array[Button] = []
var _active_signature: String = ""
var _focus_before_capture: Control
func configure(stage_theme: SandboxTheme) -> void:
_stage_theme = stage_theme
func _ready() -> void:
name = "StageInspectorCaptureOverlay"
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
mouse_filter = Control.MOUSE_FILTER_IGNORE
z_index = 120
visible = false
_build_controls()
get_viewport().size_changed.connect(_layout_capture_overlay)
func _process(_delta: float) -> void:
_layout_capture_overlay()
func show_capture(inspector: Control, prompt: String, actions: Array[Dictionary]) -> bool:
if inspector == null or not is_instance_valid(inspector) or not inspector.is_visible_in_tree():
hide_capture()
return false
var normalized_actions: Array[Dictionary] = actions.duplicate(true)
if normalized_actions.is_empty():
normalized_actions.append({"id": "cancel_capture", "label": "Cancel", "primary": true})
_set_stacked_actions(normalized_actions.size() > 1)
var action_signature: Array[String] = []
for action: Dictionary in normalized_actions:
action_signature.append("%s:%s" % [String(action.get("id", "")), String(action.get("label", ""))])
var signature: String = "%d|%s|%s" % [inspector.get_instance_id(), prompt, ";".join(action_signature)]
if _active_signature != signature:
if _active_signature.is_empty():
_focus_before_capture = get_viewport().gui_get_focus_owner()
_active_signature = signature
_inspector = inspector
_caption.text = prompt
_rebuild_actions(normalized_actions)
visible = true
if not _action_buttons.is_empty():
_action_buttons[_action_buttons.size() - 1].grab_focus()
_layout_capture_overlay()
return true
func hide_capture() -> void:
if _active_signature.is_empty():
return
_active_signature = ""
_inspector = null
visible = false
for button: Button in _action_buttons:
button.queue_free()
_action_buttons.clear()
if _focus_before_capture != null and is_instance_valid(_focus_before_capture) and _focus_before_capture.is_visible_in_tree():
_focus_before_capture.grab_focus()
_focus_before_capture = null
func has_capture() -> bool:
return not _active_signature.is_empty()
func get_inspector_rect() -> Rect2:
if not has_capture() or _inspector == null or not is_instance_valid(_inspector):
return Rect2()
return _inspector.get_global_rect()
func _build_controls() -> void:
_inspector_scrim = Panel.new()
_inspector_scrim.name = "InspectorInputBlocker"
_inspector_scrim.mouse_filter = Control.MOUSE_FILTER_STOP
_inspector_scrim.focus_mode = Control.FOCUS_NONE
_inspector_scrim.gui_input.connect(_on_capture_area_gui_input)
var scrim_style: StyleBoxFlat = StyleBoxFlat.new()
scrim_style.bg_color = Color(0.015, 0.02, 0.035, 0.56)
scrim_style.set_corner_radius_all(_integer("stage_panels.shell.corner_radius", 12, 8, 32))
_inspector_scrim.add_theme_stylebox_override("panel", scrim_style)
add_child(_inspector_scrim)
_capture_strip = PanelContainer.new()
_capture_strip.name = "CaptureCancelStrip"
_capture_strip.mouse_filter = Control.MOUSE_FILTER_STOP
_capture_strip.gui_input.connect(_on_capture_area_gui_input)
_capture_strip.add_theme_stylebox_override("panel", _strip_style())
add_child(_capture_strip)
var margin: MarginContainer = MarginContainer.new()
margin.add_theme_constant_override("margin_left", 10)
margin.add_theme_constant_override("margin_right", 10)
margin.add_theme_constant_override("margin_top", 7)
margin.add_theme_constant_override("margin_bottom", 7)
margin.mouse_filter = Control.MOUSE_FILTER_IGNORE
_capture_strip.add_child(margin)
_content_column = VBoxContainer.new()
_content_column.name = "CaptureContent"
_content_column.add_theme_constant_override("separation", 4)
_content_column.mouse_filter = Control.MOUSE_FILTER_IGNORE
margin.add_child(_content_column)
_title_row = HBoxContainer.new()
_title_row.name = "CaptureTitleRow"
_title_row.add_theme_constant_override("separation", 8)
_title_row.alignment = BoxContainer.ALIGNMENT_CENTER
_title_row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_title_row.mouse_filter = Control.MOUSE_FILTER_IGNORE
_content_column.add_child(_title_row)
var marker: Label = Label.new()
marker.text = "⊕"
marker.add_theme_font_size_override("font_size", 25)
marker.add_theme_color_override("font_color", Color("#22C6FF"))
marker.mouse_filter = Control.MOUSE_FILTER_IGNORE
_title_row.add_child(marker)
_caption = Label.new()
_caption.name = "CapturePrompt"
_caption.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_caption.text = "Choosing a target"
_caption.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_caption.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
_caption.clip_text = true
_caption.custom_minimum_size.y = 48.0
_caption_base_font_size = _integer("stage_panels.type.body_size", 17, 14, 28)
_caption.add_theme_font_size_override("font_size", _caption_base_font_size)
_caption.add_theme_color_override("font_color", _color("stage_panels.type.text_color", Color("#F5F7FA")))
_caption.size_flags_horizontal = Control.SIZE_EXPAND_FILL | Control.SIZE_SHRINK_BEGIN
_caption.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_caption.mouse_filter = Control.MOUSE_FILTER_IGNORE
_title_row.add_child(_caption)
_actions_row = HBoxContainer.new()
_actions_row.name = "CaptureActions"
_actions_row.add_theme_constant_override("separation", 6)
_actions_row.alignment = BoxContainer.ALIGNMENT_END
_actions_row.size_flags_horizontal = Control.SIZE_SHRINK_END
_actions_row.mouse_filter = Control.MOUSE_FILTER_IGNORE
_title_row.add_child(_actions_row)
func _set_stacked_actions(stacked: bool) -> void:
if _actions_row == null or _content_column == null or _title_row == null:
return
var target_parent: Node = _content_column if stacked else _title_row
if _actions_row.get_parent() != target_parent:
_actions_row.reparent(target_parent)
_actions_row.size_flags_horizontal = Control.SIZE_EXPAND_FILL if stacked else Control.SIZE_SHRINK_END
_actions_row.alignment = BoxContainer.ALIGNMENT_END
func _rebuild_actions(actions: Array[Dictionary]) -> void:
for button: Button in _action_buttons:
button.queue_free()
_action_buttons.clear()
for action: Dictionary in actions:
var action_id: StringName = StringName(String(action.get("id", "")))
if action_id.is_empty():
continue
var button: Button = Button.new()
button.name = "Capture_%s" % String(action_id)
button.text = String(action.get("label", "Cancel"))
button.tooltip_text = button.text
button.focus_mode = Control.FOCUS_ALL
button.custom_minimum_size = Vector2(CAPTURE_CANCEL_WIDTH if action_id == &"cancel_capture" else CAPTURE_ACTION_WIDTH, CAPTURE_BUTTON_HEIGHT)
button.add_theme_font_size_override("font_size", _integer("stage_panels.action.label_size", 16, 14, 24))
_style_action_button(button, action_id == &"cancel_capture")
button.pressed.connect(_on_action_pressed.bind(action_id))
_actions_row.add_child(button)
_action_buttons.append(button)
_trap_action_focus()
func _trap_action_focus() -> void:
for index: int in _action_buttons.size():
var button: Button = _action_buttons[index]
button.set_focus_neighbor(SIDE_TOP, button.get_path())
button.set_focus_neighbor(SIDE_BOTTOM, button.get_path())
button.set_focus_neighbor(SIDE_LEFT, _action_buttons[maxi(0, index - 1)].get_path())
button.set_focus_neighbor(SIDE_RIGHT, _action_buttons[mini(_action_buttons.size() - 1, index + 1)].get_path())
func _on_action_pressed(action_id: StringName) -> void:
capture_action_requested.emit(action_id)
func _on_capture_area_gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
var mouse_button: InputEventMouseButton = event as InputEventMouseButton
if mouse_button.pressed and mouse_button.button_index == MOUSE_BUTTON_RIGHT:
capture_action_requested.emit(&"cancel_capture")
accept_event()
func _layout_capture_overlay() -> void:
if not has_capture() or _inspector == null or not is_instance_valid(_inspector) or not _inspector.is_visible_in_tree():
hide_capture()
return
var viewport_rect: Rect2 = get_viewport().get_visible_rect()
var inspector_rect: Rect2 = _inspector.get_global_rect().intersection(viewport_rect)
if not inspector_rect.has_area():
hide_capture()
return
var caption_font_size: int = mini(_caption_base_font_size, 16) if inspector_rect.size.x < 340.0 else _caption_base_font_size
_caption.add_theme_font_size_override("font_size", caption_font_size)
_inspector_scrim.position = inspector_rect.position
_inspector_scrim.size = inspector_rect.size
_capture_strip.position = inspector_rect.position
var strip_height: float = CAPTURE_MULTI_ACTION_STRIP_HEIGHT if _actions_row.get_parent() == _content_column else CAPTURE_STRIP_HEIGHT
_capture_strip.size = Vector2(inspector_rect.size.x, strip_height)
_capture_strip.custom_minimum_size = Vector2(inspector_rect.size.x, strip_height)
func _strip_style() -> StyleBoxFlat:
var style: StyleBoxFlat = StyleBoxFlat.new()
style.bg_color = _color("stage_panels.shell.surface_color", Color("#202634"))
style.border_color = _color("mode_colors.direct", Color("#FFB300"))
style.set_border_width_all(2)
style.set_corner_radius_all(_integer("stage_panels.shell.corner_radius", 12, 8, 32))
style.content_margin_left = 8.0
style.content_margin_right = 8.0
style.content_margin_top = 4.0
style.content_margin_bottom = 4.0
return style
func _style_action_button(button: Button, is_cancel: bool) -> void:
var normal: Color = Color("#B83A43") if is_cancel else _color("stage_panels.action.surface_color", Color("#171B24"))
var hover: Color = Color("#CF4B54") if is_cancel else _color("stage_panels.action.hover_surface", Color("#303A4B"))
var pressed: Color = Color("#96323A") if is_cancel else _color("stage_panels.action.selected_surface", Color("#12394A"))
var border: Color = Color("#FF8790") if is_cancel else _color("stage_panels.choice.border_color", Color("#FFFFFF55"))
button.add_theme_stylebox_override("normal", _button_style(normal, border, 2))
button.add_theme_stylebox_override("hover", _button_style(hover, border.lightened(0.12), 2))
button.add_theme_stylebox_override("pressed", _button_style(pressed, border, 2))
button.add_theme_stylebox_override("focus", _button_style(normal, Color("#8BD9FF"), 3))
button.add_theme_color_override("font_color", Color("#FFFFFF"))
button.add_theme_color_override("font_hover_color", Color("#FFFFFF"))
button.add_theme_color_override("font_pressed_color", Color("#FFFFFF"))
button.add_theme_color_override("font_focus_color", Color("#FFFFFF"))
func _button_style(background: Color, border: Color, border_width: int) -> StyleBoxFlat:
var style: StyleBoxFlat = StyleBoxFlat.new()
style.bg_color = background
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 = 10.0
style.content_margin_right = 10.0
style.content_margin_top = 5.0
style.content_margin_bottom = 5.0
return style
func _color(path: String, fallback: Color) -> Color:
return _stage_theme.color(path, fallback) if _stage_theme != null else fallback
func _integer(path: String, fallback: int, minimum: int, maximum: int) -> int:
return _stage_theme.integer(path, fallback, minimum, maximum) if _stage_theme != null else fallback
@@ -0,0 +1 @@
uid://71weltw84wr
+56 -2
View File
@@ -37,6 +37,7 @@ var waypoint_context: PopupMenu
var confirm_dialog: ConfirmationDialog
var queue_panel: QueuePanel
var action_editor: ActionEditor
var action_editor_backdrop: ColorRect
var speech_composer: Control
var speech_style_toolbar: Control
var speech_show_toolbar: Control
@@ -181,7 +182,7 @@ func build_context_and_editor_views(
action_size: Vector2i
) -> void:
rig_context_popup = PopupMenu.new()
rig_context_popup.add_item("📋 Edit Queue…", 0)
rig_context_popup.add_item("📋 Step Stack…", 0)
rig_context_popup.add_item("⚡ Edit Rules…", 1)
parent.add_child(rig_context_popup)
@@ -202,11 +203,29 @@ func build_context_and_editor_views(
queue_panel.apply_font(font, emoji_font, font_sizes)
queue_panel.min_size = queue_size
action_editor_backdrop = ColorRect.new()
action_editor_backdrop.name = "ActionEditorBackdrop"
action_editor_backdrop.color = _sandbox_theme.color("popups.confirmation.backdrop_color", Color("#080D15B8")) if _sandbox_theme != null else Color("#080D15B8")
action_editor_backdrop.mouse_filter = Control.MOUSE_FILTER_STOP
action_editor_backdrop.z_index = 100
action_editor_backdrop.z_as_relative = false
action_editor_backdrop.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
action_editor_backdrop.visible = false
parent.add_child(action_editor_backdrop)
action_editor = ACTION_EDITOR_SCENE.instantiate() as ActionEditor
parent.add_child(action_editor)
action_editor.apply_font(font, emoji_font, font_sizes)
action_editor.apply_theme(_sandbox_theme)
action_editor.min_size = action_size
action_editor.set_preferred_size(action_size)
action_editor.about_to_popup.connect(func() -> void:
if is_instance_valid(action_editor_backdrop):
action_editor_backdrop.show()
)
action_editor.popup_hide.connect(func() -> void:
if is_instance_valid(action_editor_backdrop):
action_editor_backdrop.hide()
)
speech_composer = SPEECH_COMPOSER_SCENE.instantiate() as Control
parent.add_child(speech_composer)
@@ -394,6 +413,16 @@ func show_objects_here(
popup.window_input.connect(_on_objects_here_window_input.bind(popup))
popup.position = position
popup.popup()
popup.size_changed.connect(_fit_objects_here_popup_after_resize.bind(
popup,
pointer_position,
viewport_size,
avoid_rects.duplicate(),
edge_inset,
safe_top,
safe_bottom
))
_fit_objects_here_popup_after_resize(popup, pointer_position, viewport_size, avoid_rects, edge_inset, safe_top, safe_bottom)
cancel.call_deferred("grab_focus")
@@ -463,6 +492,31 @@ func _objects_here_popup_position(
return best_position
func _fit_objects_here_popup_after_resize(
popup: PopupPanel,
pointer_position: Vector2,
viewport_size: Vector2,
avoid_rects: Array[Rect2],
edge_inset: int,
safe_top: int,
safe_bottom: int
) -> void:
if popup == null or not is_instance_valid(popup) or not popup.visible:
return
var actual_size: Vector2i = popup.size
var safe_position: Vector2i = _objects_here_popup_position(
pointer_position,
viewport_size,
actual_size,
avoid_rects,
edge_inset,
safe_top,
safe_bottom
)
if popup.position != safe_position:
popup.position = safe_position
func _style_objects_here_candidate(button: Button, selected: bool) -> void:
if button == null:
return
+11 -13
View File
@@ -120,8 +120,8 @@ func collect(
entry["stage_name"] = rig.stage_name
var facing_profile: int = int(authored_state.get("facing_profile", rig.get_facing_profile()))
entry["facing_profile"] = StickmanRig.FacingProfile.keys()[facing_profile]
var queue_result: Dictionary = STAGE_SCENE_IO.encode_queue(rig.get_queue())
entry["action_queue"] = queue_result["actions"]
var plan_result: Dictionary = STAGE_SCENE_IO.encode_plan(rig.get_action_plan())
entry["action_plan"] = plan_result["steps"]
if type == "stickman" and entry.has("stk_path"):
entry["stk_path"] = CONTENT_PATHS.canonicalize(String(entry.get("stk_path", "")))
elif type == "prop" and String(entry.get("asset_id", "")).ends_with(".prop"):
@@ -284,18 +284,16 @@ func apply(data: Dictionary, world: Node2D, spawner: StageSpawner) -> Dictionary
var facing_index: int = facing_names.find(String(entry.get("facing_profile", "FORWARD")))
if facing_index >= 0:
(node as StickmanRig).set_facing_profile(facing_index)
var raw_queue: Variant = entry.get("action_queue", [])
if raw_queue is Array:
var queue: Dictionary = STAGE_SCENE_IO.decode_queue(raw_queue as Array)
for warning: String in queue.get("warnings", []):
var character_name: String = String(entry.get("stk_path", "")).get_file()
if character_name.is_empty():
character_name = file_id
warnings.append("%s: %s Open this character's queue to review the skipped action." % [character_name, warning])
for action: Dictionary in queue["actions"]:
(node as StickmanRig).queue_action(action)
var raw_plan: Variant = entry.get("action_plan", [])
if raw_plan is Array:
var decoded_plan: Dictionary = STAGE_SCENE_IO.decode_plan(raw_plan as Array)
if bool(decoded_plan.get("ok", false)):
if not (node as StickmanRig).set_action_plan(decoded_plan.get("steps", [])):
warnings.append("%s: the action plan could not be restored." % String(entry.get("stage_name", file_id)))
else:
warnings.append("%s: %s" % [String(entry.get("stage_name", file_id)), String(decoded_plan.get("error", "Invalid action plan."))])
else:
warnings.append("%s: invalid action queue. Open this character's queue and review its actions." % file_id)
warnings.append("%s: invalid action plan." % String(entry.get("stage_name", file_id)))
(node as StickmanRig).reset_collision_proxy_velocity_baselines()
loaded_nodes.append(node)
if node is TerrainBlock:
@@ -4,7 +4,7 @@ extends RefCounted
const STICKMAN_ACTION_RUNNER := preload("res://scripts/characters/stickman/stickman_action_runner.gd")
enum Kind { NONE, WAYPOINT, AREA, PROP, STICKMAN, POSITION, THROW_POSITION, SET_DOWN_POSITION, SPOT_ADD, SPOT_MOVE, USE_PROP, USE_SPOT }
enum Kind { NONE, WAYPOINT, AREA, PROP, STICKMAN, POSITION, THROW_POSITION, SET_DOWN_POSITION, PLAN_PLACE, PLAN_PROP, PICKUP_PROP, SPOT_ADD, SPOT_MOVE, USE_PROP, USE_SPOT }
var kind: int = Kind.NONE
var hint: String = ""
@@ -31,6 +31,14 @@ var use_target_preview_prefix: Array[Dictionary] = []
var use_target_prefix_known: bool = false
static func normalize_plan_destination(value: Variant) -> Dictionary:
if value is Vector2:
return {"kind": "place", "position": value}
if value is Dictionary:
return (value as Dictionary).duplicate(true)
return {}
func is_stage_custom_capture() -> bool:
return kind in [Kind.SPOT_ADD, Kind.SPOT_MOVE, Kind.USE_PROP, Kind.USE_SPOT]
@@ -152,10 +160,40 @@ func resolve_target(world_pos: Vector2, selection: StageSelection, director_visu
var prop_hit: Node2D = selection.hit_test(world_pos)
if prop_hit is PropBlock:
value = prop_hit
Kind.PICKUP_PROP:
var pickup_hit: PropBlock = null
var ineligible_hit: PropBlock = null
for candidate: Node2D in selection.hit_test_all(world_pos):
if candidate is PropBlock and (candidate as PropBlock).can_be_picked_up():
pickup_hit = candidate as PropBlock
break
elif candidate is PropBlock and ineligible_hit == null:
ineligible_hit = candidate as PropBlock
if pickup_hit == null:
var reason: String = "Click a highlighted prop to pick it up."
if ineligible_hit != null:
reason = "This prop needs pickup enabled and a grip in Edit."
return {"valid": false, "value": null, "reason": reason}
value = pickup_hit
Kind.STICKMAN:
var stickman_hit: Node2D = selection.hit_test(world_pos)
if stickman_hit is StickmanRig:
value = stickman_hit
Kind.PLAN_PROP:
var plan_prop_hit: Node2D = selection.hit_test(world_pos)
if plan_prop_hit is PropBlock:
var prop: PropBlock = plan_prop_hit as PropBlock
var approach: Vector2 = world_pos
value = {
"kind": "prop",
"target_prop_id": prop.stage_object_id,
"approach_offset": approach - prop.global_position,
"last_known_approach": approach,
}
Kind.PLAN_PLACE:
var place_hit: Node2D = selection.hit_test(world_pos)
if place_hit == null or (not place_hit is PropBlock and not place_hit is StickmanRig and not place_hit is TriggerArea):
value = _snap_to_grid(world_pos, snap_enabled, grid_size)
Kind.POSITION, Kind.THROW_POSITION:
value = _snap_to_grid(world_pos, snap_enabled, grid_size)
Kind.SET_DOWN_POSITION:
@@ -302,11 +302,16 @@ func _draw_pill(center: Vector2, text_value: String, accent: Color) -> void:
if text_value.is_empty():
return
var font: Font = ThemeDB.fallback_font
var text_size: Vector2 = font.get_string_size(text_value, HORIZONTAL_ALIGNMENT_LEFT, -1, 16)
var rect := Rect2(center + Vector2(-text_size.x * 0.5 - _pill_padding, -13.0), Vector2(text_size.x + _pill_padding * 2.0, 26.0))
var lines: PackedStringArray = text_value.split("\n", false)
var text_width: float = 0.0
for line: String in lines:
text_width = maxf(text_width, font.get_string_size(line, HORIZONTAL_ALIGNMENT_LEFT, -1, 16).x)
var rect_size: Vector2 = Vector2(text_width + _pill_padding * 2.0, maxf(26.0, float(lines.size()) * 18.0 + 8.0))
var rect := Rect2(center - rect_size * 0.5, rect_size)
draw_rect(rect, Color("#171B24"), true)
draw_rect(rect, accent, false, 2.0)
draw_string(font, rect.position + Vector2(10.0, 18.0), text_value, HORIZONTAL_ALIGNMENT_LEFT, -1, 16, Color.WHITE)
for index: int in lines.size():
draw_string(font, rect.position + Vector2(10.0, 17.0 + index * 18.0), lines[index], HORIZONTAL_ALIGNMENT_LEFT, -1, 16, Color.WHITE)
func _draw_reach_marker(center: Vector2, glyph: String, accent: Color) -> void:
@@ -7,6 +7,7 @@ signal change_prop_requested
signal capture_action_requested(action_id: StringName)
const MAX_BAR_WIDTH: float = 800.0
const PLAN_CAPTURE_MAX_WIDTH: float = 440.0
const SCENERY_CAPTURE_MAX_WIDTH: float = 900.0
const VIEWPORT_WIDTH_RATIO: float = 0.68
const HORIZONTAL_LAYOUT_MIN_WIDTH: float = 760.0
@@ -53,6 +54,8 @@ var _safe_left: float = 0.0
var _safe_right: float = 0.0
var _scenery_edit_active: bool = false
var _scenery_capture_active: bool = false
var _plan_capture_active: bool = false
var _plan_target_cancel_only: bool = false
func configure(stage_theme: SandboxTheme, top_bar_height: float) -> void:
@@ -73,6 +76,9 @@ func _ready() -> void:
func set_capture_state(prop_selected: bool, prop_has_use_spots: bool = true) -> void:
if _prompt == null:
return
_plan_capture_active = false
_plan_target_cancel_only = false
_prompt.visible = true
_show_standard_capture_layout()
_clear_custom_actions()
if not prop_selected:
@@ -88,10 +94,20 @@ func set_capture_state(prop_selected: bool, prop_has_use_spots: bool = true) ->
func set_capture_actions(prompt: String, actions: Array[Dictionary]) -> void:
_plan_capture_active = true
_plan_target_cancel_only = true
_set_capture_actions(prompt, actions, false)
func set_target_capture_actions(prompt: String, actions: Array[Dictionary]) -> void:
_plan_capture_active = false
_plan_target_cancel_only = false
_set_capture_actions(prompt, actions, false)
func set_scenery_capture_actions(prompt: String, actions: Array[Dictionary]) -> void:
_plan_capture_active = false
_plan_target_cancel_only = false
_set_capture_actions(prompt, actions, true)
@@ -100,6 +116,7 @@ func _set_capture_actions(prompt: String, actions: Array[Dictionary], scenery_ca
return
_show_standard_capture_layout()
_prompt.text = prompt
_prompt.visible = not _plan_target_cancel_only
_change_prop_button.visible = false
_cancel_button.visible = false
_clear_custom_actions()
@@ -160,6 +177,9 @@ func set_scenery_edit_state(selected_count: int) -> void:
return
_scenery_capture_active = false
_scenery_edit_active = true
_plan_capture_active = false
_plan_target_cancel_only = false
_prompt.visible = true
_clear_custom_actions()
if _active_layout.get_parent() == self:
_active_layout.visible = false
@@ -198,6 +218,9 @@ func clear_horizontal_safe_bounds() -> void:
func hide_capture() -> void:
visible = false
_plan_capture_active = false
_plan_target_cancel_only = false
_prompt.visible = true
_show_standard_capture_layout()
@@ -245,7 +268,10 @@ func apply_theme() -> void:
if body_font != null:
button.add_theme_font_override("font", body_font)
button.add_theme_font_size_override("font_size", body_size)
_style_button(button, button.name.ends_with("Keep") or button.name.ends_with("Done"))
if _plan_target_cancel_only:
_style_plan_target_cancel_button(button)
else:
_style_button(button, button.name.ends_with("Keep") or button.name.ends_with("Done"))
var button_height: float = maxf(DEFAULT_BUTTON_HEIGHT, float(_integer("stage_panels.action.min_target_size", 56, 48, 96)))
_change_prop_button.custom_minimum_size.y = button_height
_cancel_button.custom_minimum_size.y = button_height
@@ -628,6 +654,23 @@ func _style_button(button: Button, prominent: bool) -> void:
button.add_theme_color_override("font_focus_color", text_color)
func _style_plan_target_cancel_button(button: Button) -> void:
var accent: Color = _color("mode_colors.direct", Color("#FFB300"))
var surface: Color = _color("stage_panels.action.surface_color", Color("#171B24"))
var hover: Color = _color("stage_panels.action.hover_surface", Color("#303A4B"))
var pressed: Color = _color("stage_panels.action.selected_surface", Color("#12394A"))
var text_color: Color = _color("stage_panels.type.text_color", Color("#F5F7FA"))
button.add_theme_stylebox_override("normal", _button_style(surface, accent, 2))
button.add_theme_stylebox_override("hover", _button_style(hover, accent.lightened(0.1), 2))
button.add_theme_stylebox_override("pressed", _button_style(pressed, accent, 2))
button.add_theme_stylebox_override("hover_pressed", _button_style(hover, accent.lightened(0.1), 2))
button.add_theme_stylebox_override("focus", _button_style(surface, accent, 3))
button.add_theme_color_override("font_color", text_color)
button.add_theme_color_override("font_hover_color", text_color)
button.add_theme_color_override("font_pressed_color", text_color)
button.add_theme_color_override("font_focus_color", text_color)
func _button_style(background: Color, border: Color, border_width: int) -> StyleBoxFlat:
var style: StyleBoxFlat = StyleBoxFlat.new()
style.bg_color = background
@@ -651,6 +694,9 @@ func _layout_capture_bar() -> void:
if _scenery_capture_active:
_layout_scenery_capture_bar(viewport_size)
return
if _plan_target_cancel_only:
_layout_plan_target_cancel_button(viewport_size)
return
var inset: float = float(_integer("stage_panels.shell.edge_inset", 16, 12, 48))
var safe_left: float = inset
var safe_right: float = viewport_size.x - inset
@@ -658,25 +704,59 @@ func _layout_capture_bar() -> void:
safe_left = clampf(_safe_left, inset, maxf(inset, viewport_size.x - inset))
safe_right = clampf(_safe_right, safe_left + 1.0, maxf(safe_left + 1.0, viewport_size.x - inset))
var available_width: float = maxf(1.0, safe_right - safe_left)
var width: float = minf(MAX_BAR_WIDTH, minf(viewport_size.x * VIEWPORT_WIDTH_RATIO, available_width))
var maximum_width: float = PLAN_CAPTURE_MAX_WIDTH if _plan_capture_active else MAX_BAR_WIDTH
var width: float = minf(maximum_width, minf(viewport_size.x * VIEWPORT_WIDTH_RATIO, available_width))
var narrow_compact: bool = width < NARROW_COMPACT_WIDTH
custom_minimum_size.x = width
if _plan_capture_active:
custom_minimum_size.y = 0.0
size.x = width
clip_contents = true
_prompt.add_theme_font_size_override("font_size", 18 if narrow_compact else clampi(_integer("stage_panels.type.title_size", 24, 18, 36) + 4, 24, 34))
_prompt.max_lines_visible = 2 if narrow_compact else 3
_prompt.add_theme_font_size_override("font_size", 19 if _plan_capture_active else (18 if narrow_compact else clampi(_integer("stage_panels.type.title_size", 24, 18, 36) + 4, 24, 34)))
_prompt.max_lines_visible = 2 if _plan_capture_active or narrow_compact else 3
_set_compact_panel_style(narrow_compact)
if _plan_capture_active:
_set_plan_capture_panel_style()
var left: float = safe_left + (available_width - width) * 0.5 if not _has_horizontal_safe_bounds else safe_left
position = Vector2(left, _top_bar_height + 56.0)
var many_actions: bool = _custom_action_buttons.size() > 3
var stack_actions: bool = width < ACTION_ROW_MIN_WIDTH or (many_actions and width < 620.0)
var wrapped: bool = (_has_horizontal_safe_bounds and width < HORIZONTAL_LAYOUT_MIN_WIDTH and width >= ACTION_ROW_MIN_WIDTH) or (many_actions and not stack_actions)
_set_layout(width < HORIZONTAL_LAYOUT_MIN_WIDTH or wrapped, stack_actions, wrapped)
var stack_actions: bool = false if _plan_capture_active else (width < ACTION_ROW_MIN_WIDTH or (many_actions and width < 620.0))
var wrapped: bool = false if _plan_capture_active else ((_has_horizontal_safe_bounds and width < HORIZONTAL_LAYOUT_MIN_WIDTH and width >= ACTION_ROW_MIN_WIDTH) or (many_actions and not stack_actions))
var compact_layout: bool = false if _plan_capture_active else (width < HORIZONTAL_LAYOUT_MIN_WIDTH or wrapped)
_set_layout(compact_layout, stack_actions, wrapped)
_fit_custom_action_widths(width, wrapped)
_apply_compact_action_labels(narrow_compact)
call_deferred("_fit_height_to_content")
func _layout_plan_target_cancel_button(viewport_size: Vector2) -> void:
if _custom_action_buttons.is_empty():
return
var inset: float = float(_integer("stage_panels.shell.edge_inset", 16, 12, 48))
var safe_left: float = inset
var safe_right: float = viewport_size.x - inset
if _has_horizontal_safe_bounds:
safe_left = clampf(_safe_left, inset, maxf(inset, viewport_size.x - inset))
safe_right = clampf(_safe_right, safe_left + 1.0, maxf(safe_left + 1.0, viewport_size.x - inset))
var button: Button = _custom_action_buttons[0]
var button_height: float = maxf(DEFAULT_BUTTON_HEIGHT, float(_integer("stage_panels.action.min_target_size", 56, 48, 96)))
var available_width: float = maxf(1.0, safe_right - safe_left)
var button_width: float = minf(148.0, available_width)
button.custom_minimum_size = Vector2(button_width, button_height)
button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
button.add_theme_font_size_override("font_size", 19)
_prompt.visible = false
_set_layout(false, false)
_actions_row.alignment = BoxContainer.ALIGNMENT_BEGIN
_actions_row.add_theme_constant_override("separation", 0)
custom_minimum_size = Vector2(button_width, 0.0)
size = Vector2(button_width, button_height)
position = Vector2(safe_left, _top_bar_height + 56.0)
clip_contents = false
_set_plan_capture_panel_style()
call_deferred("_fit_height_to_content")
func _layout_scenery_edit_capture_bar(viewport_size: Vector2) -> void:
var inset: float = float(_integer("stage_panels.shell.edge_inset", 16, 12, 48))
var safe_left: float = inset
@@ -871,14 +951,19 @@ func _fit_custom_action_widths(width: float, wrapped: bool) -> void:
var button_width: float = ACTION_BUTTON_WIDTH
var body_size: int = clampi(_integer("stage_panels.type.body_size", 20, 16, 32) + 2, 20, 30)
var action_font_size: int = maxi(18, body_size - (4 if wrapped else 0))
if _plan_capture_active:
button_width = 124.0
action_font_size = 18
if width < NARROW_COMPACT_WIDTH:
button_width = 0.0
button_width = 124.0 if _plan_capture_active else 0.0
action_font_size = 16
if wrapped and not _custom_action_buttons.is_empty():
var gaps: float = float(maxi(0, _custom_action_buttons.size() - 1) * 12)
button_width = minf(ACTION_BUTTON_WIDTH, maxf(144.0, (width - 44.0 - gaps) / float(_custom_action_buttons.size())))
for button: Button in _custom_action_buttons:
button.custom_minimum_size.x = button_width
if _plan_capture_active:
button.custom_minimum_size.y = 48.0
button.add_theme_font_size_override("font_size", action_font_size)
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL if width < NARROW_COMPACT_WIDTH else Control.SIZE_FILL
@@ -918,9 +1003,32 @@ func _set_compact_panel_style(compact: bool) -> void:
panel_style.content_margin_bottom = 8.0 if compact else 14.0
func _set_plan_capture_panel_style() -> void:
if _plan_target_cancel_only:
var cancel_style: StyleBoxFlat = StyleBoxFlat.new()
cancel_style.bg_color = Color(0.0, 0.0, 0.0, 0.0)
cancel_style.set_border_width_all(0)
cancel_style.content_margin_left = 0.0
cancel_style.content_margin_right = 0.0
cancel_style.content_margin_top = 0.0
cancel_style.content_margin_bottom = 0.0
add_theme_stylebox_override("panel", cancel_style)
return
var panel_style: StyleBoxFlat = get_theme_stylebox("panel") as StyleBoxFlat
if panel_style == null:
return
panel_style.border_width_left = 4
panel_style.content_margin_left = 10.0
panel_style.content_margin_right = 10.0
panel_style.content_margin_top = 8.0
panel_style.content_margin_bottom = 8.0
func _fit_height_to_content() -> void:
if not is_inside_tree() or not visible:
return
if _plan_capture_active:
custom_minimum_size.y = 0.0
var target_height: float = _active_layout.get_combined_minimum_size().y
var panel_style: StyleBox = get_theme_stylebox("panel")
if panel_style != null: