aboutsummaryrefslogtreecommitdiff
path: root/game/addons/zylann.hterrain/tools/util
diff options
context:
space:
mode:
author Gone2Daly <71726742+Gone2Daly@users.noreply.github.com>2023-07-22 21:05:42 +0200
committer Gone2Daly <71726742+Gone2Daly@users.noreply.github.com>2023-07-22 21:05:42 +0200
commit71b3cd829f80de4c2cd3972d8bfd5ee470a5d180 (patch)
treeb4280fde6eef2ae6987648bc7bf8e00e9011bb7f /game/addons/zylann.hterrain/tools/util
parentce9022d0df74d6c33db3686622be2050d873ab0b (diff)
init_testtest3d
Diffstat (limited to 'game/addons/zylann.hterrain/tools/util')
-rw-r--r--game/addons/zylann.hterrain/tools/util/dialog_fitter.gd53
-rw-r--r--game/addons/zylann.hterrain/tools/util/dialog_fitter.tscn10
-rw-r--r--game/addons/zylann.hterrain/tools/util/editor_util.gd104
-rw-r--r--game/addons/zylann.hterrain/tools/util/interval_slider.gd197
-rw-r--r--game/addons/zylann.hterrain/tools/util/resource_importer_texture.gd19
-rw-r--r--game/addons/zylann.hterrain/tools/util/resource_importer_texture_layered.gd9
-rw-r--r--game/addons/zylann.hterrain/tools/util/result.gd36
-rw-r--r--game/addons/zylann.hterrain/tools/util/rich_text_label_hyperlinks.gd11
-rw-r--r--game/addons/zylann.hterrain/tools/util/spin_slider.gd385
-rw-r--r--game/addons/zylann.hterrain/tools/util/spin_slider.tscn13
10 files changed, 837 insertions, 0 deletions
diff --git a/game/addons/zylann.hterrain/tools/util/dialog_fitter.gd b/game/addons/zylann.hterrain/tools/util/dialog_fitter.gd
new file mode 100644
index 0000000..59bb194
--- /dev/null
+++ b/game/addons/zylann.hterrain/tools/util/dialog_fitter.gd
@@ -0,0 +1,53 @@
+
+# If you make a container-based UI inside a WindowDialog, there is a chance it will overflow
+# because WindowDialogs don't adjust by themselves. This happens when the user has a different
+# font size than yours, and can cause controls to be unusable (like buttons at the bottom).
+# This script adjusts the size of the parent WindowDialog based on the first Container it finds
+# when the node becomes visible.
+
+@tool
+# Needs to be a Control, otherwise we don't receive the notification...
+extends Control
+
+const HT_Util = preload("../../util/util.gd")
+
+
+func _notification(what: int):
+ if HT_Util.is_in_edited_scene(self):
+ return
+ if is_inside_tree() and what == Control.NOTIFICATION_VISIBILITY_CHANGED:
+ #print("Visible ", is_visible_in_tree(), ", ", visible)
+ call_deferred("_fit_to_contents")
+
+
+func _fit_to_contents():
+ var dialog : Window = get_parent()
+ for child in dialog.get_children():
+ if child is Container:
+ var child_rect : Rect2 = child.get_global_rect()
+ var dialog_rect := Rect2(Vector2(), dialog.size)
+ #print("Dialog: ", dialog_rect, ", contents: ", child_rect, " ", child.get_path())
+ if not dialog_rect.encloses(child_rect):
+ var margin : Vector2 = child.get_rect().position
+ #print("Fitting ", dialog.get_path(), " from ", dialog.rect_size,
+ # " to ", child_rect.size + margin * 2.0)
+ dialog.min_size = child_rect.size + margin * 2.0
+
+
+#func _process(delta):
+# update()
+
+# DEBUG
+#func _draw():
+# var self_global_pos = get_global_rect().position
+#
+# var dialog : Control = get_parent()
+# var dialog_rect := dialog.get_global_rect()
+# dialog_rect.position -= self_global_pos
+# draw_rect(dialog_rect, Color(1,1,0), false)
+#
+# for child in dialog.get_children():
+# if child is Container:
+# var child_rect : Rect2 = child.get_global_rect()
+# child_rect.position -= self_global_pos
+# draw_rect(child_rect, Color(1,1,0,0.1))
diff --git a/game/addons/zylann.hterrain/tools/util/dialog_fitter.tscn b/game/addons/zylann.hterrain/tools/util/dialog_fitter.tscn
new file mode 100644
index 0000000..2e3b00c
--- /dev/null
+++ b/game/addons/zylann.hterrain/tools/util/dialog_fitter.tscn
@@ -0,0 +1,10 @@
+[gd_scene load_steps=2 format=2]
+
+[ext_resource path="res://addons/zylann.hterrain/tools/util/dialog_fitter.gd" type="Script" id=1]
+
+[node name="DialogFitter" type="Control"]
+mouse_filter = 2
+script = ExtResource( 1 )
+__meta__ = {
+"_edit_use_anchors_": false
+}
diff --git a/game/addons/zylann.hterrain/tools/util/editor_util.gd b/game/addons/zylann.hterrain/tools/util/editor_util.gd
new file mode 100644
index 0000000..a6d9eff
--- /dev/null
+++ b/game/addons/zylann.hterrain/tools/util/editor_util.gd
@@ -0,0 +1,104 @@
+
+# Editor-specific utilities.
+# This script cannot be loaded in an exported game.
+
+@tool
+
+
+# This is normally an `EditorFileDialog`. I can't type-hint this one properly,
+# because when I test UI in isolation, I can't use `EditorFileDialog`.
+static func create_open_file_dialog() -> ConfirmationDialog:
+ var d
+ if Engine.is_editor_hint():
+ # TODO Workaround bug when editor-only classes are created in source code, even if not run
+ # https://github.com/godotengine/godot/issues/73525
+# d = EditorFileDialog.new()
+ d = ClassDB.instantiate(&"EditorFileDialog")
+ d.file_mode = EditorFileDialog.FILE_MODE_OPEN_FILE
+ d.access = EditorFileDialog.ACCESS_RESOURCES
+ else:
+ d = FileDialog.new()
+ d.file_mode = FileDialog.FILE_MODE_OPEN_FILE
+ d.access = FileDialog.ACCESS_RESOURCES
+ d.unresizable = false
+ return d
+
+
+static func create_open_dir_dialog() -> ConfirmationDialog:
+ var d
+ if Engine.is_editor_hint():
+ # TODO Workaround bug when editor-only classes are created in source code, even if not run
+ # https://github.com/godotengine/godot/issues/73525
+# d = EditorFileDialog.new()
+ d = ClassDB.instantiate(&"EditorFileDialog")
+ d.file_mode = EditorFileDialog.FILE_MODE_OPEN_DIR
+ d.access = EditorFileDialog.ACCESS_RESOURCES
+ else:
+ d = FileDialog.new()
+ d.file_mode = FileDialog.FILE_MODE_OPEN_DIR
+ d.access = FileDialog.ACCESS_RESOURCES
+ d.unresizable = false
+ return d
+
+
+# If you want to open using Image.load()
+static func create_open_image_dialog() -> ConfirmationDialog:
+ var d = create_open_file_dialog()
+ _add_image_filters(d)
+ return d
+
+
+# If you want to open using load(),
+# although it might still fail if the file is imported as Image...
+static func create_open_texture_dialog() -> ConfirmationDialog:
+ var d = create_open_file_dialog()
+ _add_texture_filters(d)
+ return d
+
+
+static func create_open_texture_array_dialog() -> ConfirmationDialog:
+ var d = create_open_file_dialog()
+ _add_texture_array_filters(d)
+ return d
+
+# TODO Post a proposal, we need a file dialog filtering on resource types, not on file extensions!
+
+static func _add_image_filters(file_dialog):
+ file_dialog.add_filter("*.png ; PNG files")
+ file_dialog.add_filter("*.jpg ; JPG files")
+ #file_dialog.add_filter("*.exr ; EXR files")
+
+
+static func _add_texture_filters(file_dialog):
+ _add_image_filters(file_dialog)
+ # Godot
+ file_dialog.add_filter("*.ctex ; CompressedTexture files")
+ # Packed textures
+ file_dialog.add_filter("*.packed_tex ; HTerrainPackedTexture files")
+
+
+static func _add_texture_array_filters(file_dialog):
+ _add_image_filters(file_dialog)
+ # Godot
+ file_dialog.add_filter("*.ctexarray ; TextureArray files")
+ # Packed textures
+ file_dialog.add_filter("*.packed_texarr ; HTerrainPackedTextureArray files")
+
+
+# Tries to load a texture with the ResourceLoader, and if it fails, attempts
+# to load it manually as an ImageTexture
+static func load_texture(path: String, logger) -> Texture:
+ var tex : Texture = load(path)
+ if tex != null:
+ return tex
+ # This can unfortunately happen when the editor didn't import assets yet.
+ # See https://github.com/godotengine/godot/issues/17483
+ logger.error(str("Failed to load texture ", path, ", attempting to load manually"))
+ var im := Image.new()
+ var err = im.load(path)
+ if err != OK:
+ logger.error(str("Failed to load image ", path))
+ return null
+ var itex := ImageTexture.create_from_image(im)
+ return itex
+
diff --git a/game/addons/zylann.hterrain/tools/util/interval_slider.gd b/game/addons/zylann.hterrain/tools/util/interval_slider.gd
new file mode 100644
index 0000000..481d08b
--- /dev/null
+++ b/game/addons/zylann.hterrain/tools/util/interval_slider.gd
@@ -0,0 +1,197 @@
+
+# Slider with two handles representing an interval.
+
+@tool
+extends Control
+
+const VALUE_LOW = 0
+const VALUE_HIGH = 1
+const VALUE_COUNT = 2
+
+const FG_MARGIN = 1
+
+signal changed
+
+var _min_value := 0.0
+var _max_value := 1.0
+var _values = [0.2, 0.6]
+var _grabbing := false
+
+
+func _get_property_list():
+ return [
+ {
+ "name": "min_value",
+ "type": TYPE_FLOAT,
+ "usage": PROPERTY_USAGE_EDITOR
+ },
+ {
+ "name": "max_value",
+ "type": TYPE_FLOAT,
+ "usage": PROPERTY_USAGE_EDITOR
+ },
+ {
+ "name": "range",
+ "type": TYPE_VECTOR2,
+ "usage": PROPERTY_USAGE_STORAGE
+ }
+ ]
+
+
+func _get(key: StringName):
+ match key:
+ &"min_value":
+ return _min_value
+ &"max_value":
+ return _max_value
+ &"range":
+ return Vector2(_min_value, _max_value)
+
+
+func _set(key: StringName, value):
+ match key:
+ &"min_value":
+ _min_value = min(value, _max_value)
+ queue_redraw()
+ &"max_value":
+ _max_value = max(value, _min_value)
+ queue_redraw()
+ &"range":
+ _min_value = value.x
+ _max_value = value.y
+
+
+func set_values(low: float, high: float):
+ if low > high:
+ low = high
+ if high < low:
+ high = low
+ _values[VALUE_LOW] = low
+ _values[VALUE_HIGH] = high
+ queue_redraw()
+
+
+func set_value(i: int, v: float, notify_change: bool):
+ var min_value = _min_value
+ var max_value = _max_value
+
+ match i:
+ VALUE_LOW:
+ max_value = _values[VALUE_HIGH]
+ VALUE_HIGH:
+ min_value = _values[VALUE_LOW]
+ _:
+ assert(false)
+
+ v = clampf(v, min_value, max_value)
+ if v != _values[i]:
+ _values[i] = v
+ queue_redraw()
+ if notify_change:
+ changed.emit()
+
+
+func get_value(i: int) -> float:
+ return _values[i]
+
+
+func get_low_value() -> float:
+ return _values[VALUE_LOW]
+
+
+func get_high_value() -> float:
+ return _values[VALUE_HIGH]
+
+
+func get_ratio(i: int) -> float:
+ return _value_to_ratio(_values[i])
+
+
+func get_low_ratio() -> float:
+ return get_ratio(VALUE_LOW)
+
+
+func get_high_ratio() -> float:
+ return get_ratio(VALUE_HIGH)
+
+
+func _ratio_to_value(r: float) -> float:
+ return r * (_max_value - _min_value) + _min_value
+
+
+func _value_to_ratio(v: float) -> float:
+ if absf(_max_value - _min_value) < 0.001:
+ return 0.0
+ return (v - _min_value) / (_max_value - _min_value)
+
+
+func _get_closest_index(ratio: float) -> int:
+ var distance_low := absf(ratio - get_low_ratio())
+ var distance_high := absf(ratio - get_high_ratio())
+ if distance_low < distance_high:
+ return VALUE_LOW
+ return VALUE_HIGH
+
+
+func _set_from_pixel(px: float):
+ var r := (px - FG_MARGIN) / (size.x - FG_MARGIN * 2.0)
+ var i := _get_closest_index(r)
+ var v := _ratio_to_value(r)
+ set_value(i, v, true)
+
+
+func _gui_input(event: InputEvent):
+ if event is InputEventMouseButton:
+ if event.pressed:
+ if event.button_index == MOUSE_BUTTON_LEFT:
+ _grabbing = true
+ _set_from_pixel(event.position.x)
+ else:
+ if event.button_index == MOUSE_BUTTON_LEFT:
+ _grabbing = false
+
+ elif event is InputEventMouseMotion:
+ if _grabbing:
+ _set_from_pixel(event.position.x)
+
+
+func _draw():
+ var grabber_width := 3
+ var background_v_margin := 0
+ var foreground_margin := FG_MARGIN
+ var grabber_color := Color(0.8, 0.8, 0.8)
+ var interval_color := Color(0.4,0.4,0.4)
+ var background_color := Color(0.1, 0.1, 0.1)
+
+ var control_rect := Rect2(Vector2(), size)
+
+ var bg_rect := Rect2(
+ control_rect.position.x,
+ control_rect.position.y + background_v_margin,
+ control_rect.size.x,
+ control_rect.size.y - 2 * background_v_margin)
+ draw_rect(bg_rect, background_color)
+
+ var fg_rect := control_rect.grow(-foreground_margin)
+
+ var low_ratio := get_low_ratio()
+ var high_ratio := get_high_ratio()
+
+ var low_x := fg_rect.position.x + low_ratio * fg_rect.size.x
+ var high_x := fg_rect.position.x + high_ratio * fg_rect.size.x
+
+ var interval_rect := Rect2(
+ low_x, fg_rect.position.y, high_x - low_x, fg_rect.size.y)
+ draw_rect(interval_rect, interval_color)
+
+ low_x = fg_rect.position.x + low_ratio * (fg_rect.size.x - grabber_width)
+ high_x = fg_rect.position.x + high_ratio * (fg_rect.size.x - grabber_width)
+
+ for x in [low_x, high_x]:
+ var grabber_rect := Rect2(
+ x,
+ fg_rect.position.y,
+ grabber_width,
+ fg_rect.size.y)
+ draw_rect(grabber_rect, grabber_color)
+
diff --git a/game/addons/zylann.hterrain/tools/util/resource_importer_texture.gd b/game/addons/zylann.hterrain/tools/util/resource_importer_texture.gd
new file mode 100644
index 0000000..b24f0e2
--- /dev/null
+++ b/game/addons/zylann.hterrain/tools/util/resource_importer_texture.gd
@@ -0,0 +1,19 @@
+@tool
+
+# Stuff not exposed by Godot for making .import files
+
+const COMPRESS_LOSSLESS = 0
+const COMPRESS_LOSSY = 1
+const COMPRESS_VRAM_COMPRESSED = 2
+const COMPRESS_VRAM_UNCOMPRESSED = 3
+const COMPRESS_BASIS_UNIVERSAL = 4
+
+const ROUGHNESS_DETECT = 0
+const ROUGHNESS_DISABLED = 1
+# Godot internally subtracts 2 to magically obtain a `Image.RoughnessChannel` enum
+# (also not exposed)
+const ROUGHNESS_RED = 2
+const ROUGHNESS_GREEN = 3
+const ROUGHNESS_BLUE = 4
+const ROUGHNESS_ALPHA = 5
+const ROUGHNESS_GRAY = 6
diff --git a/game/addons/zylann.hterrain/tools/util/resource_importer_texture_layered.gd b/game/addons/zylann.hterrain/tools/util/resource_importer_texture_layered.gd
new file mode 100644
index 0000000..ff77b1a
--- /dev/null
+++ b/game/addons/zylann.hterrain/tools/util/resource_importer_texture_layered.gd
@@ -0,0 +1,9 @@
+@tool
+
+# Stuff not exposed by Godot for making .import files
+
+const COMPRESS_LOSSLESS = 0
+const COMPRESS_LOSSY = 1
+const COMPRESS_VRAM_COMPRESSED = 2
+const COMPRESS_VRAM_UNCOMPRESSED = 3
+const COMPRESS_BASIS_UNIVERSAL = 4
diff --git a/game/addons/zylann.hterrain/tools/util/result.gd b/game/addons/zylann.hterrain/tools/util/result.gd
new file mode 100644
index 0000000..5e897f1
--- /dev/null
+++ b/game/addons/zylann.hterrain/tools/util/result.gd
@@ -0,0 +1,36 @@
+# Data structure to hold the result of a function that can be expected to fail.
+# The use case is to report errors back to the GUI and act accordingly,
+# instead of forgetting them to the console or having the script break on an assertion.
+# This is a C-like way of things, where the result can bubble, and does not require globals.
+
+@tool
+
+# Replace `success` with `error : int`?
+var success := false
+var value = null
+var message := ""
+var inner_result = null
+
+
+func _init(p_success: bool, p_message := "", p_inner = null):
+ success = p_success
+ message = p_message
+ inner_result = p_inner
+
+
+# TODO Can't type-hint self return
+func with_value(v):
+ value = v
+ return self
+
+
+func get_message() -> String:
+ var msg := message
+ if inner_result != null:
+ msg += "\n"
+ msg += inner_result.get_message()
+ return msg
+
+
+func is_ok() -> bool:
+ return success
diff --git a/game/addons/zylann.hterrain/tools/util/rich_text_label_hyperlinks.gd b/game/addons/zylann.hterrain/tools/util/rich_text_label_hyperlinks.gd
new file mode 100644
index 0000000..242fe6e
--- /dev/null
+++ b/game/addons/zylann.hterrain/tools/util/rich_text_label_hyperlinks.gd
@@ -0,0 +1,11 @@
+@tool
+extends RichTextLabel
+
+
+func _init():
+ meta_clicked.connect(_on_meta_clicked)
+
+
+func _on_meta_clicked(meta):
+ OS.shell_open(meta)
+
diff --git a/game/addons/zylann.hterrain/tools/util/spin_slider.gd b/game/addons/zylann.hterrain/tools/util/spin_slider.gd
new file mode 100644
index 0000000..fa690d5
--- /dev/null
+++ b/game/addons/zylann.hterrain/tools/util/spin_slider.gd
@@ -0,0 +1,385 @@
+@tool
+extends Control
+
+const FG_MARGIN = 2
+const MAX_DECIMALS_VISUAL = 3
+
+signal value_changed(value)
+
+
+var _value := 0.0
+@export var value: float:
+ get:
+ return _value
+ set(v):
+ set_value_no_notify(v)
+
+
+var _min_value := 0.0
+@export var min_value: float:
+ get:
+ return _min_value
+ set(v):
+ set_min_value(v)
+
+
+var _max_value := 100.0
+@export var max_value: float:
+ get:
+ return _max_value
+ set(v):
+ set_max_value(v)
+
+
+var _prefix := ""
+@export var prefix: String:
+ get:
+ return _prefix
+ set(v):
+ set_prefix(v)
+
+
+var _suffix := ""
+@export var suffix: String:
+ get:
+ return _suffix
+ set(v):
+ set_suffix(v)
+
+
+var _rounded := false
+@export var rounded: bool:
+ get:
+ return _rounded
+ set(v):
+ set_rounded(v)
+
+
+var _centered := true
+@export var centered: bool:
+ get:
+ return _centered
+ set(v):
+ set_centered(v)
+
+
+var _allow_greater := false
+@export var allow_greater: bool:
+ get:
+ return _allow_greater
+ set(v):
+ set_allow_greater(v)
+
+
+# There is still a limit when typing a larger value, but this one is to prevent software
+# crashes or freezes. The regular min and max values are for slider UX. Exceeding it should be
+# a corner case.
+var _greater_max_value := 10000.0
+@export var greater_max_value: float:
+ get:
+ return _greater_max_value
+ set(v):
+ set_greater_max_value(v)
+
+
+var _label : Label
+var _label2 : Label
+var _line_edit : LineEdit
+var _ignore_line_edit := false
+var _pressing := false
+var _grabbing := false
+var _press_pos := Vector2()
+
+
+func _init():
+ custom_minimum_size = Vector2(32, 28)
+
+ _label = Label.new()
+ _label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
+ _label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
+ _label.clip_text = true
+ #_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ _label.anchor_top = 0
+ _label.anchor_left = 0
+ _label.anchor_right = 1
+ _label.anchor_bottom = 1
+ _label.mouse_filter = Control.MOUSE_FILTER_IGNORE
+ _label.add_theme_color_override("font_color_shadow", Color(0,0,0,0.5))
+ _label.add_theme_constant_override("shadow_offset_x", 1)
+ _label.add_theme_constant_override("shadow_offset_y", 1)
+ add_child(_label)
+
+ _label2 = Label.new()
+ _label2.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
+ _label2.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
+ _label2.clip_text = true
+ #_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ _label2.anchor_top = 0
+ _label2.anchor_left = 0
+ _label2.anchor_right = 1
+ _label2.anchor_bottom = 1
+ _label2.offset_left = 8
+ _label2.mouse_filter = Control.MOUSE_FILTER_IGNORE
+ _label2.add_theme_color_override("font_color_shadow", Color(0,0,0,0.5))
+ _label2.add_theme_constant_override("shadow_offset_x", 1)
+ _label2.add_theme_constant_override("shadow_offset_y", 1)
+ _label2.hide()
+ add_child(_label2)
+
+ _line_edit = LineEdit.new()
+ _line_edit.alignment = HORIZONTAL_ALIGNMENT_CENTER
+ _line_edit.anchor_top = 0
+ _line_edit.anchor_left = 0
+ _line_edit.anchor_right = 1
+ _line_edit.anchor_bottom = 1
+ _line_edit.gui_input.connect(_on_LineEdit_gui_input)
+ _line_edit.focus_exited.connect(_on_LineEdit_focus_exited)
+ _line_edit.text_submitted.connect(_on_LineEdit_text_submitted)
+ _line_edit.hide()
+ add_child(_line_edit)
+
+ mouse_default_cursor_shape = Control.CURSOR_HSIZE
+
+
+func _ready():
+ pass # Replace with function body.
+
+
+func set_centered(p_centered: bool):
+ _centered = p_centered
+ if _centered:
+ _label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
+ _label.offset_right = 0
+ _label2.hide()
+ else:
+ _label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
+ _label.offset_right = -8
+ _label2.show()
+ queue_redraw()
+
+
+func is_centered() -> bool:
+ return _centered
+
+
+func set_value_no_notify(v: float):
+ set_value(v, false, false)
+
+
+func set_value(v: float, notify_change: bool, use_slider_maximum: bool = false):
+ if _allow_greater and not use_slider_maximum:
+ v = clampf(v, _min_value, _greater_max_value)
+ else:
+ v = clampf(v, _min_value, _max_value)
+
+ if v != _value:
+ _value = v
+
+ queue_redraw()
+
+ if notify_change:
+ value_changed.emit(get_value())
+
+
+func get_value():
+ if _rounded:
+ return int(roundf(_value))
+ return _value
+
+
+func set_min_value(minv: float):
+ _min_value = minv
+ #queue_redraw()
+
+
+func get_min_value() -> float:
+ return _min_value
+
+
+func set_max_value(maxv: float):
+ _max_value = maxv
+ #queue_redraw()
+
+
+func get_max_value() -> float:
+ return _max_value
+
+
+func set_greater_max_value(gmax: float):
+ _greater_max_value = gmax
+
+
+func get_greater_max_value() -> float:
+ return _greater_max_value
+
+
+func set_rounded(b: bool):
+ _rounded = b
+ queue_redraw()
+
+
+func is_rounded() -> bool:
+ return _rounded
+
+
+func set_prefix(p_prefix: String):
+ _prefix = p_prefix
+ queue_redraw()
+
+
+func get_prefix() -> String:
+ return _prefix
+
+
+func set_suffix(p_suffix: String):
+ _suffix = p_suffix
+ queue_redraw()
+
+
+func get_suffix() -> String:
+ return _suffix
+
+
+func set_allow_greater(allow: bool):
+ _allow_greater = allow
+
+
+func is_allowing_greater() -> bool:
+ return _allow_greater
+
+
+func _set_from_pixel(px: float):
+ var r := (px - FG_MARGIN) / (size.x - FG_MARGIN * 2.0)
+ var v := _ratio_to_value(r)
+ set_value(v, true, true)
+
+
+func get_ratio() -> float:
+ return _value_to_ratio(get_value())
+
+
+func _ratio_to_value(r: float) -> float:
+ return r * (_max_value - _min_value) + _min_value
+
+
+func _value_to_ratio(v: float) -> float:
+ if absf(_max_value - _min_value) < 0.001:
+ return 0.0
+ return (v - _min_value) / (_max_value - _min_value)
+
+
+func _on_LineEdit_gui_input(event: InputEvent):
+ if event is InputEventKey:
+ if event.pressed:
+ if event.keycode == KEY_ESCAPE:
+ _ignore_line_edit = true
+ _hide_line_edit()
+ grab_focus()
+ _ignore_line_edit = false
+
+
+func _on_LineEdit_focus_exited():
+ if _ignore_line_edit:
+ return
+ _enter_text()
+
+
+func _on_LineEdit_text_submitted(text: String):
+ _enter_text()
+
+
+func _enter_text():
+ var s = _line_edit.text.strip_edges()
+ if s.is_valid_float():
+ var v := s.to_float()
+ if not _allow_greater:
+ v = minf(v, _max_value)
+ set_value(v, true, false)
+ _hide_line_edit()
+
+
+func _hide_line_edit():
+ _line_edit.hide()
+ _label.show()
+ queue_redraw()
+
+
+func _show_line_edit():
+ _line_edit.show()
+ _line_edit.text = str(get_value())
+ _line_edit.select_all()
+ _line_edit.grab_focus()
+ _label.hide()
+ queue_redraw()
+
+
+func _gui_input(event: InputEvent):
+ if event is InputEventMouseButton:
+ if event.pressed:
+ if event.button_index == MOUSE_BUTTON_LEFT:
+ _press_pos = event.position
+ _pressing = true
+ else:
+ if event.button_index == MOUSE_BUTTON_LEFT:
+ _pressing = false
+ if _grabbing:
+ _grabbing = false
+ _set_from_pixel(event.position.x)
+ else:
+ _show_line_edit()
+
+ elif event is InputEventMouseMotion:
+ if _pressing and _press_pos.distance_to(event.position) > 2.0:
+ _grabbing = true
+ if _grabbing:
+ _set_from_pixel(event.position.x)
+
+
+func _draw():
+ if _line_edit.visible:
+ return
+
+ #var grabber_width := 3
+ var background_v_margin := 0
+ var foreground_margin := FG_MARGIN
+ #var grabber_color := Color(0.8, 0.8, 0.8)
+ var interval_color := Color(0.4,0.4,0.4)
+ var background_color := Color(0.1, 0.1, 0.1)
+
+ var control_rect := Rect2(Vector2(), size)
+
+ var bg_rect := Rect2(
+ control_rect.position.x,
+ control_rect.position.y + background_v_margin,
+ control_rect.size.x,
+ control_rect.size.y - 2 * background_v_margin)
+ draw_rect(bg_rect, background_color)
+
+ var fg_rect := control_rect.grow(-foreground_margin)
+ # Clamping the ratio because the value can be allowed to exceed the slider's boundaries
+ var ratio := clampf(get_ratio(), 0.0, 1.0)
+ fg_rect.size.x *= ratio
+ draw_rect(fg_rect, interval_color)
+
+ var value_text := str(get_value())
+
+ var dot_pos := value_text.find(".")
+ if dot_pos != -1:
+ var decimal_count := len(value_text) - dot_pos
+ if decimal_count > MAX_DECIMALS_VISUAL:
+ value_text = value_text.substr(0, dot_pos + MAX_DECIMALS_VISUAL + 1)
+
+ if _centered:
+ var text := value_text
+ if _prefix != "":
+ text = str(_prefix, " ", text)
+ if _suffix != "":
+ text = str(text, " ", _suffix)
+ _label.text = text
+
+ else:
+ _label2.text = _prefix
+ var text := value_text
+ if _suffix != "":
+ text = str(text, " ", _suffix)
+ _label.text = text
diff --git a/game/addons/zylann.hterrain/tools/util/spin_slider.tscn b/game/addons/zylann.hterrain/tools/util/spin_slider.tscn
new file mode 100644
index 0000000..70a0da8
--- /dev/null
+++ b/game/addons/zylann.hterrain/tools/util/spin_slider.tscn
@@ -0,0 +1,13 @@
+[gd_scene load_steps=2 format=2]
+
+[ext_resource path="res://addons/zylann.hterrain/tools/util/spin_slider.gd" type="Script" id=1]
+
+[node name="SpinSlider" type="Control"]
+anchor_right = 1.0
+anchor_bottom = 1.0
+rect_min_size = Vector2( 32, 28 )
+mouse_default_cursor_shape = 10
+script = ExtResource( 1 )
+__meta__ = {
+"_edit_use_anchors_": false
+}