saumwacht-godot/scripts/ui/components/choice_group.gd
Martin Stickelbroeck c2b447610c Baseline: Saumwacht 0.15.0 (Phase 20A) vor dem Umbau zu Champions of Luneraria
Unveränderter Ausgangsstand inklusive Masterprompt und Konzeptbildern
unter assets/. .godot/ und docs/screenshots/ bleiben laut .gitignore
unversioniert; letztere sind unter ../backups gesichert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 20:00:36 +02:00

97 lines
3.5 KiB
GDScript

class_name ChoiceGroup
extends BoxContainer
## Segmented choice with the OptionButton surface used by tools and tests:
## select(index) changes the display only, item_selected fires on player input.
## Selection shows a filled diamond, a bright frame and brighter text, never colour alone.
signal item_selected(index: int)
const MARK_ON := preload("res://assets/ui/controls/choice_on.svg")
const MARK_OFF := preload("res://assets/ui/controls/choice_off.svg")
var selected: int = -1
var _buttons: Array[Button] = []
var _marks: Array[TextureRect] = []
var _group := ButtonGroup.new()
func _init(horizontal: bool = true) -> void:
vertical = not horizontal
add_theme_constant_override("separation", 8)
func add_option(title: String, detail: String = "", tooltip: String = "") -> Button:
var button := Button.new()
button.theme_type_variation = "ChoiceButton"
button.toggle_mode = true
button.button_group = _group
button.focus_mode = Control.FOCUS_ALL
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
button.tooltip_text = tooltip
button.set_meta("title", title)
var row := HBoxContainer.new()
row.mouse_filter = Control.MOUSE_FILTER_IGNORE
row.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
row.offset_left = 14
row.offset_right = -12
row.add_theme_constant_override("separation", 12)
button.add_child(row)
var mark := TextureRect.new()
mark.texture = MARK_OFF
mark.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
mark.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
mark.custom_minimum_size = Vector2(16, 16)
mark.mouse_filter = Control.MOUSE_FILTER_IGNORE
mark.size_flags_vertical = Control.SIZE_SHRINK_CENTER
row.add_child(mark)
var text := VBoxContainer.new()
text.mouse_filter = Control.MOUSE_FILTER_IGNORE
text.alignment = BoxContainer.ALIGNMENT_CENTER
text.add_theme_constant_override("separation", 0)
text.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_child(text)
var heading := Label.new()
heading.text = title
heading.mouse_filter = Control.MOUSE_FILTER_IGNORE
heading.add_theme_font_size_override("font_size", 18)
text.add_child(heading)
if not detail.is_empty():
var info := Label.new()
info.text = detail
info.theme_type_variation = "MutedLabel"
info.mouse_filter = Control.MOUSE_FILTER_IGNORE
info.add_theme_font_size_override("font_size", 15)
info.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
text.add_child(info)
var base_height: float = 62.0 if not detail.is_empty() else 46.0
button.custom_minimum_size = Vector2(0, base_height)
# Buttons do not size to child controls; follow the wrapped text height instead.
row.minimum_size_changed.connect(func() -> void:
button.custom_minimum_size.y = maxf(base_height, row.get_combined_minimum_size().y + 20.0))
var index: int = _buttons.size()
button.pressed.connect(func() -> void:
if selected != index:
select(index)
item_selected.emit(index))
add_child(button)
_buttons.append(button)
_marks.append(mark)
return button
func select(index: int) -> void:
if index < 0 or index >= _buttons.size():
return
selected = index
for i: int in range(_buttons.size()):
_buttons[i].set_pressed_no_signal(i == index)
_marks[i].texture = MARK_ON if i == index else MARK_OFF
_buttons[i].set_meta("selected", i == index)
func get_item_count() -> int:
return _buttons.size()
func get_button(index: int) -> Button:
return _buttons[index]
func focus_selected() -> void:
if selected >= 0 and selected < _buttons.size():
_buttons[selected].grab_focus()