diff --git a/lib/UI/GDSubMenuButton.gd b/lib/UI/GDSubMenuButton.gd new file mode 100644 index 00000000..fba69732 --- /dev/null +++ b/lib/UI/GDSubMenuButton.gd @@ -0,0 +1,199 @@ +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 diff --git a/lib/UI/GDSubMenuButton.gd.uid b/lib/UI/GDSubMenuButton.gd.uid new file mode 100644 index 00000000..e3a18673 --- /dev/null +++ b/lib/UI/GDSubMenuButton.gd.uid @@ -0,0 +1 @@ +uid://8715vyt7prmd