forked from xiangwang25/whale-town-front-v2
131 lines
4.1 KiB
GDScript
131 lines
4.1 KiB
GDScript
extends Control
|
|
|
|
const DEFAULT_DURATION: float = 5.0
|
|
const TARGET_OFFSET: Vector2 = Vector2(0, -84)
|
|
const TARGET_TOP_GAP: float = 10.0
|
|
const FALLBACK_TARGET_HEIGHT: float = 64.0
|
|
const MIN_TEXT_WIDTH: float = 96.0
|
|
const MAX_TEXT_WIDTH: float = 230.0
|
|
const TEXT_FONT_SIZE: float = 18.0
|
|
const LINE_HEIGHT: float = 24.0
|
|
|
|
@onready var label: Label = $PanelContainer/Label
|
|
@onready var panel: PanelContainer = $PanelContainer
|
|
|
|
var _targetNode: Node2D
|
|
var _targetOffset: Vector2 = TARGET_OFFSET
|
|
var _originalText: String = ""
|
|
|
|
func _process(_delta: float) -> void:
|
|
if _targetNode == null or not is_instance_valid(_targetNode):
|
|
return
|
|
|
|
_update_position()
|
|
|
|
func set_text(text: String, targetNode: Node2D = null, targetOffset: Vector2 = TARGET_OFFSET) -> void:
|
|
_originalText = text
|
|
_targetNode = targetNode
|
|
_targetOffset = targetOffset
|
|
_update_size()
|
|
|
|
if _targetNode != null:
|
|
_update_position()
|
|
|
|
await get_tree().create_timer(DEFAULT_DURATION).timeout
|
|
queue_free()
|
|
|
|
func _update_size() -> void:
|
|
if panel == null:
|
|
return
|
|
var metrics := _get_text_metrics(_originalText)
|
|
label.text = str(metrics.get("display_text", _originalText))
|
|
label.autowrap_mode = TextServer.AUTOWRAP_OFF
|
|
label.custom_minimum_size = Vector2(float(metrics.get("width", MIN_TEXT_WIDTH)), float(metrics.get("height", LINE_HEIGHT)))
|
|
label.size = label.custom_minimum_size
|
|
panel.reset_size()
|
|
reset_size()
|
|
panel.size = panel.get_combined_minimum_size()
|
|
size = panel.size
|
|
|
|
func _update_position() -> void:
|
|
var anchor := _get_target_top_screen_position()
|
|
var bubbleSize := _get_bubble_size()
|
|
global_position = Vector2(
|
|
anchor.x - bubbleSize.x * 0.5 + _targetOffset.x,
|
|
anchor.y - bubbleSize.y - TARGET_TOP_GAP
|
|
).round()
|
|
|
|
func _get_bubble_size() -> Vector2:
|
|
if panel != null:
|
|
var combined := panel.get_combined_minimum_size()
|
|
if combined.x > 0.0 and combined.y > 0.0:
|
|
return combined
|
|
if size.x > 0.0 and size.y > 0.0:
|
|
return size
|
|
return Vector2(280.0, 52.0)
|
|
|
|
func _get_text_metrics(text: String) -> Dictionary:
|
|
var lines: Array[String] = []
|
|
var currentLine := ""
|
|
var currentWidth := 0.0
|
|
var maxLineWidth := 0.0
|
|
for index in range(text.length()):
|
|
var character := text.substr(index, 1)
|
|
var characterWidth := _measure_text_width(character)
|
|
if not currentLine.is_empty() and currentWidth + characterWidth > MAX_TEXT_WIDTH:
|
|
lines.append(currentLine)
|
|
maxLineWidth = max(maxLineWidth, currentWidth)
|
|
currentLine = character
|
|
currentWidth = characterWidth
|
|
else:
|
|
currentLine += character
|
|
currentWidth += characterWidth
|
|
|
|
if not currentLine.is_empty() or lines.is_empty():
|
|
lines.append(currentLine)
|
|
maxLineWidth = max(maxLineWidth, currentWidth)
|
|
|
|
return {
|
|
"display_text": "\n".join(lines),
|
|
"width": clamp(maxLineWidth + 4.0, MIN_TEXT_WIDTH, MAX_TEXT_WIDTH),
|
|
"height": LINE_HEIGHT * float(lines.size())
|
|
}
|
|
|
|
func _measure_text_width(text: String) -> float:
|
|
if label != null:
|
|
var font := label.get_theme_font("font")
|
|
if font != null:
|
|
var fontSize := label.get_theme_font_size("font_size")
|
|
if fontSize <= 0:
|
|
fontSize = int(TEXT_FONT_SIZE)
|
|
var measured := font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1.0, fontSize).x
|
|
if measured > 0.0:
|
|
return measured
|
|
|
|
var estimated := 0.0
|
|
for index in range(text.length()):
|
|
var code := text.unicode_at(index)
|
|
estimated += TEXT_FONT_SIZE * 0.56 if code <= 0x7f else TEXT_FONT_SIZE
|
|
return estimated
|
|
|
|
func _get_target_top_screen_position() -> Vector2:
|
|
var targetScreenPosition: Vector2 = _targetNode.get_global_transform_with_canvas().origin
|
|
var visualTopOffset := _get_target_visual_top_offset()
|
|
return targetScreenPosition + visualTopOffset
|
|
|
|
func _get_target_visual_top_offset() -> Vector2:
|
|
var topOffset := Vector2(0.0, -FALLBACK_TARGET_HEIGHT)
|
|
if _targetNode == null:
|
|
return topOffset
|
|
|
|
var sprite := _targetNode.get_node_or_null("Sprite2D") as Sprite2D
|
|
if sprite == null:
|
|
return topOffset
|
|
|
|
var rect := sprite.get_rect()
|
|
var localTop := sprite.position + Vector2(0.0, rect.position.y * sprite.scale.y)
|
|
var nodeToCanvas := _targetNode.get_global_transform_with_canvas()
|
|
var topScreenPosition := nodeToCanvas * localTop
|
|
var targetScreenPosition: Vector2 = nodeToCanvas.origin
|
|
return topScreenPosition - targetScreenPosition
|