StreamOverlay/lib/UI/GDSubMenuButton.gd

200 lines
6 KiB
GDScript3
Raw Permalink Normal View History

extends Button
class_name GDSubMenuButton
# Original code by iRadDev (https://github.com/iRadEntertainment/ridiculous-stream-overlay/)
@export var expand_on_hover := true:
set(val):
expand_on_hover = val
if !is_node_ready(): await ready
if val:
if not mouse_entered.is_connected(expand_menu):
mouse_entered.connect(expand_menu.bind(true))
else:
if mouse_entered.is_connected(expand_menu):
mouse_entered.disconnect(expand_menu)
@export var fold_on_child_button_pressed := true
@export var is_radial := false
@export_range(0.0, 64.0, 0.5) var offset_gap: float = 8:
set(val):
offset_gap = val
expand_menu(is_open)
@export_range(0.0, 360.0, 0.5) var radial_angle: float = 180:
set(val):
radial_angle = val
_radial_angle_rad = deg_to_rad(radial_angle)
expand_menu(is_open)
@export_range(0.0, 0.6, 0.01) var anim_duration = 0.15
@export_range(0.0, 0.1, 0.001) var anim_delay = 0.03
const MIN_SIZE = Vector2(48, 48)
var main_menu_button: GDSubMenuButton
var tw: Tween
enum Edge { UP, LEFT, BOT, RIGHT}
var closest_edge := Edge.RIGHT
var _radial_angle_rad: float = PI
var parent: GDSubMenuButton
var parent_dir := Vector2.DOWN
var custom_pos := Vector2()
var is_open := false
var is_sub_menu := false
var is_dragged := false
var is_mouse_click_down := false
var grabbed_at := Vector2()
var all_btns: Array[Button]
signal btn_child_expanded(btn_child: GDSubMenuButton, is_open: bool)
signal properly_pressed
func start(_main_menu_button: GDSubMenuButton = null) -> void:
main_menu_button = _main_menu_button
if not main_menu_button:
main_menu_button = self
gui_input.connect(_on_gui_input)
properly_pressed.connect(toggle_menu)
if expand_on_hover:
mouse_entered.connect(expand_menu.bind(true))
if get_parent() is GDSubMenuButton:
parent = get_parent()
is_sub_menu = true
all_btns = []
for child in get_children():
if child is Button:
all_btns.append(child)
for btn in all_btns:
if btn is GDSubMenuButton:
btn.start(main_menu_button)
btn.btn_child_expanded.connect(_on_btn_child_expanded)
elif btn is Button:
if fold_on_child_button_pressed:
btn.pressed.connect(main_menu_button.expand_menu.bind(false))
btn.focus_mode = Control.FOCUS_NONE
btn.custom_minimum_size = MIN_SIZE
GDSubMenuButton.assign_texture_to_button_from_icon(btn, 10)
btn.add_to_group("UI")
expand_menu(false)
func _on_btn_child_expanded(btn_expanded: GDSubMenuButton, opened: bool) -> void:
for btn in all_btns:
if btn is GDSubMenuButton and btn != btn_expanded and opened:
btn.expand_menu(false)
func expand_menu(value: bool, except: GDSubMenuButton = null) -> void:
if !is_node_ready(): await ready
if all_btns.is_empty(): return
if parent:
if parent.tw:
if parent.tw.is_running() and parent.is_open:
return
is_open = value
if is_sub_menu:
var parent_center: Vector2 = parent.size/2
var this_center := size/2 + position
parent_dir = parent_center.direction_to(this_center)
var angle_start = 0
var angle_step = 0
if is_open and is_radial:
angle_start = - _radial_angle_rad/2 + parent_dir.angle()
angle_step = _radial_angle_rad / (all_btns.size()-1)
if tw:
tw.kill()
tw = create_tween()
tw.set_ease(Tween.EASE_IN_OUT)
tw.set_trans(Tween.TRANS_CUBIC)
for i in all_btns.size():
var btn: Button = all_btns[i]
if btn == except:
continue
var delay: float = i * anim_delay
var new_pos := size / 2.0 -(btn.size / 2.0)
# opening
if is_open:
if btn is GDSubMenuButton and btn.custom_pos != Vector2():
new_pos = btn.custom_pos.rotated(main_menu_button.parent_dir.angle())
elif is_radial:
new_pos += Vector2.from_angle(angle_start + i * angle_step) * (size.x + offset_gap)
else:
new_pos += parent_dir * (i+1) * (size.x + offset_gap)
btn.visible = true
# closing
else:
if btn is GDSubMenuButton:
btn.expand_menu(false)
tw.parallel().tween_property(btn, "visible", false, anim_duration).set_delay(delay)
tw.parallel().tween_property(btn, "position", new_pos, anim_duration).set_delay(delay)
tw.parallel().tween_property(btn, "modulate:a", 1 if is_open else 0, anim_duration).set_delay(delay)
btn_child_expanded.emit(self, is_open)
func toggle_menu():
expand_menu(!is_open)
func _process(d: float) -> void:
if is_dragged:
position = position.lerp(get_parent().get_local_mouse_position() - grabbed_at, d*10)
func _on_gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT:
is_mouse_click_down = event.is_pressed()
if is_mouse_click_down:
grabbed_at = event.position
elif is_dragged:
custom_pos = position.rotated(-main_menu_button.parent_dir.angle())
is_dragged = false
if event.button_index == MOUSE_BUTTON_RIGHT:
custom_pos = Vector2()
if is_sub_menu:
expand_menu(false)
parent.expand_menu(parent.is_open)
if event is InputEventMouseMotion and is_mouse_click_down:
is_dragged = true
if is_open:
expand_menu(false)
func _pressed() -> void:
if is_dragged: return
properly_pressed.emit()
static func assign_texture_to_button_from_icon(btn: Button, offset: float) -> void:
var tex = btn.icon
if not tex: return
btn.icon = null
var normal_col = btn.get_theme_color("icon_normal_color")
var pressed_col = btn.get_theme_color("icon_pressed_color")
var hover_col = btn.get_theme_color("icon_hover_color")
var tex_rect = TextureRect.new()
tex_rect.name = "ico"
tex_rect.texture = tex
tex_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE
tex_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
tex_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
tex_rect.modulate = normal_col
tex_rect.set_anchor_and_offset(SIDE_TOP, 0, offset)
tex_rect.set_anchor_and_offset(SIDE_LEFT, 0, offset)
tex_rect.set_anchor_and_offset(SIDE_RIGHT, 1, -offset)
tex_rect.set_anchor_and_offset(SIDE_BOTTOM, 1, -offset)
btn.mouse_exited.connect(func(): tex_rect.modulate = normal_col)
btn.mouse_entered.connect(func(): tex_rect.modulate = hover_col)
btn.pressed.connect(func(): tex_rect.modulate = pressed_col)
btn.add_child(tex_rect)
btn.size = btn.custom_minimum_size