aboutsummaryrefslogtreecommitdiff
path: root/ui/option_set
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/option_set
parent75791aecbff0d8adc1011f45a69877cabff616e0 (diff)
downloadsurgery-log-2d7da6bbc23fb917dfe931eaeb5566da0a102ac3.tar.zst
surgery-log-2d7da6bbc23fb917dfe931eaeb5566da0a102ac3.zip
Code cleanup.
Diffstat (limited to 'ui/option_set')
-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
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 )