straywild/ui/components/bubble_menu/bubble_button.gd

358 lines
10 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

class_name BubbleButton
extends Button
const ICON_FILL_RATIO: float = 0.76
const LABELED_ICON_FILL_RATIO: float = 0.58
const BubbleGradientBackgroundType = preload(
"res://ui/components/bubble_menu/bubble_gradient_background.gd"
)
const BubbleHoverDrawerType = preload(
"res://ui/components/bubble_menu/bubble_hover_drawer.gd"
)
@export var profile: BubbleMenuProfile
@export_group("Authored Layout")
@export var neutral_size: Vector2 = Vector2(120.0, 116.0)
@export var desktop_anchor: Vector2 = Vector2.ZERO
@export var compact_anchor: Vector2 = Vector2.ZERO
@export var compact_minimum_size: Vector2 = Vector2.ZERO
@export_group("Typography")
@export var label_control_path: NodePath
@export_range(1, 256, 1) var minimum_font_size: int = 14
@export_range(1, 256, 1) var maximum_font_size: int = 32
@export_group("Icon")
@export_range(0.0, 1.0, 0.01) var icon_fill_ratio_override: float = 0.0
@export_group("Motion")
@export_range(0.0, 32.0, 0.1) var horizontal_amplitude: float = 1.8
@export_range(0.0, 32.0, 0.1) var vertical_amplitude: float = 4.0
@export_range(0.1, 30.0, 0.1) var motion_period: float = 5.0
@export_range(0.0, TAU, 0.01) var motion_phase: float = 0.0
@export_range(0.0, 0.2, 0.001) var deformation_amplitude: float = 0.016
@export_range(0.1, 30.0, 0.1) var deformation_period: float = 6.0
@export var hover_wiggle_amplitude: Vector2 = Vector2.ZERO
@export_range(0.1, 30.0, 0.1) var hover_wiggle_period: float = 2.0
@export_group("Hover Drawer")
@export var hover_drawer_text: String = ""
@export_enum("Right", "Left Vertical") var hover_drawer_mode: int = 0
@export_range(0.1, 12.0, 0.1) var hover_drawer_speed: float = 4.0
@export_group("Hit Area")
@export var elliptical_hit_area: bool = true
var neutral_position: Vector2 = Vector2.ZERO
var presented_size: Vector2 = Vector2.ZERO
var emphasis: float = 0.0
var _hovered: bool = false
var _drawer_focused: bool = false
var _drawer_reveal: float = 0.0
var _gradient_background: BubbleGradientBackground
var _hover_drawer: BubbleHoverDrawer
func _ready() -> void:
mouse_entered.connect(_set_hovered.bind(true))
mouse_exited.connect(_set_hovered.bind(false))
focus_entered.connect(_set_drawer_focused.bind(true))
focus_exited.connect(_set_drawer_focused.bind(false))
resized.connect(_update_pivot)
_update_pivot()
apply_profile()
_apply_icon_presentation(neutral_size)
_update_hover_drawer()
func _gui_input(event: InputEvent) -> void:
if _adjustment_direction(self) == 0:
return
var direction: int = 0
if event.is_action_pressed(&"ui_left"):
direction = -1
elif event.is_action_pressed(&"ui_right"):
direction = 1
if direction == 0:
return
var adjustment_button: BaseButton = _find_adjustment_button(direction)
if adjustment_button == null:
return
adjustment_button.pressed.emit()
accept_event()
func _find_adjustment_button(direction: int) -> BaseButton:
var parent_node: Node = get_parent()
if parent_node == null:
return null
for child: Node in parent_node.get_children():
var button := child as BaseButton
if (
button != null
and button.visible
and not button.disabled
and _adjustment_direction(button) == direction
):
return button
return null
func _adjustment_direction(button: BaseButton) -> int:
var descriptions: Array[String] = [
button.text.strip_edges().to_lower(),
button.tooltip_text.strip_edges().to_lower(),
button.accessibility_name.strip_edges().to_lower(),
]
for description: String in descriptions:
if description in ["-", "", "minus", "decrease"]:
return -1
if description in ["+", "plus", "increase"]:
return 1
return 0
func apply_layout(
center: Vector2,
bubble_size: Vector2,
font_size_ratio: float,
) -> void:
position = center - bubble_size * 0.5
size = bubble_size
neutral_position = position
presented_size = bubble_size
_update_pivot()
_apply_icon_presentation(bubble_size)
if _hover_drawer != null:
_hover_drawer.refresh_layout()
var label_control: Control = get_label_control()
var font_size := clampi(
roundi(minf(bubble_size.x, bubble_size.y) * font_size_ratio),
minimum_font_size,
maximum_font_size
)
label_control.add_theme_font_size_override("font_size", font_size)
func get_layout_size(layout_scale: float, compact: bool) -> Vector2:
var bubble_size: Vector2 = neutral_size * layout_scale
if compact:
bubble_size.x = maxf(bubble_size.x, compact_minimum_size.x)
bubble_size.y = maxf(bubble_size.y, compact_minimum_size.y)
if _uses_icon_only_presentation():
var diameter: float = maxf(bubble_size.x, bubble_size.y)
bubble_size = Vector2(diameter, diameter)
return bubble_size
func get_authored_anchor(compact: bool) -> Vector2:
return compact_anchor if compact else desktop_anchor
func get_label_control() -> Control:
if not label_control_path.is_empty():
var custom_label := get_node_or_null(label_control_path) as Control
if custom_label != null:
return custom_label
return self
func _uses_icon_only_presentation() -> bool:
return icon != null and text.is_empty() and label_control_path.is_empty()
func _apply_icon_presentation(bubble_size: Vector2) -> void:
if icon == null:
return
vertical_icon_alignment = VERTICAL_ALIGNMENT_CENTER
expand_icon = true
texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
var fill_ratio: float = (
ICON_FILL_RATIO
if _uses_icon_only_presentation()
else LABELED_ICON_FILL_RATIO
)
if _uses_icon_only_presentation() and icon_fill_ratio_override > 0.0:
fill_ratio = icon_fill_ratio_override
add_theme_constant_override(
"icon_max_width",
maxi(1, roundi(minf(bubble_size.x, bubble_size.y) * fill_ratio)),
)
if not _uses_icon_only_presentation():
return
alignment = HORIZONTAL_ALIGNMENT_CENTER
icon_alignment = HORIZONTAL_ALIGNMENT_CENTER
func advance_emphasis(delta: float) -> void:
var target: float = 1.0 if _hovered else 0.0
emphasis = move_toward(
emphasis,
target,
profile.emphasis_speed * delta
)
if _gradient_background != null:
_gradient_background.queue_redraw()
var drawer_target: float = 1.0 if (_hovered or _drawer_focused) else 0.0
_drawer_reveal = move_toward(
_drawer_reveal,
drawer_target,
hover_drawer_speed * delta,
)
if _hover_drawer != null:
_hover_drawer.set_reveal(_drawer_reveal)
func reset_hover_presentation() -> void:
_hovered = false
_drawer_focused = false
emphasis = 0.0
_drawer_reveal = 0.0
if _gradient_background != null:
_gradient_background.queue_redraw()
if _hover_drawer != null:
_hover_drawer.set_reveal(0.0)
func calculate_target(
elapsed: float,
layout_scale: float,
motion_scale: float,
) -> Vector2:
var phase: float = (
elapsed / motion_period * TAU
+ motion_phase
)
var idle_offset := Vector2(
sin(phase * 0.73 + motion_phase) * horizontal_amplitude,
sin(phase) * vertical_amplitude
) * layout_scale * motion_scale
var hover_phase: float = elapsed / hover_wiggle_period * TAU
var hover_offset := Vector2(
sin(hover_phase),
sin(hover_phase * 2.0 + motion_phase) * 0.5,
) * hover_wiggle_amplitude * emphasis * layout_scale * motion_scale
idle_offset.y -= (
profile.hover_focus_lift
* emphasis
* layout_scale
)
return neutral_position + idle_offset + hover_offset
func calculate_visual_scale(
elapsed: float,
motion_scale: float,
) -> Vector2:
var deformation_phase: float = (
elapsed / deformation_period * TAU
+ motion_phase * 1.37
)
var deformation_amount: float = (
sin(deformation_phase)
* deformation_amplitude
* motion_scale
* lerpf(1.0, profile.emphasized_deformation_scale, emphasis)
)
if _uses_icon_only_presentation():
deformation_amount = 0.0
var hover_scale: float = lerpf(
1.0,
profile.hover_focus_scale,
emphasis
)
return Vector2(
1.0 + deformation_amount,
1.0 - deformation_amount
) * hover_scale
func get_visual_radius(visual_scale: Vector2) -> float:
var visual_size: Vector2 = presented_size * visual_scale
return (visual_size.x + visual_size.y) * 0.25
func apply_presentation(
target_position: Vector2,
visual_scale: Vector2,
position_weight: float,
) -> void:
position = position.lerp(target_position, position_weight)
scale = visual_scale
func _has_point(point: Vector2) -> bool:
if not elliptical_hit_area:
return Rect2(Vector2.ZERO, size).has_point(point)
var radius: Vector2 = size * 0.5
if radius.x <= 0.0 or radius.y <= 0.0:
return false
var normalized: Vector2 = (point - radius) / radius
return normalized.length_squared() <= 1.0
func _set_hovered(value: bool) -> void:
_hovered = value
if _gradient_background != null:
_gradient_background.queue_redraw()
func _set_drawer_focused(value: bool) -> void:
_drawer_focused = value
func _update_pivot() -> void:
pivot_offset = size * 0.5
func apply_profile() -> void:
if profile == null:
return
_update_gradient_background()
add_theme_stylebox_override("normal", profile.make_normal_style())
var hover_style: StyleBoxFlat = profile.make_hover_style()
add_theme_stylebox_override("hover", hover_style)
add_theme_stylebox_override("focus", profile.make_normal_style())
add_theme_stylebox_override("pressed", profile.make_pressed_style())
add_theme_stylebox_override("disabled", profile.make_disabled_style())
add_theme_color_override("font_color", profile.text_color)
add_theme_color_override("font_hover_color", profile.text_hover_color)
add_theme_color_override("font_focus_color", profile.text_color)
add_theme_color_override("font_pressed_color", profile.text_pressed_color)
add_theme_color_override("font_disabled_color", profile.text_disabled_color)
var label_control: Control = get_label_control()
if label_control != self:
label_control.add_theme_color_override("font_color", profile.text_color)
func _update_gradient_background() -> void:
if not profile.gradient_enabled:
if _gradient_background != null:
_gradient_background.queue_free()
_gradient_background = null
return
if _gradient_background == null:
_gradient_background = BubbleGradientBackgroundType.new()
_gradient_background.name = "GradientBackground"
add_child(_gradient_background)
move_child(_gradient_background, 0)
_gradient_background.configure(self, profile)
func _update_hover_drawer() -> void:
if hover_drawer_text.is_empty():
if _hover_drawer != null:
_hover_drawer.queue_free()
_hover_drawer = null
return
if _hover_drawer == null:
_hover_drawer = BubbleHoverDrawerType.new()
_hover_drawer.name = "HoverDrawer"
add_child(_hover_drawer)
move_child(_hover_drawer, 0)
_hover_drawer.configure(
self,
hover_drawer_text,
hover_drawer_mode as BubbleHoverDrawer.DrawerMode,
)