extends Control class_name ValuePicker const VELOCITY_DECAYING_FACTOR: float = 5.5 const BINNING_THRESHOLD: float = 0.75 const BINNING_ADJUST_P: float = 5.0 const DRAG_THRESHOLD_CM: float = 0.250 export var min_value: int export var max_value: int onready var input := get_node("current/input") as LineEdit onready var label_previous := get_node("previous") as Label onready var label_current := get_node("current") as Label onready var label_next := get_node("next") as Label var pointer: Dictionary var anchor: float var value: int var offset: float var screen_dpcm: float var scroll_unit_height: float var label_previous_base_position: float var label_current_base_position: float var label_next_base_position: float func _ready(): pointer = { index = -1, initial_position = Vector2.ZERO, current_position = Vector2.ZERO, velocity = Vector2.ZERO, was_dragged = false, is_active = false, } input.connect("text_entered", self, "input_text_entered") input.connect("focus_entered", self, "input_focus_entered") input.connect("focus_exited", self, "input_focus_exited") screen_dpcm = float(OS.get_screen_dpi()) / 2.54 scroll_unit_height = label_current.rect_size.y label_previous_base_position = label_previous.rect_position.y label_current_base_position = label_current.rect_position.y label_next_base_position = label_next.rect_position.y value = min_value offset = 0.0 func _process(delta: float): if pointer.is_active: var dragged_distance: float = - (pointer.current_position.y - pointer.initial_position.y) offset = anchor + (dragged_distance / scroll_unit_height) value = int(offset) offset -= value else: pointer.velocity *= clamp((1.0 - VELOCITY_DECAYING_FACTOR * delta), 0.0, 1.0) offset -= pointer.velocity.y * delta / scroll_unit_height if abs(pointer.velocity.y) < BINNING_THRESHOLD * scroll_unit_height: offset -= offset * BINNING_ADJUST_P * delta # Using 'offset * 2.0' (equivalent to 'offset / 0.5') rounds the value based on 0.5 units. var cummulative_displacement := int(offset * 2.0) value = wrapi(value + cummulative_displacement, min_value, max_value + 1) offset -= cummulative_displacement label_current.text = "%d" % value label_next.text = "%d" % wrapi(value + 1, min_value, max_value + 1) label_previous.text = "%d" % wrapi(value - 1, min_value, max_value + 1) var offset_position := offset * scroll_unit_height label_current.rect_position.y = label_current_base_position - offset_position label_previous.rect_position.y = label_previous_base_position - offset_position label_next.rect_position.y = label_next_base_position - offset_position label_previous.modulate.a = 0.5 - offset label_next.modulate.a = offset + 0.5 func _gui_input(event: InputEvent): # @DAM A bug on GODOT-3.X makes events from non-mouse-emulated pointers (index > 0) unreliable. if event is InputEventScreenTouch || event is InputEventScreenDrag: if event.index != 0: return if event is InputEventScreenTouch && (pointer.is_active == false || pointer.index == event.index): var touch := event as InputEventScreenTouch pointer.is_active = event.pressed pointer.current_position = touch.position if pointer.is_active: input.release_focus() pointer.index = touch.index pointer.initial_position = touch.position anchor = value + offset else: if pointer.was_dragged == false: # Click detected. input.grab_focus() pointer.index = -1 pointer.was_dragged = false if event is InputEventScreenDrag && event.index == pointer.index: var drag := event as InputEventScreenDrag pointer.current_position = drag.position pointer.velocity = drag.speed if pointer.current_position.distance_to(pointer.initial_position) / screen_dpcm > DRAG_THRESHOLD_CM: pointer.was_dragged = true func input_text_entered(new_text: String): input.release_focus() func input_focus_entered(): pointer.velocity = Vector2.ZERO # Avoid changing to other value once entering input. input.text = "%d" % value input.visible = true input.select_all() func input_focus_exited(): if input.text.is_valid_integer(): value = wrapi(int(input.text), min_value, max_value + 1) input.visible = false