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()