diff options
Diffstat (limited to 'ui/option_set')
| -rw-r--r-- | ui/option_set/option_set.gd | 44 | ||||
| -rw-r--r-- | ui/option_set/option_set.tscn | 34 | ||||
| -rw-r--r-- | ui/option_set/option_set_list.gd | 294 | ||||
| -rw-r--r-- | ui/option_set/option_set_list.tscn | 9 |
4 files changed, 381 insertions, 0 deletions
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 ) |
