Files
mtgodot-poc/project/ui/mobile/mobile_touch_button.gd
T

227 lines
6.6 KiB
GDScript

# MobileTouchButton —— a mouse and screen-touch friendly action surface.
#
# BaseButton's mouse emulation is intentionally disabled in project.godot, so
# the mobile layer owns the small amount of press/release handling it needs.
# This keeps a touch index alive until release and gives gameplay a reliable
# cancellation path when the app loses focus.
extends Control
signal pressed
signal released
signal press_state(down: bool)
signal long_pressed
signal drag_started
signal drag_changed(delta: Vector2)
signal drag_released(delta: Vector2)
var label_text := ""
var accent := Color(0.36, 0.64, 0.95, 0.95)
var idle_modulate := Color.WHITE
var circular := true
var active := false
var _touch_index := -1
var _caption: Label
var _press_position := Vector2.ZERO
var _last_position := Vector2.ZERO
var _drag_delta := Vector2.ZERO
var _long_press_token := 0
var _long_pressed := false
var _dragging := false
var _aiming := false
const LONG_PRESS_SECONDS := 0.55
const DRAG_THRESHOLD := 10.0
func setup(text: String, color: Color = Color(0.36, 0.64, 0.95, 0.95)) -> void:
label_text = text
accent = color
mouse_filter = Control.MOUSE_FILTER_STOP
custom_minimum_size = Vector2(56, 56)
_caption = Label.new()
_caption.text = label_text
_caption.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_caption.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_caption.set_anchors_preset(Control.PRESET_FULL_RECT)
_caption.mouse_filter = Control.MOUSE_FILTER_IGNORE
_caption.add_theme_font_size_override("font_size", 13)
_caption.add_theme_color_override("font_color", Color(0.94, 0.97, 1.0))
_caption.add_theme_color_override("font_outline_color", Color(0.02, 0.03, 0.05, 0.95))
_caption.add_theme_constant_override("outline_size", 3)
add_child(_caption)
queue_redraw()
func set_caption(text: String) -> void:
label_text = text
if _caption:
_caption.text = text
func set_idle_modulate(value: Color) -> void:
idle_modulate = value
modulate = value if not active else Color(0.82, 0.9, 1.0, value.a)
## Enables the long-press/drag gesture used by mobile skill aiming and
## inventory movement. The ordinary pressed/released signals remain intact
## for buttons that do not need a gesture.
func set_gesture_enabled(enabled: bool) -> void:
set_meta("gesture_enabled", enabled)
func was_long_pressed() -> bool:
return _long_pressed
func is_dragging() -> bool:
return _dragging
func drag_total() -> Vector2:
return _drag_delta
func drag_end_position() -> Vector2:
return _last_position
func set_aiming(value: bool) -> void:
_aiming = value
queue_redraw()
func set_rect_style() -> void:
circular = false
queue_redraw()
func _ready() -> void:
set_process_unhandled_input(true)
queue_redraw()
func _gui_input(event: InputEvent) -> void:
if event is InputEventScreenTouch:
if event.pressed:
if _touch_index < 0:
_touch_index = event.index
_begin_press(_event_position(event))
else:
if event.index == _touch_index:
_touch_index = -1
_end_press(_event_position(event))
accept_event()
return
if event is InputEventScreenDrag and event.index == _touch_index:
_update_drag(_event_position(event))
accept_event()
return
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
if event.pressed:
_touch_index = -2
_begin_press(_event_position(event))
else:
_touch_index = -1
_end_press(_event_position(event))
accept_event()
return
if event is InputEventMouseMotion and _touch_index == -2:
_update_drag(_event_position(event))
accept_event()
func _event_position(event: InputEvent) -> Vector2:
if event is InputEventMouse:
return (event as InputEventMouse).position
if event is InputEventScreenTouch:
return (event as InputEventScreenTouch).position
if event is InputEventScreenDrag:
return (event as InputEventScreenDrag).position
return Vector2.ZERO
func _begin_press(position: Vector2) -> void:
if active:
return
active = true
_press_position = position
_last_position = position
_drag_delta = Vector2.ZERO
_long_pressed = false
_dragging = false
_aiming = false
_long_press_token += 1
var token := _long_press_token
if bool(get_meta("gesture_enabled", false)) and get_tree() != null:
get_tree().create_timer(LONG_PRESS_SECONDS).timeout.connect(func():
if token != _long_press_token or not active or _long_pressed:
return
_long_pressed = true
_dragging = true
long_pressed.emit()
queue_redraw())
modulate = Color(0.82, 0.9, 1.0, idle_modulate.a)
pressed.emit()
press_state.emit(true)
queue_redraw()
func _update_drag(position: Vector2) -> void:
if not active:
return
var delta := position - _last_position
_last_position = position
_drag_delta = position - _press_position
if not bool(get_meta("gesture_enabled", false)):
return
if not _long_pressed:
if _drag_delta.length() > DRAG_THRESHOLD:
_long_press_token += 1
return
if not _dragging:
_dragging = true
drag_started.emit()
drag_changed.emit(delta)
queue_redraw()
func _end_press(position: Vector2) -> void:
if not active:
return
_update_drag(position)
_long_press_token += 1
if _dragging and _long_pressed:
drag_released.emit(_drag_delta)
active = false
modulate = idle_modulate
released.emit()
press_state.emit(false)
queue_redraw()
func cancel_press() -> void:
_long_press_token += 1
_touch_index = -1
if active:
# A cancelled gesture must never look like a completed drag to its
# receiver. Clear the gesture flags before ending the visual press.
_dragging = false
_long_pressed = false
_aiming = false
_end_press(_last_position)
else:
_long_pressed = false
_dragging = false
_aiming = false
func _unhandled_input(event: InputEvent) -> void:
# A focus loss / window pause can leave a touch-up event outside the
# control. MobileUiRoot also calls cancel_all() for the full overlay.
if event is InputEventScreenTouch and not event.pressed and event.index == _touch_index:
_touch_index = -1
_end_press(_last_position)
func _draw() -> void:
if not circular:
var rect_style := StyleBoxFlat.new()
rect_style.bg_color = Color(0.06, 0.1, 0.16, 0.86)
rect_style.border_color = accent
rect_style.set_border_width_all(1)
rect_style.set_corner_radius_all(6)
draw_style_box(rect_style, Rect2(Vector2.ZERO, size))
return
var center := size * 0.5
var radius := minf(size.x, size.y) * 0.5 - 3.0
if radius <= 0.0:
return
draw_circle(center, radius, Color(0.025, 0.04, 0.07, 0.68))
draw_arc(center, radius, 0.0, TAU, 48, accent, 3.0 if active or _aiming else 2.0)
draw_arc(center, radius - 5.0, 0.0, TAU, 48, Color(accent, 0.22), 1.0)
if _aiming:
draw_circle(center, 5.0, Color(1.0, 0.88, 0.52, 0.9))
draw_arc(center, radius - 12.0, -PI * 0.5, PI * 0.5, 24,
Color(1.0, 0.88, 0.52, 0.8), 2.0)