aboutsummaryrefslogtreecommitdiff
path: root/ui
diff options
context:
space:
mode:
authordam <dam@gudinoff>2022-04-10 08:32:49 +0000
committerdam <dam@gudinoff>2022-04-10 08:32:49 +0000
commit2d7da6bbc23fb917dfe931eaeb5566da0a102ac3 (patch)
treeae4d7541267dd1a7a5633ff79ac7de1e9055437b /ui
parent75791aecbff0d8adc1011f45a69877cabff616e0 (diff)
downloadsurgery-log-2d7da6bbc23fb917dfe931eaeb5566da0a102ac3.tar.zst
surgery-log-2d7da6bbc23fb917dfe931eaeb5566da0a102ac3.zip
Code cleanup.
Diffstat (limited to 'ui')
-rw-r--r--ui/date_picker/date_picker.gd64
-rw-r--r--ui/date_picker/date_picker.tscn189
-rw-r--r--ui/date_picker/value_picker.gd132
-rw-r--r--ui/dialog/dialog.gd79
-rw-r--r--ui/modal_popup/modal_popup.gd76
-rw-r--r--ui/modal_popup/modal_popup.tscn40
-rw-r--r--ui/option_set/option_set.gd44
-rw-r--r--ui/option_set/option_set.tscn34
-rw-r--r--ui/option_set/option_set_list.gd294
-rw-r--r--ui/option_set/option_set_list.tscn9
-rw-r--r--ui/pointer_input_sensor.gd117
-rw-r--r--ui/touch_item_list/touch_item_list.gd68
-rw-r--r--ui/touch_item_list/touch_item_list.tscn19
-rw-r--r--ui/touch_vertical_container/touch_vertical_container.gd118
-rw-r--r--ui/touch_vertical_container/touch_vertical_container.tscn19
15 files changed, 1302 insertions, 0 deletions
diff --git a/ui/date_picker/date_picker.gd b/ui/date_picker/date_picker.gd
new file mode 100644
index 0000000..e2a793f
--- /dev/null
+++ b/ui/date_picker/date_picker.gd
@@ -0,0 +1,64 @@
+extends Control
+class_name DatePicker
+
+const days_per_month: Dictionary = {
+ 1: 31,
+ 2: 28,
+ 3: 31,
+ 4: 30,
+ 5: 31,
+ 6: 30,
+ 7: 31,
+ 8: 31,
+ 9: 30,
+ 10: 31,
+ 11: 30,
+ 12: 31,
+}
+
+onready var year_picker := get_node("year") as ValuePicker
+onready var month_picker := get_node("month") as ValuePicker
+onready var day_picker := get_node("day") as ValuePicker
+
+
+func _process(delta: float):
+ var year := year_picker.value
+ var month := month_picker.value
+ var day := day_picker.value
+ var days_on_month: int = days_per_month[month]
+
+ var is_leap_year := (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
+ if is_leap_year && month == 2:
+ days_on_month = 29
+
+ if day > days_on_month:
+ day_picker.value = days_on_month
+ day_picker.max_value = days_on_month
+
+
+func get_day() -> int:
+ return day_picker.value
+
+
+func get_month() -> int:
+ return month_picker.value
+
+
+func get_year() -> int:
+ return year_picker.value
+
+
+func get_date() -> Dictionary:
+ return {
+ year = year_picker.value,
+ month = month_picker.value,
+ day = day_picker.value,
+ }
+
+
+func set_date(new_year: int, new_month: int, new_day: int):
+ year_picker.value = new_year
+ month_picker.value = new_month
+ day_picker.value = new_day
+
+
diff --git a/ui/date_picker/date_picker.tscn b/ui/date_picker/date_picker.tscn
new file mode 100644
index 0000000..e3d88c4
--- /dev/null
+++ b/ui/date_picker/date_picker.tscn
@@ -0,0 +1,189 @@
+[gd_scene load_steps=3 format=2]
+
+[ext_resource path="res://ui/date_picker/value_picker.gd" type="Script" id=1]
+[ext_resource path="res://ui/date_picker/date_picker.gd" type="Script" id=2]
+
+[node name="date_picker" type="Control"]
+anchor_right = 1.0
+anchor_bottom = 1.0
+script = ExtResource( 2 )
+
+[node name="year" type="Control" parent="."]
+anchor_right = 0.333
+anchor_bottom = 1.0
+script = ExtResource( 1 )
+__meta__ = {
+"_edit_use_anchors_": false
+}
+min_value = 1
+max_value = 9999
+
+[node name="previous" type="Label" parent="year"]
+anchor_right = 1.0
+anchor_bottom = 0.333
+mouse_filter = 1
+align = 1
+valign = 1
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="current" type="Label" parent="year"]
+anchor_top = 0.333
+anchor_right = 1.0
+anchor_bottom = 0.666
+mouse_filter = 1
+align = 1
+valign = 1
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="input" type="LineEdit" parent="year/current"]
+visible = false
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+focus_next = NodePath("../../../month/current/input")
+custom_constants/minimum_spaces = 0
+align = 1
+max_length = 4
+caret_blink = true
+
+[node name="next" type="Label" parent="year"]
+anchor_top = 0.666
+anchor_right = 1.0
+anchor_bottom = 1.0
+mouse_filter = 1
+align = 1
+valign = 1
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="month" type="Control" parent="."]
+anchor_left = 0.333
+anchor_right = 0.666
+anchor_bottom = 1.0
+script = ExtResource( 1 )
+min_value = 1
+max_value = 12
+
+[node name="previous" type="Label" parent="month"]
+anchor_right = 1.0
+anchor_bottom = 0.333
+mouse_filter = 1
+align = 1
+valign = 1
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="current" type="Label" parent="month"]
+anchor_top = 0.333
+anchor_right = 1.0
+anchor_bottom = 0.666
+mouse_filter = 1
+align = 1
+valign = 1
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="input" type="LineEdit" parent="month/current"]
+visible = false
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+focus_next = NodePath("../../../day/current/input")
+focus_previous = NodePath("../../../year/current/input")
+custom_constants/minimum_spaces = 0
+align = 1
+max_length = 2
+caret_blink = true
+
+[node name="next" type="Label" parent="month"]
+anchor_top = 0.666
+anchor_right = 1.0
+anchor_bottom = 1.0
+mouse_filter = 1
+align = 1
+valign = 1
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="day" type="Control" parent="."]
+anchor_left = 0.666
+anchor_right = 1.0
+anchor_bottom = 1.0
+script = ExtResource( 1 )
+__meta__ = {
+"_edit_use_anchors_": false
+}
+min_value = 1
+max_value = 31
+
+[node name="previous" type="Label" parent="day"]
+anchor_right = 1.0
+anchor_bottom = 0.333
+mouse_filter = 1
+align = 1
+valign = 1
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="current" type="Label" parent="day"]
+anchor_top = 0.333
+anchor_right = 1.0
+anchor_bottom = 0.666
+mouse_filter = 1
+align = 1
+valign = 1
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="input" type="LineEdit" parent="day/current"]
+visible = false
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+focus_previous = NodePath("../../../month/current/input")
+custom_constants/minimum_spaces = 0
+align = 1
+max_length = 2
+caret_blink = true
+
+[node name="next" type="Label" parent="day"]
+anchor_top = 0.666
+anchor_right = 1.0
+anchor_bottom = 1.0
+mouse_filter = 1
+align = 1
+valign = 1
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="split_upper" type="ColorRect" parent="."]
+anchor_top = 0.333
+anchor_right = 1.0
+anchor_bottom = 0.343
+color = Color( 1, 1, 1, 0.25 )
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="split_lower" type="ColorRect" parent="."]
+anchor_top = 0.666
+anchor_right = 1.0
+anchor_bottom = 0.676
+color = Color( 1, 1, 1, 0.25 )
+__meta__ = {
+"_edit_use_anchors_": false
+}
diff --git a/ui/date_picker/value_picker.gd b/ui/date_picker/value_picker.gd
new file mode 100644
index 0000000..b70f1aa
--- /dev/null
+++ b/ui/date_picker/value_picker.gd
@@ -0,0 +1,132 @@
+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
+
+var pointer: Dictionary
+var anchor: float
+var value: int
+var offset: float
+
+var scroll_unit_height: float
+var label_previous_base_position: float
+var label_current_base_position: float
+var label_next_base_position: float
+
+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
+onready var screen_dpcm := float(OS.get_screen_dpi()) / 2.54
+
+
+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")
+
+ 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()
+ label_current.self_modulate.a = 0.0
+
+
+func input_focus_exited():
+ if input.text.is_valid_integer():
+ value = wrapi(int(input.text), min_value, max_value + 1)
+ input.visible = false
+ label_current.self_modulate.a = 1.0
+
+
diff --git a/ui/dialog/dialog.gd b/ui/dialog/dialog.gd
new file mode 100644
index 0000000..89fee5c
--- /dev/null
+++ b/ui/dialog/dialog.gd
@@ -0,0 +1,79 @@
+extends Control
+class_name Dialog
+
+signal answered # (accepted: bool)
+signal accepted # ()
+signal rejected # ()
+
+export var clear_signals_on_hide := true
+
+var message : Label
+var accept_button : Button
+var reject_button : Button
+
+
+func _init():
+ self.anchor_right = 1.0
+ self.anchor_bottom = 1.0
+ self.rect_clip_content = true
+ self.connect("hide", self, "_clear_signals")
+
+ reject_button = Button.new()
+ reject_button.anchor_top = 1.0
+ reject_button.anchor_left = 0.0
+ reject_button.anchor_right = 0.5
+ reject_button.margin_right = -5.0
+ reject_button.rect_min_size.y = 62.0
+ reject_button.grow_vertical = Control.GROW_DIRECTION_BEGIN
+ reject_button.name = "reject"
+ reject_button.connect("pressed", self, "_signal_rejected")
+ add_child(reject_button)
+
+ accept_button = Button.new()
+ accept_button.anchor_top = 1.0
+ accept_button.anchor_left = 0.5
+ accept_button.anchor_right = 1.0
+ accept_button.margin_left = 5.0
+ accept_button.rect_min_size.y = 62.0
+ accept_button.grow_vertical = Control.GROW_DIRECTION_BEGIN
+ accept_button.name = "accept"
+ accept_button.connect("pressed", self, "_signal_accepted")
+ add_child(accept_button)
+
+ message = Label.new()
+ message.autowrap = true
+ message.align = Label.ALIGN_CENTER
+ message.anchor_right = 1.0
+ message.anchor_bottom = 1.0
+ add_child(message)
+
+
+func _clear_signals():
+ if clear_signals_on_hide == false:
+ return
+
+ for signal_name in ["answered", "accepted", "rejected"]:
+ for it in get_signal_connection_list(signal_name):
+ disconnect(it.signal, it.target, it.method)
+
+
+func _signal_rejected():
+ emit_signal("rejected")
+ emit_signal("answered", false)
+ hide()
+
+
+func _signal_accepted():
+ emit_signal("accepted")
+ emit_signal("answered", true)
+ hide()
+
+
+func setup(message: String, accept_label: String = "Accept", reject_label: String = "Reject"):
+ self.message.text = message
+ accept_button.visible = accept_label != ""
+ accept_button.text = accept_label
+ reject_button.visible = reject_label != ""
+ reject_button.text = reject_label
+
+
diff --git a/ui/modal_popup/modal_popup.gd b/ui/modal_popup/modal_popup.gd
new file mode 100644
index 0000000..95fc2c3
--- /dev/null
+++ b/ui/modal_popup/modal_popup.gd
@@ -0,0 +1,76 @@
+extends Control
+class_name ModalPopup
+
+signal dismissed # ()
+
+export var clear_signals_on_hide := true
+
+var control : Control
+var control_parent : Node
+
+onready var title := get_node("title") as Label
+onready var background := get_node("background") as Panel
+onready var dismiss_button := get_node("dismiss") as Button
+
+
+func _init():
+ self.anchor_right = 1.0
+ self.anchor_bottom = 1.0
+ self.rect_clip_content = true
+ self.connect("hide", self, "_clear_signals")
+
+
+func _ready():
+ dismiss_button.connect("pressed", self, "dismiss")
+
+
+func _clear_signals():
+ if clear_signals_on_hide == false:
+ return
+
+ for signal_name in ["dismissed"]:
+ for it in get_signal_connection_list(signal_name):
+ disconnect(it.signal, it.target, it.method)
+
+
+func dismiss():
+ emit_signal("dismissed")
+ close_popup()
+
+
+func open_popup(title: String, item: Control):
+ if visible == true:
+ return
+
+ self.title.text = title
+
+ control = item
+ control.connect("hide", self, "close_popup")
+ control_parent = control.get_parent()
+ control_parent.remove_child(control)
+ self.add_child(control)
+
+ control.anchor_left = background.anchor_left
+ control.anchor_top = background.anchor_top
+ control.anchor_right = background.anchor_right
+ control.anchor_bottom = background.anchor_bottom
+ control.margin_left = 20
+ control.margin_top = 20
+ control.margin_right = -20
+ control.margin_bottom = -20
+
+ self.show()
+ control.show()
+
+
+func close_popup():
+ if visible == false:
+ return
+
+ control.disconnect("hide", self, "close_popup")
+ control.hide()
+ self.hide()
+ remove_child(control)
+ control_parent.add_child(control)
+ control_parent = null
+
diff --git a/ui/modal_popup/modal_popup.tscn b/ui/modal_popup/modal_popup.tscn
new file mode 100644
index 0000000..3814bef
--- /dev/null
+++ b/ui/modal_popup/modal_popup.tscn
@@ -0,0 +1,40 @@
+[gd_scene load_steps=3 format=2]
+
+[ext_resource path="res://ui/modal_popup/modal_popup.gd" type="Script" id=1]
+[ext_resource path="res://fonts/font_icons.tres" type="DynamicFont" id=2]
+
+[node name="popup" type="ColorRect"]
+visible = false
+anchor_right = 1.0
+anchor_bottom = 1.0
+color = Color( 0, 0, 0, 0.870588 )
+script = ExtResource( 1 )
+
+[node name="background" type="Panel" parent="."]
+anchor_left = 0.05
+anchor_top = 0.1
+anchor_right = 0.95
+anchor_bottom = 0.975
+
+[node name="title" type="Label" parent="."]
+anchor_left = 0.124
+anchor_top = 0.025
+anchor_right = 0.95
+anchor_bottom = 0.1
+margin_left = 0.0799866
+margin_right = -80.0
+margin_bottom = -20.0
+align = 1
+valign = 1
+autowrap = true
+
+[node name="dismiss" type="Button" parent="."]
+anchor_left = 0.05
+anchor_top = 0.025
+anchor_right = 0.124
+anchor_bottom = 0.1
+margin_right = 0.0799866
+margin_bottom = -20.0
+custom_fonts/font = ExtResource( 2 )
+text = ""
+flat = true
diff --git a/ui/option_set/option_set.gd b/ui/option_set/option_set.gd
new file mode 100644
index 0000000..25ca0ff
--- /dev/null
+++ b/ui/option_set/option_set.gd
@@ -0,0 +1,44 @@
+extends Control
+class_name OptionSet
+
+export var placeholder: String
+
+var text: String setget set_text, get_text
+
+func set_text(var value: String):
+ input.text = value
+
+func get_text() -> String:
+ return input.text
+
+var selected_idx: int
+
+onready var input := get_node("input") as LineEdit
+onready var button := get_node("button") as Button
+onready var popup := get_node("/root/main/popup") as ModalPopup
+onready var options := get_node("/root/main/option_set_list") as OptionSetList
+
+
+func _ready():
+ assert(popup != null, "OptionSet failed to get 'popup' node.")
+ input.placeholder_text = placeholder
+ input.connect("focus_entered", input, "set", ["caret_position", input.max_length])
+ input.connect("focus_exited", input, "deselect")
+
+
+func show_options(options_array: Array):
+ options.clear_items()
+ options.add_items(options_array)
+ options.select(options_array.find(input.text))
+ options.connect("selection_changed", self, "popup_result")
+ popup.open_popup(input.placeholder_text, options)
+
+
+func popup_result(index: int, text: String):
+ if index != -1:
+ selected_idx = index
+ input.text = text
+ input.caret_position = input.max_length
+ popup.close_popup()
+
+
diff --git a/ui/option_set/option_set.tscn b/ui/option_set/option_set.tscn
new file mode 100644
index 0000000..b971429
--- /dev/null
+++ b/ui/option_set/option_set.tscn
@@ -0,0 +1,34 @@
+[gd_scene load_steps=3 format=2]
+
+[ext_resource path="res://ui/option_set/option_set.gd" type="Script" id=1]
+[ext_resource path="res://fonts/font_icons.tres" type="DynamicFont" id=2]
+
+[node name="option_set" type="Control"]
+anchor_right = 1.0
+anchor_bottom = 1.0
+script = ExtResource( 1 )
+
+[node name="input" type="LineEdit" parent="."]
+anchor_right = 1.0
+anchor_bottom = 1.0
+margin_right = -100.0
+size_flags_horizontal = 3
+max_length = 4096
+placeholder_text = "placeholder"
+caret_blink = true
+caret_blink_speed = 0.5
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="button" type="Button" parent="."]
+anchor_left = 1.0
+anchor_right = 1.0
+anchor_bottom = 1.0
+margin_left = -100.0
+grow_horizontal = 0
+custom_fonts/font = ExtResource( 2 )
+text = ""
+__meta__ = {
+"_edit_use_anchors_": false
+}
diff --git a/ui/option_set/option_set_list.gd b/ui/option_set/option_set_list.gd
new file mode 100644
index 0000000..fda2c04
--- /dev/null
+++ b/ui/option_set/option_set_list.gd
@@ -0,0 +1,294 @@
+extends Control
+class_name OptionSetList
+
+const POINTER_VELOCITY_DECAYING_FACTOR: float = PI
+const POINTER_VELOCITY_BOOST_FACTOR: float = 1.25
+const EXACT_SELECTION: bool = false
+
+signal selection_changed # (idx: int, text: String)
+
+export var clear_signals_on_hide := true
+export var autowrap := true
+export var separation := 25
+
+var v_scroll_bar : VScrollBar
+var sensor : PointerInputSensor
+var is_pointer_dragging := false
+var pointer_drag_velocity := 0.0
+var when_last_dragged := 0
+
+var labels : Control
+var labels_end_positions : Array
+var labels_sizes : Array
+
+var items : Array
+var selected_idx := -1
+var selected_item := ""
+var is_dirty := true
+
+var normal_style : StyleBoxFlat
+var selected_style : StyleBoxFlat
+var border_size := 5
+
+onready var font : Font = get_font("font")
+onready var screen_dpcm : float = float(OS.get_screen_dpi()) / 2.54
+
+func _init():
+ self.anchor_right = 1.0
+ self.anchor_bottom = 1.0
+ self.rect_clip_content = true
+ self.connect("hide", self, "_clear_signals")
+
+ labels = Control.new()
+ labels.anchor_right = 1.0
+ labels.anchor_bottom = 1.0
+ labels.mouse_filter = Control.MOUSE_FILTER_IGNORE
+ labels.name = "labels"
+ add_child(labels)
+
+ v_scroll_bar = VScrollBar.new()
+ v_scroll_bar.anchor_left = 1.0
+ v_scroll_bar.anchor_bottom = 1.0
+ v_scroll_bar.grow_horizontal = Control.GROW_DIRECTION_BEGIN
+ v_scroll_bar.name = "v_scroll_bar"
+ add_child(v_scroll_bar)
+
+ sensor = PointerInputSensor.new()
+ sensor.anchor_right = 1.0
+ sensor.anchor_bottom = 1.0
+ sensor.name = "sensor"
+ add_child(sensor)
+
+ connect("resized", self, "mark_as_dirty")
+
+ sensor.connect("on_press", self, "pointer_input_on_press_handler")
+ sensor.connect("on_drag", self, "pointer_input_on_drag_handler")
+ sensor.connect("on_end_drag", self, "pointer_input_on_end_drag_handler")
+ sensor.connect("on_click", self, "pointer_input_on_click_handler")
+ sensor.connect("on_scroll", self, "pointer_input_on_scroll_handler")
+
+
+func _ready():
+ var button_style := get_stylebox("pressed", "Button")
+ var label_style := get_stylebox("normal", "Label")
+
+ normal_style = StyleBoxFlat.new()
+ normal_style.border_width_bottom = border_size
+ normal_style.border_color = Color.white * 0.333
+ normal_style.bg_color = Color.transparent
+ normal_style.content_margin_left = label_style.content_margin_left
+ normal_style.content_margin_top = label_style.content_margin_top
+ normal_style.content_margin_right = label_style.content_margin_right
+
+ var bg_color := button_style.bg_color as Color
+ var corner_radius := button_style.corner_radius_top_right as int
+ selected_style = StyleBoxFlat.new()
+ selected_style.bg_color = bg_color
+ selected_style.corner_radius_top_left = corner_radius
+ selected_style.corner_radius_top_right = corner_radius
+ selected_style.corner_radius_bottom_right = corner_radius
+ selected_style.corner_radius_bottom_left = corner_radius
+ selected_style.content_margin_left = label_style.content_margin_left
+ selected_style.content_margin_top = label_style.content_margin_top
+ selected_style.content_margin_right = label_style.content_margin_right
+
+
+func mark_as_dirty():
+ is_dirty = true
+
+
+func add_item(text: String):
+ items.append(text)
+ is_dirty = true
+
+
+func add_items(texts: Array):
+ items.append_array(texts)
+ is_dirty = true
+
+
+func set_item(index: int, text: String):
+ items[index] = text
+ is_dirty = true
+
+
+func set_items(indices: Array, texts: Array):
+ for idx in indices.size():
+ items[indices[idx]] = texts[idx]
+ is_dirty = true
+
+
+func remove_item(index: int):
+ items.remove(index)
+ is_dirty = true
+
+
+func remove_items(indices: Array):
+ indices.sort()
+ var idx := indices.size()-1
+ while idx >= 0:
+ items.remove(indices[idx])
+ idx -= 1
+ is_dirty = true
+
+
+func clear_items():
+ items.clear()
+ selected_idx = -1
+ selected_item = ""
+ v_scroll_bar.value = 0.0
+ is_dirty = true
+
+
+func build_labels():
+
+ var num_of_labels := int(min(
+ items.size(),
+ ceil(labels.rect_size.y / (font.get_height() + border_size)) + 1
+ ))
+
+ var delta := num_of_labels - labels.get_child_count()
+
+ while delta > 0:
+ var label := Label.new()
+ label.autowrap = autowrap
+ label.anchor_right = 1.0
+ label.valign = Label.VALIGN_TOP
+ labels.add_child(label)
+ delta -= 1
+
+ while delta < 0:
+ var label := labels.get_child(0) as Label
+ labels.remove_child(label)
+ label.free()
+ delta += 1
+
+ labels_end_positions.clear()
+ labels_sizes.clear()
+
+ if num_of_labels == 0:
+ return
+
+ var position := 0.0
+ var proto_label := labels.get_child(0) as Label
+ var line_spacing := proto_label.get_constant("line_spacing")
+ var line_height := proto_label.get_line_height()
+ for it in items:
+ proto_label.text = it
+ var line_count := proto_label.get_line_count()
+ var height := line_count * line_height + (line_count - 1) * line_spacing + border_size + separation
+
+ position += height
+ labels_end_positions.append(position)
+ labels_sizes.append(height)
+
+
+func _process(delta):
+
+ if is_dirty:
+ build_labels()
+ is_dirty = false
+
+ var viewable_ratio := 1.0
+ var viewable_height := labels.rect_size.y
+ if items.size() > 0:
+ viewable_ratio = viewable_height / float(labels_end_positions.back())
+
+ v_scroll_bar.min_value = 0
+ v_scroll_bar.max_value = viewable_height / viewable_ratio
+ v_scroll_bar.visible = viewable_ratio < 1.0
+ v_scroll_bar.page = viewable_height
+
+ var labels_offset := v_scroll_bar.value
+ var idx_offset := labels_end_positions.bsearch(labels_offset)
+
+ var idx := idx_offset
+ for label in labels.get_children():
+
+ if idx >= items.size():
+ label.visible = false
+ continue
+
+ label.visible = true
+ label.text = items[idx]
+ label.rect_size.y = labels_sizes[idx]
+ label.rect_position.y = labels_end_positions[idx] - labels_sizes[idx] - labels_offset
+
+ if idx == selected_idx:
+ label.add_stylebox_override("normal", selected_style)
+ else:
+ label.add_stylebox_override("normal", normal_style)
+
+ idx += 1
+
+ var right_margin = - v_scroll_bar.rect_size.x if v_scroll_bar.visible else 0.0
+ labels.margin_right = right_margin
+ sensor.margin_right = right_margin
+
+ # Apply drag movement inertia.
+ if is_pointer_dragging == false && abs(pointer_drag_velocity) > 0.5:
+ pointer_drag_velocity *= clamp((1.0 - POINTER_VELOCITY_DECAYING_FACTOR * delta), 0.0, 1.0)
+ v_scroll_bar.value -= pointer_drag_velocity * delta
+
+
+func get_item_at_position(mouse_position: Vector2) -> int:
+ var labels_offset := v_scroll_bar.value
+ var position := mouse_position.y + labels_offset
+ var item_idx := labels_end_positions.bsearch(position)
+
+ if item_idx == items.size():
+ item_idx = -1
+ return item_idx
+
+
+func select(index: int):
+ selected_idx = index
+ selected_item = items[selected_idx] if selected_idx >= 0 && selected_idx < items.size() else ""
+ emit_signal("selection_changed", selected_idx, selected_item)
+
+
+func unselect():
+ select(-1)
+
+
+func pointer_input_on_press_handler(pointer: PointerInputSensor.PointerInputData):
+ is_pointer_dragging = true
+ grab_focus()
+
+
+func pointer_input_on_drag_handler(pointer: PointerInputSensor.PointerInputData):
+ is_pointer_dragging = true
+ var reported_velocity_abs := abs(pointer.velocity.y)
+ var relative_velocity := pointer.relative_position.y * Engine.get_frames_per_second()
+ var relative_velocity_abs := abs(relative_velocity)
+ pointer_drag_velocity = pointer.velocity.y if reported_velocity_abs > relative_velocity_abs else relative_velocity
+ v_scroll_bar.value -= pointer.relative_position.y
+ when_last_dragged = OS.get_ticks_msec()
+
+
+func pointer_input_on_end_drag_handler(pointer: PointerInputSensor.PointerInputData):
+ is_pointer_dragging = false
+ if OS.get_ticks_msec() - when_last_dragged > 20:
+ pointer_drag_velocity = 0.0
+ else:
+ pointer_drag_velocity *= POINTER_VELOCITY_BOOST_FACTOR
+
+
+func pointer_input_on_click_handler(pointer: PointerInputSensor.PointerInputData):
+ var item_idx := get_item_at_position(pointer.current_position - rect_global_position)
+ select(item_idx)
+
+
+func pointer_input_on_scroll_handler(pointer: PointerInputSensor.PointerInputData):
+ pointer_drag_velocity -= pointer.scroll * POINTER_VELOCITY_BOOST_FACTOR * screen_dpcm
+
+
+func _clear_signals():
+ if clear_signals_on_hide == false:
+ return
+
+ for signal_name in ["selection_changed"]:
+ for it in get_signal_connection_list(signal_name):
+ disconnect(it.signal, it.target, it.method)
+
+
diff --git a/ui/option_set/option_set_list.tscn b/ui/option_set/option_set_list.tscn
new file mode 100644
index 0000000..82473a5
--- /dev/null
+++ b/ui/option_set/option_set_list.tscn
@@ -0,0 +1,9 @@
+[gd_scene load_steps=2 format=2]
+
+[ext_resource path="res://ui/option_set/option_set_list.gd" type="Script" id=1]
+
+[node name="option_set_list" type="Control"]
+anchor_right = 1.0
+anchor_bottom = 1.0
+rect_clip_content = true
+script = ExtResource( 1 )
diff --git a/ui/pointer_input_sensor.gd b/ui/pointer_input_sensor.gd
new file mode 100644
index 0000000..f99fc17
--- /dev/null
+++ b/ui/pointer_input_sensor.gd
@@ -0,0 +1,117 @@
+extends Control
+class_name PointerInputSensor
+
+# All on_ACTION signals have a single argument of type PointerInputData.
+signal on_press
+signal on_release
+#signal on_release_outside
+signal on_click
+signal on_enter
+signal on_exit
+#signal on_exit_app_window
+signal on_begin_drag
+signal on_drag
+signal on_end_drag
+signal on_scroll
+
+enum PointerInputAction {
+ UNDEFINED,
+ ON_PRESS,
+ ON_RELEASE,
+# ON_RELEASE_OUTSIDE,
+ ON_CLICK,
+ ON_ENTER,
+ ON_EXIT,
+# ON_EXIT_APP_WINDOW,
+ ON_BEGIN_DRAG,
+ ON_DRAG,
+ ON_END_DRAG,
+ ON_SCROLL,
+}
+
+class PointerInputData:
+ var target: PointerInputSensor
+ var event: InputEvent
+ var index := -1
+ var initial_position := Vector2.ZERO
+ var current_position := Vector2.ZERO
+ var relative_position := Vector2.ZERO
+ var velocity := Vector2.ZERO
+ var was_dragged := false
+ var is_pressed := false
+ var scroll := 0.0
+ var action: int = PointerInputAction.UNDEFINED
+
+
+export var drag_threshold_cm: float = 0.250
+
+var pointer: PointerInputData
+
+onready var screen_dpcm := float(OS.get_screen_dpi()) / 2.54
+
+
+func _ready():
+ pointer = PointerInputData.new()
+ pointer.target = self
+ connect("mouse_entered", self, "_on_enter_exit", [true])
+ connect("mouse_exited", self, "_on_enter_exit", [false])
+
+
+func _on_enter_exit(is_inside: bool):
+ pointer.action = PointerInputAction.ON_ENTER if is_inside else PointerInputAction.ON_EXIT
+ emit_signal("on_enter" if is_inside else "on_exit", pointer)
+
+
+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
+
+ pointer.event = event
+ pointer.action = PointerInputAction.UNDEFINED
+ pointer.velocity = Vector2.ZERO
+ pointer.relative_position = Vector2.ZERO
+ pointer.scroll = 0.0
+ pointer.current_position = get_global_mouse_position()
+
+ if event is InputEventScreenTouch && (pointer.is_pressed == false || pointer.index == event.index):
+ var touch := event as InputEventScreenTouch
+ pointer.is_pressed = event.pressed
+
+ if pointer.is_pressed:
+ pointer.index = touch.index
+ pointer.initial_position = pointer.current_position
+ pointer.action = PointerInputAction.ON_PRESS
+ emit_signal("on_press", pointer)
+ else:
+ pointer.action = PointerInputAction.ON_RELEASE
+ emit_signal("on_release", pointer)
+ if pointer.was_dragged == false:
+ pointer.action = PointerInputAction.ON_CLICK
+ emit_signal("on_click", pointer)
+ else:
+ pointer.action = PointerInputAction.ON_END_DRAG
+ emit_signal("on_end_drag", pointer)
+ pointer.index = -1
+ pointer.was_dragged = false
+
+ if event is InputEventScreenDrag && event.index == pointer.index:
+ var drag := event as InputEventScreenDrag
+ pointer.velocity = drag.speed
+ pointer.relative_position = drag.relative
+ if pointer.was_dragged == false && pointer.current_position.distance_to(pointer.initial_position) > drag_threshold_cm * screen_dpcm:
+ pointer.was_dragged = true
+ pointer.action = PointerInputAction.ON_BEGIN_DRAG
+ emit_signal("on_begin_drag", pointer)
+ pointer.action = PointerInputAction.ON_DRAG
+ emit_signal("on_drag", pointer)
+
+ if event is InputEventMouseButton && event.is_pressed():
+ if event.button_index == BUTTON_WHEEL_UP || event.button_index == BUTTON_WHEEL_DOWN:
+ pointer.scroll = -1.0 if event.button_index == BUTTON_WHEEL_UP else 1.0
+ pointer.action = PointerInputAction.ON_SCROLL
+ emit_signal("on_scroll", pointer)
+
+
diff --git a/ui/touch_item_list/touch_item_list.gd b/ui/touch_item_list/touch_item_list.gd
new file mode 100644
index 0000000..6794138
--- /dev/null
+++ b/ui/touch_item_list/touch_item_list.gd
@@ -0,0 +1,68 @@
+extends ItemList
+class_name TouchItemList
+
+const POINTER_VELOCITY_DECAYING_FACTOR: float = PI
+const POINTER_VELOCITY_BOOST_FACTOR: float = 1.25
+const EXACT_SELECTION: bool = true
+
+var is_pointer_dragging := false
+var pointer_drag_velocity := 0.0
+var when_last_dragged := 0
+
+onready var sensor := get_node("sensor") as PointerInputSensor
+onready var v_scroll_bar := get_v_scroll() as ScrollBar
+
+
+func _ready():
+ sensor.connect("on_press", self, "pointer_input_on_press_handler")
+ sensor.connect("on_drag", self, "pointer_input_on_drag_handler")
+ sensor.connect("on_end_drag", self, "pointer_input_on_end_drag_handler")
+ sensor.connect("on_click", self, "pointer_input_on_click_handler")
+ sensor.connect("on_scroll", self, "pointer_input_on_scroll_handler")
+
+
+func _process(delta: float):
+ # Apply drag movement inertia.
+ if is_pointer_dragging == false && abs(pointer_drag_velocity) > 0.5:
+ pointer_drag_velocity *= clamp((1.0 - POINTER_VELOCITY_DECAYING_FACTOR * delta), 0.0, 1.0)
+ v_scroll_bar.value -= pointer_drag_velocity * delta
+
+
+func pointer_input_on_press_handler(pointer: PointerInputSensor.PointerInputData):
+ is_pointer_dragging = true
+ grab_focus()
+
+
+func pointer_input_on_drag_handler(pointer: PointerInputSensor.PointerInputData):
+ is_pointer_dragging = true
+ var reported_velocity_abs := abs(pointer.velocity.y)
+ var relative_velocity := pointer.relative_position.y * Engine.get_frames_per_second()
+ var relative_velocity_abs := abs(relative_velocity)
+ pointer_drag_velocity = pointer.velocity.y if reported_velocity_abs > relative_velocity_abs else relative_velocity
+ v_scroll_bar.value -= pointer.relative_position.y
+ when_last_dragged = OS.get_ticks_msec()
+
+
+func pointer_input_on_end_drag_handler(pointer: PointerInputSensor.PointerInputData):
+ is_pointer_dragging = false
+ if OS.get_ticks_msec() - when_last_dragged > 20:
+ pointer_drag_velocity = 0.0
+ else:
+ pointer_drag_velocity *= POINTER_VELOCITY_BOOST_FACTOR
+
+
+func pointer_input_on_click_handler(pointer: PointerInputSensor.PointerInputData):
+ var selected_idx := get_item_at_position(pointer.current_position - rect_global_position, EXACT_SELECTION)
+ if selected_idx >= 0:
+ select(selected_idx)
+ emit_signal("item_selected", selected_idx)
+ else:
+ unselect_all()
+ emit_signal("nothing_selected")
+
+
+func pointer_input_on_scroll_handler(pointer: PointerInputSensor.PointerInputData):
+ var target := self
+ target._gui_input(pointer.event)
+
+
diff --git a/ui/touch_item_list/touch_item_list.tscn b/ui/touch_item_list/touch_item_list.tscn
new file mode 100644
index 0000000..38f9a7e
--- /dev/null
+++ b/ui/touch_item_list/touch_item_list.tscn
@@ -0,0 +1,19 @@
+[gd_scene load_steps=3 format=2]
+
+[ext_resource path="res://ui/pointer_input_sensor.gd" type="Script" id=1]
+[ext_resource path="res://ui/touch_item_list/touch_item_list.gd" type="Script" id=2]
+
+[node name="item_list" type="ItemList"]
+anchor_right = 1.0
+anchor_bottom = 1.0
+mouse_filter = 2
+script = ExtResource( 2 )
+
+[node name="sensor" type="Control" parent="."]
+anchor_right = 1.0
+anchor_bottom = 1.0
+margin_right = -8.0
+script = ExtResource( 1 )
+__meta__ = {
+"_edit_use_anchors_": false
+}
diff --git a/ui/touch_vertical_container/touch_vertical_container.gd b/ui/touch_vertical_container/touch_vertical_container.gd
new file mode 100644
index 0000000..8f87338
--- /dev/null
+++ b/ui/touch_vertical_container/touch_vertical_container.gd
@@ -0,0 +1,118 @@
+extends ScrollContainer
+class_name TouchVerticalContainer
+
+const POINTER_VELOCITY_DECAYING_FACTOR: float = PI
+const POINTER_VELOCITY_BOOST_FACTOR: float = 1.25
+
+var is_pointer_dragging := false
+var pointer_drag_velocity := 0.0
+var when_last_dragged := 0
+var exclude_controls := []
+
+onready var controls = get_node("controls")
+
+
+func _ready():
+ assert(controls != null, "TouchVerticalContainer failed to get 'controls' node.")
+
+ var item_list_separation := float(controls.get_constant("separation"))
+
+ for it in controls.get_children():
+ it = it as Control
+ if exclude_controls.has(it.name):
+ continue
+
+ var sensor = PointerInputSensor.new()
+ it.add_child(sensor)
+
+ sensor.name = "sensor"
+ sensor.anchor_right = 1.0
+ sensor.anchor_bottom = 1.0
+ sensor.margin_top = - item_list_separation / 2.0
+ sensor.margin_bottom = item_list_separation / 2.0
+
+ sensor.connect("on_press", self, "pointer_input_on_press_handler")
+ sensor.connect("on_drag", self, "pointer_input_on_drag_handler")
+ sensor.connect("on_end_drag", self, "pointer_input_on_end_drag_handler")
+ sensor.connect("on_click", self, "pointer_input_on_click_handler")
+ sensor.connect("on_scroll", self, "pointer_input_on_scroll_handler")
+
+
+func _process(delta: float):
+ # Apply drag movement inertia.
+ if is_pointer_dragging == false && abs(pointer_drag_velocity) > 0.5:
+ pointer_drag_velocity *= clamp((1.0 - POINTER_VELOCITY_DECAYING_FACTOR * delta), 0.0, 1.0)
+ self.scroll_vertical -= pointer_drag_velocity * delta
+
+
+func pointer_input_on_press_handler(pointer: PointerInputSensor.PointerInputData):
+ is_pointer_dragging = true
+ grab_focus()
+
+
+func pointer_input_on_drag_handler(pointer: PointerInputSensor.PointerInputData):
+ is_pointer_dragging = true
+ var reported_velocity_abs := abs(pointer.velocity.y)
+ var relative_velocity := pointer.relative_position.y * Engine.get_frames_per_second()
+ var relative_velocity_abs := abs(relative_velocity)
+ pointer_drag_velocity = pointer.velocity.y if reported_velocity_abs > relative_velocity_abs else relative_velocity
+ self.scroll_vertical -= pointer.relative_position.y
+ when_last_dragged = OS.get_ticks_msec()
+
+
+func pointer_input_on_end_drag_handler(pointer: PointerInputSensor.PointerInputData):
+ is_pointer_dragging = false
+ if OS.get_ticks_msec() - when_last_dragged > 20:
+ pointer_drag_velocity = 0.0
+ else:
+ pointer_drag_velocity *= POINTER_VELOCITY_BOOST_FACTOR
+
+
+func pointer_input_on_click_handler(pointer: PointerInputSensor.PointerInputData):
+ # Get last leaf node.
+ var root := pointer.target.get_parent() as Control
+ var leaf := root as Node
+ while leaf.get_child_count() > 0:
+ leaf = leaf.get_child(leaf.get_child_count() - 1)
+
+ # Navigate backwards from leaf to root until we find a node accepting the input.
+ var tried_leaf_as_root := false
+ while leaf != root || tried_leaf_as_root == false:
+ tried_leaf_as_root = leaf == root # Allow a final iteration cycle when leaf reaches root.
+ var node := leaf
+
+ if node is PointerInputSensor \
+ || node is Control == false \
+ || node.mouse_filter == MOUSE_FILTER_IGNORE \
+ || node.visible == false \
+ || node.get_global_rect().has_point(pointer.current_position) == false:
+ # Get next node to be processed.
+ var leaf_index_in_parent := leaf.get_position_in_parent()
+ var parent := leaf.get_parent()
+ if leaf_index_in_parent == 0:
+ leaf = parent
+ else:
+ leaf = parent.get_child(leaf_index_in_parent - 1)
+ # Drill down into the new tree branch.
+ while leaf.get_child_count() > 0:
+ leaf = leaf.get_child(leaf.get_child_count() - 1)
+ continue
+
+ var control: Control = node
+ if control is CheckBox || control is CheckButton || (control is Button && control.toggle_mode == true):
+ control.pressed = !control.pressed
+
+ control.grab_focus()
+ control.emit_signal("button_down")
+ control.emit_signal("pressed")
+ control.emit_signal("button_up")
+ control.connect("focus_exited", pointer.target, "set_visible", [true], CONNECT_ONESHOT)
+ pointer.target.visible = false
+ break
+
+
+func pointer_input_on_scroll_handler(pointer: PointerInputSensor.PointerInputData):
+ var target := self
+ target._gui_input(pointer.event)
+
+
diff --git a/ui/touch_vertical_container/touch_vertical_container.tscn b/ui/touch_vertical_container/touch_vertical_container.tscn
new file mode 100644
index 0000000..e741e8a
--- /dev/null
+++ b/ui/touch_vertical_container/touch_vertical_container.tscn
@@ -0,0 +1,19 @@
+[gd_scene load_steps=2 format=2]
+
+[ext_resource path="res://ui/touch_vertical_container/touch_vertical_container.gd" type="Script" id=1]
+
+[node name="touch_vertical_container" type="ScrollContainer"]
+anchor_right = 1.0
+anchor_bottom = 1.0
+mouse_filter = 2
+scroll_horizontal_enabled = false
+script = ExtResource( 1 )
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="controls" type="VBoxContainer" parent="."]
+margin_right = 1080.0
+margin_bottom = 1920.0
+size_flags_horizontal = 3
+size_flags_vertical = 3