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