Skip to content

Instantly share code, notes, and snippets.

@Shilo
Last active December 26, 2024 16:55
Show Gist options
  • Save Shilo/ebac01e4ef0a3e93d9dae6dffeb164fb to your computer and use it in GitHub Desktop.
Save Shilo/ebac01e4ef0a3e93d9dae6dffeb164fb to your computer and use it in GitHub Desktop.
Godot 4 more advanced version of the `TouchScreenButton` class that allows for more customization and control over the button's behavior.
/// <summary>
/// A more advanced version of the `TouchScreenButton` class that allows
/// for more customization and control over the button's behavior.
/// Example: Unhandled input processing, consuming input, circle hit box, and visual properties.
/// Fixes: Will not react to input that is handled/stopped by nodes. Will not pass input to nodes behind it.
/// TODO: Implement `PassbyPress` property.
/// </summary>
[GlobalClass, Tool]
public partial class TOLTouchScreenButton : TouchScreenButton
{
private bool _processUnhandledInput;
[Export]
public bool ProcessUnhandledInput
{
set
{
this._processUnhandledInput = value;
this.SetProcessUnhandledInput(value);
this.SetProcessInput(!value);
}
get => this._processUnhandledInput;
}
[Export]
public bool ConsumeInput;
private bool _disabled;
[Export]
public bool Disabled
{
set
{
if (this._disabled == value)
return;
this._disabled = value;
this.SelfModulate = value ? this.DisabledTint : Colors.White;
}
get => this._disabled;
}
// The amount of extra pixels to add to the button's hit box. Allows more forgiving touch input.
[Export]
public Vector2I TouchMargin;
[ExportGroup("Visual")]
[Export]
public bool IsCircle
{
set => this.ShaderMaterial?.SetShaderParameter("is_circle", value);
get => (bool)(this.ShaderMaterial?.GetShaderParameter("is_circle") ?? false);
}
[Export]
public Color PressedTint = Colors.Gray;
[Export]
public Color DisabledTint = new(1, 1, 1, 0.5f);
[Export(PropertyHint.ColorNoAlpha)]
public Color BorderColor
{
set => this.ShaderMaterial?.SetShaderParameter("border_color", value);
get => (Color)(this.ShaderMaterial?.GetShaderParameter("border_color") ?? Colors.Black);
}
[Export(PropertyHint.Range, "0, 10, 1")]
public float BorderWidth
{
set => this.ShaderMaterial?.SetShaderParameter("border_width", value);
get => (float)(this.ShaderMaterial?.GetShaderParameter("border_width") ?? 0);
}
public new event Action Pressed;
public new event Action Released;
private int _pressedIndex = -1;
private string _action;
private Color _lastModulate;
public ShaderMaterial ShaderMaterial => this.Material as ShaderMaterial;
public override void _Ready()
{
if (Engine.IsEditorHint())
return;
this.ProcessUnhandledInput = this._processUnhandledInput;
this._action = this.Action;
this.Action = "";
this.Connect(CanvasItem.SignalName.VisibilityChanged, new Callable(this, MethodName.OnVisibilityChanged),
(uint)ConnectFlags.Deferred);
}
private void OnVisibilityChanged()
{
// Reset the input processing state when the button's visibility is re-shown.
// Because godot overrides the node's ProcessUnhandledInput and ProcessInput states when re-drawing node.
if (this.Visible)
this.ProcessUnhandledInput = this._processUnhandledInput;
}
public override void _Input(InputEvent @event)
{
this.ProcessInput(@event);
}
public override void _UnhandledInput(InputEvent @event)
{
this.ProcessInput(@event);
}
private void ProcessInput(InputEvent @event)
{
if (this.Disabled || !this.Visible || @event is not InputEventScreenTouch touchEvent)
return;
if (touchEvent.Pressed && this.HasGlobalPoint(touchEvent.Position))
{
this._pressedIndex = touchEvent.Index;
if (this.ConsumeInput)
GetViewport().SetInputAsHandled();
this._lastModulate = this.SelfModulate;
this.SelfModulate = this.PressedTint;
this.Pressed?.Invoke();
this.EmitAction(true);
}
else if (!touchEvent.Pressed && this._pressedIndex == touchEvent.Index)
{
this._pressedIndex = -1;
if (this.ConsumeInput)
GetViewport().SetInputAsHandled();
this.SelfModulate = this._lastModulate;
this.Released?.Invoke();
this.EmitAction(false);
}
}
private bool HasGlobalPoint(Vector2 point)
{
if (this.TextureNormal == null)
return false;
Vector2 position = this.GlobalPosition - this.TouchMargin;
Vector2 size = this.TextureNormal.GetSize() * this.GlobalScale + this.TouchMargin * 2;
if (this.IsCircle)
{
Vector2 halfSize = size / 2;
float radius = Mathf.Min(halfSize.X, halfSize.Y);
return (point - (position + halfSize)).Length() <= radius;
}
else
{
var rect = new Rect2(position, size);
return rect.HasPoint(point);
}
}
private void EmitAction(bool pressed)
{
if (string.IsNullOrEmpty(this._action))
return;
var pressedEvent = new InputEventAction();
pressedEvent.Action = this._action;
pressedEvent.Pressed = pressed;
Input.ParseInputEvent(pressedEvent);
}
}
shader_type canvas_item;
uniform vec3 border_color: source_color = vec3(0);
uniform float border_width: hint_range(0, 10) = 0;
uniform bool is_circle = false;
void fragment() {
float center = 0.5;
float border_width_uv = border_width * min(TEXTURE_PIXEL_SIZE.x, TEXTURE_PIXEL_SIZE.y);
float border_start = center - border_width_uv;
if (is_circle) {
if (step(border_start, length(UV - vec2(center, center))) > 0.5) {
COLOR.rgb = border_color;
}
COLOR.a *= step(length(UV - vec2(center, center)), center);
} else {
if (step(border_start, abs(UV.x - center)) > 0.5 ||
step(border_start, abs(UV.y - center)) > 0.5) {
COLOR.rgb = border_color;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment