OverviewFilesCommitsIssuesDocsReleasesPeers

godot-keet / scripts / avatar_picker.gd

118 lines · gd ref:
class_name AvatarPicker
extends ConfirmationDialog
## Reusable avatar upload + crop dialog.
## Opens a FileDialog, loads image, center-crops to square, scales to target sizes.

signal avatar_selected(small_b64: String, large_b64: String)

const SMALL_SIZE := 48
const LARGE_SIZE := 128

var _preview: TextureRect
var _source_image: Image = null
var _file_dialog: FileDialog


func _init() -> void:
	title = "Choose Avatar"
	min_size = Vector2i(300, 340)
	ok_button_text = "Apply"
	get_ok_button().disabled = true


func _ready() -> void:
	var vbox := VBoxContainer.new()
	add_child(vbox)

	var label := Label.new()
	label.text = "Preview"
	label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	label.add_theme_font_size_override("font_size", 13)
	vbox.add_child(label)

	# Center the preview
	var center := CenterContainer.new()
	center.custom_minimum_size.y = 140
	vbox.add_child(center)

	_preview = TextureRect.new()
	_preview.custom_minimum_size = Vector2(128, 128)
	_preview.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
	_preview.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
	center.add_child(_preview)

	# Placeholder
	var placeholder := Image.create(LARGE_SIZE, LARGE_SIZE, false, Image.FORMAT_RGBA8)
	placeholder.fill(Color(0.25, 0.25, 0.3))
	_preview.texture = ImageTexture.create_from_image(placeholder)

	var pick_btn := Button.new()
	pick_btn.text = "Browse..."
	pick_btn.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
	pick_btn.pressed.connect(_open_file_dialog)
	vbox.add_child(pick_btn)

	var info := Label.new()
	info.text = "PNG, JPG, or WebP. Will be cropped to square."
	info.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	info.add_theme_font_size_override("font_size", 11)
	info.add_theme_color_override("font_color", Color(0.5, 0.55, 0.6))
	vbox.add_child(info)

	confirmed.connect(_on_confirmed)
	canceled.connect(func(): hide())


func _open_file_dialog() -> void:
	if _file_dialog != null:
		_file_dialog.queue_free()
	_file_dialog = FileDialog.new()
	_file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
	_file_dialog.access = FileDialog.ACCESS_FILESYSTEM
	_file_dialog.filters = PackedStringArray(["*.png ; PNG", "*.jpg ; JPEG", "*.jpeg ; JPEG", "*.webp ; WebP"])
	_file_dialog.min_size = Vector2i(500, 400)
	_file_dialog.file_selected.connect(_on_file_selected)
	add_child(_file_dialog)
	_file_dialog.popup_centered()


func _on_file_selected(path: String) -> void:
	var img := Image.new()
	var err: Error = img.load(path)
	if err != OK:
		push_warning("AvatarPicker: failed to load image: %s" % path)
		return
	_source_image = _crop_square(img)
	# Show preview at large size
	var preview_img: Image = _source_image.duplicate()
	preview_img.resize(LARGE_SIZE, LARGE_SIZE, Image.INTERPOLATE_LANCZOS)
	_preview.texture = ImageTexture.create_from_image(preview_img)
	get_ok_button().disabled = false


func _on_confirmed() -> void:
	if _source_image == null:
		return
	var small_img: Image = _source_image.duplicate()
	small_img.resize(SMALL_SIZE, SMALL_SIZE, Image.INTERPOLATE_LANCZOS)

	var large_img: Image = _source_image.duplicate()
	large_img.resize(LARGE_SIZE, LARGE_SIZE, Image.INTERPOLATE_LANCZOS)

	var small_b64: String = Marshalls.raw_to_base64(small_img.save_png_to_buffer())
	var large_b64: String = Marshalls.raw_to_base64(large_img.save_png_to_buffer())

	avatar_selected.emit(small_b64, large_b64)
	hide()


static func _crop_square(img: Image) -> Image:
	var w: int = img.get_width()
	var h: int = img.get_height()
	if w == h:
		return img
	var side: int = mini(w, h)
	var x: int = (w - side) / 2
	var y: int = (h - side) / 2
	return img.get_region(Rect2i(x, y, side, side))