feat: complete mobile UI implementation
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user