1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
|
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 item_selected # (idx: int, text: String)
signal nothing_selected # ()
export var autowrap := true
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 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
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)
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
selected_style = StyleBoxFlat.new()
selected_style.border_width_bottom = border_size
selected_style.border_color = Color.white * 0.333
selected_style.bg_color = Color.dimgray
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 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()
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
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:
if items.size() == 0:
return -1
var labels_offset := v_scroll_bar.value
var position := mouse_position.y + labels_offset
var item_idx := labels_end_positions.bsearch(position)
return int(min(item_idx, items.size()-1)) # Return last item when position is below it.
func select(index: int):
selected_idx = index
emit_signal("item_selected", selected_idx, items[selected_idx])
func unselect():
selected_idx = -1
emit_signal("nothing_selected")
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)
if selected_idx >= 0:
select(selected_idx)
emit_signal("item_selected", selected_idx)
else:
unselect()
emit_signal("nothing_selected")
func pointer_input_on_scroll_handler(pointer: PointerInputSensor.PointerInputData):
pointer_drag_velocity -= pointer.scroll * POINTER_VELOCITY_BOOST_FACTOR * screen_dpcm
|