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