Files
EGJ25/addons/guide/ui/guide_input_formatter.gd
minimata 9a79715e47
All checks were successful
Create tag and build when new code gets to main / BumpTag (push) Successful in 6s
Create tag and build when new code gets to main / Export (push) Successful in 3m16s
feat: made sure the aspect ration fit a pixel art game and added useful addons
2025-06-27 15:19:12 +02:00

359 lines
12 KiB
GDScript

@tool
## Helper class for formatting GUIDE input for the UI.
class_name GUIDEInputFormatter
const IconMaker = preload("icon_maker/icon_maker.gd")
const KeyRenderer = preload("renderers/keyboard/key_renderer.tscn")
const MouseRenderer = preload("renderers/mouse/mouse_renderer.tscn")
const TouchRenderer = preload("renderers/touch/touch_renderer.tscn")
const JoyRenderer = preload("renderers/joy/joy_renderer.tscn")
const XboxRenderer = preload("renderers/controllers/xbox/xbox_controller_renderer.tscn")
const PlayStationRenderer = preload("renderers/controllers/playstation/playstation_controller_renderer.tscn")
const SwitchRenderer = preload("renderers/controllers/switch/switch_controller_renderer.tscn")
const ActionRenderer = preload("renderers/misc/action_renderer.tscn")
const FallbackRenderer = preload("renderers/misc/fallback_renderer.tscn")
const DefaultTextProvider = preload("text_providers/default_text_provider.gd")
const XboxTextProvider = preload("text_providers/controllers/xbox/xbox_controller_text_provider.gd")
const PlayStationTextProvider = preload("text_providers/controllers/playstation/playstation_controller_text_provider.gd")
const SwitchTextProvider = preload("text_providers/controllers/switch/switch_controller_text_provider.gd")
# These are shared across all instances
static var _icon_maker:IconMaker
static var _icon_renderers:Array[GUIDEIconRenderer] = []
static var _text_providers:Array[GUIDETextProvider] = []
static var _is_ready:bool = false
## Separator to separate mixed input.
static var mixed_input_separator:String = ", "
## Separator to separate chorded input.
static var chorded_input_separator:String = " + "
## Separator to separate combo input.
static var combo_input_separator:String = " > "
# These are per-instance
var _action_resolver:Callable
var _icon_size:int
static func _ensure_readiness():
if _is_ready:
return
# reconnect to an icon maker that might be there
var root = Engine.get_main_loop().root
for child in root.get_children():
if child is IconMaker:
_icon_maker = child
if _icon_maker == null:
_icon_maker = preload("icon_maker/icon_maker.tscn").instantiate()
root.add_child.call_deferred(_icon_maker)
add_icon_renderer(KeyRenderer.instantiate())
add_icon_renderer(MouseRenderer.instantiate())
add_icon_renderer(TouchRenderer.instantiate())
add_icon_renderer(ActionRenderer.instantiate())
add_icon_renderer(JoyRenderer.instantiate())
add_icon_renderer(XboxRenderer.instantiate())
add_icon_renderer(PlayStationRenderer.instantiate())
add_icon_renderer(SwitchRenderer.instantiate())
add_icon_renderer(FallbackRenderer.instantiate())
add_text_provider(DefaultTextProvider.new())
add_text_provider(XboxTextProvider.new())
add_text_provider(PlayStationTextProvider.new())
add_text_provider(SwitchTextProvider.new())
_is_ready = true
## This will clean up the rendering infrastructure used for generating
## icons. Note that in a normal game you will have no need to call this
## as the infrastructure is needed throughout the run of your game.
## It might be useful in tests though, to get rid of spurious warnings
## about orphaned nodes.
static func cleanup():
_is_ready = false
# free all the nodes to avoid memory leaks
for renderer in _icon_renderers:
renderer.queue_free()
_icon_renderers.clear()
_text_providers.clear()
if is_instance_valid(_icon_maker):
_icon_maker.queue_free()
func _init(icon_size:int = 32, resolver:Callable = func(action) -> GUIDEActionMapping: return null ):
_icon_size = icon_size
_action_resolver = resolver
## Adds an icon renderer for rendering icons.
static func add_icon_renderer(renderer:GUIDEIconRenderer) -> void:
_icon_renderers.append(renderer)
_icon_renderers.sort_custom(func(r1, r2): return r1.priority < r2.priority)
## Removes an icon renderer.
static func remove_icon_renderer(renderer:GUIDEIconRenderer) -> void:
_icon_renderers.erase(renderer)
## Adds a text provider for rendering text.
static func add_text_provider(provider:GUIDETextProvider) -> void:
_text_providers.append(provider)
_text_providers.sort_custom(func(r1, r2): return r1.priority < r2.priority)
## Removes a text provider
static func remove_text_provider(provider:GUIDETextProvider) -> void:
_text_providers.erase(provider)
## Returns an input formatter that can format actions using the currently active inputs.
static func for_active_contexts(icon_size:int = 32) -> GUIDEInputFormatter:
var resolver = func(action:GUIDEAction) -> GUIDEActionMapping:
for mapping in GUIDE._active_action_mappings:
if mapping.action == action:
return mapping
return null
return GUIDEInputFormatter.new(icon_size, resolver)
## Returns an input formatter that can format actions using the given context.
static func for_context(context:GUIDEMappingContext, icon_size:int = 32) -> GUIDEInputFormatter:
var resolver:Callable = func(action:GUIDEAction) -> GUIDEActionMapping:
for mapping in context.mappings:
if mapping.action == action:
return mapping
return null
return GUIDEInputFormatter.new(icon_size, resolver)
## Formats the action input as richtext with icons suitable for a RichTextLabel. This function
## is async as icons may need to be rendered in the background which can take a few frames, so
## you will need to await on it.
func action_as_richtext_async(action:GUIDEAction) -> String:
return await _materialized_as_richtext_async(_materialize_action_input(action))
## Formats the action input as plain text which can be used in any UI component. This is a bit
## more light-weight than formatting as icons and returns immediately.
func action_as_text(action:GUIDEAction) -> String:
return _materialized_as_text(_materialize_action_input(action))
## Formats the input as richtext with icons suitable for a RichTextLabel. This function
## is async as icons may need to be rendered in the background which can take a few frames, so
## you will need to await on it.
func input_as_richtext_async(input:GUIDEInput, materialize_actions:bool = true) -> String:
return await _materialized_as_richtext_async(_materialize_input(input, materialize_actions))
## Formats the input as plain text which can be used in any UI component. This is a bit
## more light-weight than formatting as icons and returns immediately.
func input_as_text(input:GUIDEInput, materialize_actions:bool = true) -> String:
return _materialized_as_text(_materialize_input(input, materialize_actions))
## Renders materialized input as text.
func _materialized_as_text(input:MaterializedInput) -> String:
_ensure_readiness()
if input is MaterializedSimpleInput:
var text:String = ""
for provider in _text_providers:
if provider.supports(input.input):
text = provider.get_text(input.input)
# first provider wins
break
if text == "":
pass
## push_warning("No formatter found for input ", input)
return text
var separator = _separator_for_input(input)
if separator == "" or input.parts.is_empty():
return ""
var parts:Array[String] = []
for part in input.parts:
parts.append(_materialized_as_text(part))
return separator.join(parts)
## Renders materialized input as rich text.
func _materialized_as_richtext_async(input:MaterializedInput) -> String:
_ensure_readiness()
if input is MaterializedSimpleInput:
var icon:Texture2D = null
for renderer in _icon_renderers:
if renderer.supports(input.input):
icon = await _icon_maker.make_icon(input.input, renderer, _icon_size)
# first renderer wins
break
if icon == null:
push_warning("No renderer found for input ", input)
return ""
return "[img]%s[/img]" % [icon.resource_path]
var separator = _separator_for_input(input)
if separator == "" or input.parts.is_empty():
return ""
var parts:Array[String] = []
for part in input.parts:
parts.append(await _materialized_as_richtext_async(part))
return separator.join(parts)
func _separator_for_input(input:MaterializedInput) -> String:
if input is MaterializedMixedInput:
return mixed_input_separator
elif input is MaterializedComboInput:
return combo_input_separator
elif input is MaterializedChordedInput:
return chorded_input_separator
push_error("Unknown materialized input type")
return ""
## Materializes action input.
func _materialize_action_input(action:GUIDEAction) -> MaterializedInput:
var result := MaterializedMixedInput.new()
if action == null:
push_warning("Trying to get inputs for a null action.")
return result
# get the mapping for this action
var mapping:GUIDEActionMapping = _action_resolver.call(action)
# if we have no mapping, well that's it, return an empty mixed input
if mapping == null:
return result
# collect input mappings
for input_mapping in mapping.input_mappings:
var chorded_actions:Array[MaterializedInput] = []
var combos:Array[MaterializedInput] = []
for trigger in input_mapping.triggers:
# if we have a combo trigger, materialize its input.
if trigger is GUIDETriggerCombo:
var combo := MaterializedComboInput.new()
for step:GUIDETriggerComboStep in trigger.steps:
combo.parts.append(_materialize_action_input(step.action))
combos.append(combo)
# if we have a chorded action, materialize its input
if trigger is GUIDETriggerChordedAction:
chorded_actions.append(_materialize_action_input(trigger.action))
if not chorded_actions.is_empty():
# if we have chorded action then the whole mapping is chorded.
var chord := MaterializedChordedInput.new()
for chorded_action in chorded_actions:
chord.parts.append(chorded_action)
for combo in combos:
chord.parts.append(combo)
if combos.is_empty():
if input_mapping.input != null:
chord.parts.append(_materialize_input(input_mapping.input))
result.parts.append(chord)
else:
for combo in combos:
result.parts.append(combo)
if combos.is_empty():
if input_mapping.input != null:
result.parts.append(_materialize_input(input_mapping.input))
return result
## Materializes direct input.
func _materialize_input(input:GUIDEInput, materialize_actions:bool = true) -> MaterializedInput:
if input == null:
push_warning("Trying to materialize a null input.")
return MaterializedMixedInput.new()
# if its an action input, get its parts
if input is GUIDEInputAction:
if materialize_actions:
return _materialize_action_input(input.action)
else:
return MaterializedSimpleInput.new(input)
# if its a key input, split out the modifiers
if input is GUIDEInputKey:
var chord := MaterializedChordedInput.new()
if input.control:
var ctrl = GUIDEInputKey.new()
ctrl.key = KEY_CTRL
chord.parts.append(MaterializedSimpleInput.new(ctrl))
if input.alt:
var alt = GUIDEInputKey.new()
alt.key = KEY_ALT
chord.parts.append(MaterializedSimpleInput.new(alt))
if input.shift:
var shift = GUIDEInputKey.new()
shift.key = KEY_SHIFT
chord.parts.append(MaterializedSimpleInput.new(shift))
if input.meta:
var meta = GUIDEInputKey.new()
meta.key = KEY_META
chord.parts.append(MaterializedSimpleInput.new(meta))
# got no modifiers?
if chord.parts.is_empty():
return MaterializedSimpleInput.new(input)
chord.parts.append(MaterializedSimpleInput.new(input))
return chord
# everything else is just a simple input
return MaterializedSimpleInput.new(input)
class MaterializedInput:
pass
class MaterializedSimpleInput:
extends MaterializedInput
var input:GUIDEInput
func _init(input:GUIDEInput):
self.input = input
class MaterializedMixedInput:
extends MaterializedInput
var parts:Array[MaterializedInput] = []
class MaterializedChordedInput:
extends MaterializedInput
var parts:Array[MaterializedInput] = []
class MaterializedComboInput:
extends MaterializedInput
var parts:Array[MaterializedInput] = []
## Returns the name of the associated joystick/pad of the given input.
## If the input is no joy input or the device name cannot be determined
## returns an empty string.
static func _joy_name_for_input(input:GUIDEInput) -> String:
if not input is GUIDEInputJoyBase:
return ""
var joypads:Array[int] = Input.get_connected_joypads()
var joy_index = input.joy_index
if joy_index < 0:
# pick the first one
joy_index = 0
# We don't have such a controller, so bail out.
if joypads.size() <= joy_index:
return ""
var id = joypads[joy_index]
return Input.get_joy_name(id)