7const DEFAULT_OPTIONS := {
12 "button_size": Vector2(72, 64),
15 "fill_parent_height": false,
18 {
"id":
"up",
"label":
"Up",
"action":
"move_up"},
19 {
"id":
"down",
"label":
"Down",
"action":
"move_down"}
24var options := DEFAULT_OPTIONS.duplicate(true)
27var queued_presses := []
28var overlays_created:int = 0
29var visibility_changes:int = 0
32func attach(parent:Control, next_options:Dictionary = {}) -> Control:
33 if overlay != null
and is_instance_valid(overlay)
and overlay.get_parent() != null:
34 overlay.get_parent().remove_child(overlay)
35 overlay = create_overlay(next_options)
36 parent.add_child(overlay)
40func create_overlay(next_options:Dictionary = {}) -> Control:
41 options = DEFAULT_OPTIONS.duplicate(true)
42 for key
in next_options.keys():
43 options[key] = next_options[key]
46 queued_presses.clear()
47 var root = Control.new()
49 root.name =
"OnscreenControls"
50 root.mouse_filter = Control.MOUSE_FILTER_PASS
51 root.set_anchors_preset(Control.PRESET_FULL_RECT)
52 root.visible = bool(options.get(
"visible", true))
55 if str(options.get(
"layout",
"vertical")) ==
"dpad":
56 pad = GridContainer.new()
59 pad = VBoxContainer.new()
61 pad.mouse_filter = Control.MOUSE_FILTER_PASS
62 pad.add_theme_constant_override(
"separation",
int(options.get(
"gap", 10.0)))
64 _apply_pad_layout(pad)
66 if str(options.get(
"layout",
"vertical")) ==
"dpad":
69 for control
in options.get(
"controls", []):
70 var button = _make_button(control)
75func set_visible(enabled:bool) -> void:
76 if is_visible() == enabled
and bool(options.get(
"visible", true)) == enabled:
78 options[
"visible"] = enabled
79 visibility_changes += 1
80 if overlay != null
and is_instance_valid(overlay):
81 overlay.visible = enabled
84 queued_presses.clear()
87func is_visible() -> bool:
88 if overlay == null
or not is_instance_valid(overlay):
89 return bool(options.get(
"visible", true))
90 return overlay.visible
93func get_axis(negative_id:String =
"up", positive_id:String =
"down") -> float:
95 if bool(pressed.get(negative_id, false)):
97 if bool(pressed.get(positive_id, false)):
102func get_vector(left_id:String =
"left", right_id:String =
"right", up_id:String =
"up", down_id:String =
"down") -> Vector2:
104 get_axis(left_id, right_id),
105 get_axis(up_id, down_id)
111func consume_vector(left_id:String =
"left", right_id:String =
"right", up_id:String =
"up", down_id:String =
"down") -> Vector2:
112 while not queued_presses.is_empty():
113 var control_id = str(queued_presses.pop_front())
114 if control_id == left_id:
116 if control_id == right_id:
118 if control_id == up_id:
120 if control_id == down_id:
125func get_state() -> Dictionary:
127 "visible": bool(options.get(
"visible", true)),
128 "side": str(options.get(
"side",
"left")),
129 "anchor_y": float(options.get(
"anchor_y", 0.5)),
130 "margin": float(options.get(
"margin", 14.0)),
131 "button_size": options.get(
"button_size", Vector2(72, 64)),
132 "gap": float(options.get(
"gap", 10.0)),
133 "opacity": float(options.get(
"opacity", 0.34)),
134 "fill_parent_height": bool(options.get(
"fill_parent_height", false)),
135 "layout": str(options.get(
"layout",
"vertical")),
136 "controls": options.get(
"controls", [])
141func get_stats() -> Dictionary:
143 "overlays_created": overlays_created,
144 "buttons": buttons.size(),
145 "queued_presses": queued_presses.size(),
146 "visibility_changes": visibility_changes,
147 "visible": is_visible()
151func get_cache_stats() -> Dictionary:
155func apply_state(state:Dictionary) -> void:
156 for key
in state.keys():
157 options[key] = state[key]
158 if overlay != null
and is_instance_valid(overlay):
159 overlay.visible = bool(options.get(
"visible", true))
160 var pad = overlay.get_node_or_null(
"Pad")
162 _apply_pad_layout(pad)
163 for child
in pad.get_children():
168func set_layout(next_options:Dictionary) -> void:
169 apply_state(next_options)
172func _make_button(control:Dictionary) -> Button:
173 var control_id = str(control.get(
"id",
"control"))
174 pressed[control_id] = false
175 var button = Button.new()
176 button.name =
"%sButton" % control_id.capitalize()
177 button.text = str(control.get(
"label", control_id.capitalize()))
178 button.focus_mode = Control.FOCUS_NONE
179 button.custom_minimum_size = options.get(
"button_size", Vector2(72, 64))
180 if bool(options.get(
"fill_parent_height", false)):
181 button.custom_minimum_size.y = 0
182 button.size_flags_vertical = Control.SIZE_EXPAND_FILL
183 button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
184 button.mouse_filter = Control.MOUSE_FILTER_STOP
185 buttons[control_id] = button
186 button.button_down.connect(func():
187 pressed[control_id] = true
188 queued_presses.append(control_id)
190 button.button_up.connect(func(): pressed[control_id] = false)
191 button.visibility_changed.connect(func():
192 if not button.visible:
193 pressed[control_id] = false
195 _style_button(button)
199func _build_dpad(pad:Control) -> void:
200 var controls_by_id := {}
201 for control
in options.get(
"controls", []):
202 controls_by_id[str(control.get(
"id",
""))] = control
204 {}, controls_by_id.get(
"up", {
"id":
"up",
"label":
"Up"}), {},
205 controls_by_id.get(
"left", {
"id":
"left",
"label":
"Left"}), {}, controls_by_id.get(
"right", {
"id":
"right",
"label":
"Right"}),
206 {}, controls_by_id.get(
"down", {
"id":
"down",
"label":
"Down"}), {}
210 var spacer = Control.new()
211 spacer.custom_minimum_size = options.get(
"button_size", Vector2(72, 64))
212 pad.add_child(spacer)
214 pad.add_child(_make_button(cell))
217func _apply_pad_layout(pad:Control) -> void:
218 var button_size = options.get(
"button_size", Vector2(72, 64))
219 var controls_count = max(1, options.get(
"controls", []).size())
220 var is_dpad = str(options.get(
"layout",
"vertical")) ==
"dpad"
221 var width = float(button_size.x)
222 var height = float(button_size.y) * controls_count + float(options.get(
"gap", 10.0)) * max(0, controls_count - 1)
224 width = float(button_size.x) * 3.0 + float(options.get(
"gap", 10.0)) * 2.0
225 height = float(button_size.y) * 3.0 + float(options.get(
"gap", 10.0)) * 2.0
226 var margin = float(options.get(
"margin", 14.0))
227 var anchor_y = clamp(float(options.get(
"anchor_y", 0.5)), 0.0, 1.0)
228 if bool(options.get(
"fill_parent_height", false)):
229 pad.custom_minimum_size = Vector2(width, 0)
231 pad.anchor_bottom = 1.0
232 pad.offset_top = margin
233 pad.offset_bottom = -margin
235 pad.custom_minimum_size = Vector2(width, height)
236 pad.anchor_top = anchor_y
237 pad.anchor_bottom = anchor_y
238 pad.offset_top = -height * 0.5
239 pad.offset_bottom = height * 0.5
240 if str(options.get(
"side",
"left")) ==
"right":
241 pad.anchor_left = 1.0
242 pad.anchor_right = 1.0
243 pad.offset_left = -width - margin
244 pad.offset_right = -margin
246 pad.anchor_left = 0.0
247 pad.anchor_right = 0.0
248 pad.offset_left = margin
249 pad.offset_right = margin + width
252func _style_button(button:Button) -> void:
253 var opacity = clamp(float(options.get(
"opacity", 0.34)), 0.05, 0.85)
254 button.self_modulate = Color(1, 1, 1, opacity)
255 var normal = _make_style(Color(0.08, 0.13, 0.18, 1.0), Color(1.0, 1.0, 1.0, 0.55))
256 var hover = _make_style(Color(0.12, 0.20, 0.28, 1.0), Color(1.0, 1.0, 1.0, 0.7))
257 var pressed_style = _make_style(Color(0.22, 0.42, 0.58, 1.0), Color(1.0, 1.0, 1.0, 0.85))
258 button.add_theme_stylebox_override(
"normal", normal)
259 button.add_theme_stylebox_override(
"hover", hover)
260 button.add_theme_stylebox_override(
"pressed", pressed_style)
261 button.add_theme_stylebox_override(
"focus", hover)
262 button.add_theme_stylebox_override(
"disabled", normal)
263 button.add_theme_color_override(
"font_color", Color(1, 1, 1, 0.92))
264 button.add_theme_color_override(
"font_hover_color", Color.WHITE)
265 button.add_theme_color_override(
"font_pressed_color", Color.WHITE)
266 button.add_theme_font_size_override(
"font_size", 18)
269func _make_style(fill:Color, border:Color) -> StyleBoxFlat:
270 var style = StyleBoxFlat.new()
271 style.bg_color = fill
272 style.border_color = border
273 style.draw_center = true
274 style.set_border_width_all(1)
275 style.set_corner_radius_all(8)
276 style.content_margin_left = 4
277 style.content_margin_right = 4
278 style.content_margin_top = 4
279 style.content_margin_bottom = 4