Ui nodes won't detect mouse input

Godot Version

4.7

Question

I’m kinda confused ass to why or how this is happening, but the title says it, I’m trying to recreate a “fake OS” for a game, and so far so good, but i had to make a custom window since the window node can’t have custom styles
I’ve managed to implement the basic of the ui (dragging from the top, resizing from a little icon on the bottom right, close, hide, minimize/maximize) this is what i have so far


Now this is where the issue comes in you see the UI inside the window (the items and then the button)? that UI won’t accept any mouse input nothing at all i made a signal to detect whether the mouse enter it but nothing
The window itself is just a script and through it i assemble the UI in the _ready() function i’m sure i set all the ui filter mode to pass, this is how the scene tree looks relative to the image above:

Base window is just the window with nothing and then scroll container have all of the labels and the button as seen in the image above

This is the code for the base window (without any ui added):

@tool
class_name BaseWindow extends Panel

@export var window_name: String = "Window"
@export var resize_image: Texture2D
@export var window_icon: Texture2D

var ui_container: Control
var window_title: Label 
var header_panel: Panel 
var resize_icon: TextureRect

var close_btn: TextureButton
var minimize_btn: TextureButton
var hide_tab_btn: TextureButton

var pickup_initial_position: Vector2 = Vector2.ZERO
var mouse_pos: Vector2 = Vector2.ZERO

var is_holding_resize: bool = false
var is_grabbed: bool = false
var is_focused: bool = true
var is_minimized: bool = true

const MINIMUM_SIZE: Vector2 = Vector2(300, 100)
const HEADER_MINIMUM_SIZE: Vector2 = Vector2(MINIMUM_SIZE.x, 40)
const BTN_MINIMUM_SIZE: Vector2 = Vector2(32, 32)
const BTN_CONTAINER_OFFSET: float = -112.0

const HEADER_STYLEBOX: StyleBoxTexture = preload("uid://cdip1tf3mffjj")
const HEADER_STYLEBOX_UNFOCUSED: StyleBoxTexture = preload("uid://c4xogvurfn8e3")

const CLOSE_BTN_STATES: ButtonStates = preload("uid://q4jcid8euesy")
const MINI_BTN_STATES: ButtonStates = preload("uid://bsbc206hb24vf")
const HIDE_BTN_STATES: ButtonStates = preload("uid://bbkkv7mr5opoq")
#======================================================#
func _ready() -> void:
	create_scene()

func create_scene() -> void:
	clip_contents = true
	custom_minimum_size = MINIMUM_SIZE
	pivot_offset_ratio = Vector2(0.5, 0.5)
	
	var resize_icon_node_inst: TextureRect = TextureRect.new()
	resize_icon_node_inst.texture = resize_image
	resize_icon_node_inst.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
	resize_icon_node_inst.set_anchor_and_offset(SIDE_LEFT, 1, -28)
	resize_icon_node_inst.set_anchor_and_offset(SIDE_TOP, 1, -28)
	resize_icon = resize_icon_node_inst
	add_child(resize_icon_node_inst)
	
	var vBoxContainer: VBoxContainer = VBoxContainer.new()
	vBoxContainer.set_anchors_preset(Control.PRESET_FULL_RECT)
	
	var panel: Panel = Panel.new()
	panel.custom_minimum_size = HEADER_MINIMUM_SIZE
	panel.mouse_filter = Control.MOUSE_FILTER_PASS
	panel.add_theme_stylebox_override("panel", HEADER_STYLEBOX)
	header_panel = panel
	vBoxContainer.add_child(panel)
	
	#==================== Title Label + Icons ===================#
	var title_container: HBoxContainer = HBoxContainer.new()
	title_container.set_anchors_preset(Control.PRESET_CENTER_LEFT)
	title_container.grow_vertical = Control.GROW_DIRECTION_BOTH
	title_container.set_anchor_and_offset(SIDE_LEFT, 1, 5)
	
	var window_icon_node: TextureRect = TextureRect.new()
	window_icon_node.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
	window_icon_node.texture = window_icon
	window_icon_node.custom_minimum_size = Vector2(24, 24)
	
	var title_lbl: Label = Label.new()
	title_lbl.add_theme_color_override("font_color", Color.WHITE)
	title_lbl.text = window_name
	
	title_container.add_child(window_icon_node)
	title_container.add_child(title_lbl)
	
	#==================== Buttons Label + Icons ===================#
	var btns_container: HBoxContainer = HBoxContainer.new()
	btns_container.set_anchors_preset(Control.PRESET_CENTER_RIGHT)
	btns_container.grow_vertical = Control.GROW_DIRECTION_BOTH
	btns_container.grow_horizontal = Control.GROW_DIRECTION_BOTH
	btns_container.set_anchor_and_offset(SIDE_RIGHT, 1, BTN_CONTAINER_OFFSET)
	
	var quit_btn: TextureButton = TextureButton.new()
	quit_btn.stretch_mode = TextureButton.STRETCH_SCALE
	quit_btn.custom_minimum_size = BTN_MINIMUM_SIZE
	quit_btn.texture_normal = CLOSE_BTN_STATES.button_normal
	quit_btn.texture_hover = CLOSE_BTN_STATES.button_normal
	quit_btn.texture_pressed = CLOSE_BTN_STATES.button_pressed
	quit_btn.texture_disabled = CLOSE_BTN_STATES.button_disabled
	quit_btn.pressed.connect(_on_quit_window_btn_pressed)
	close_btn = quit_btn
	
	var mini_btn: TextureButton = TextureButton.new()
	mini_btn.stretch_mode = TextureButton.STRETCH_SCALE
	mini_btn.custom_minimum_size = BTN_MINIMUM_SIZE
	mini_btn.texture_normal = MINI_BTN_STATES.button_normal
	mini_btn.texture_hover = MINI_BTN_STATES.button_normal
	mini_btn.texture_pressed = MINI_BTN_STATES.button_pressed
	mini_btn.texture_disabled = MINI_BTN_STATES.button_disabled
	mini_btn.pressed.connect(_on_minimize_btn_pressed)
	minimize_btn = mini_btn
	
	var hide_btn: TextureButton = TextureButton.new()
	hide_btn.stretch_mode = TextureButton.STRETCH_SCALE
	hide_btn.custom_minimum_size = BTN_MINIMUM_SIZE
	hide_btn.texture_normal = HIDE_BTN_STATES.button_normal
	hide_btn.texture_hover = HIDE_BTN_STATES.button_normal
	hide_btn.texture_pressed = HIDE_BTN_STATES.button_pressed
	hide_btn.texture_disabled = HIDE_BTN_STATES.button_disabled
	
	hide_btn.pressed.connect(_on_hide_tab_btn_pressed)
	hide_tab_btn = hide_btn
	
	btns_container.add_child(hide_btn)
	btns_container.add_child(mini_btn)
	btns_container.add_child(quit_btn)
	
	#==============================================================#
	panel.add_child(title_container)
	panel.add_child(btns_container)
	
	#var scroll_cont: ScrollContainer = ScrollContainer.new()
	#scroll_cont.size_flags_vertical = Control.SIZE_EXPAND_FILL
	#scroll_cont.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	#
	#var margin_node: MarginContainer = MarginContainer.new()
	#margin_node.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	#margin_node.size_flags_vertical = Control.SIZE_EXPAND_FILL
	#margin_node.add_theme_constant_override("margin_left", 15)
	#margin_node.add_theme_constant_override("margin_right", 15)
	#margin_node.add_theme_constant_override("margin_top", 15)
	#margin_node.add_theme_constant_override("margin_bottom", 15)
	#
	#ui_container = margin_node
	#scroll_cont.add_child(margin_node)
	#
	#vBoxContainer.add_child(scroll_cont)
	vBoxContainer.mouse_filter = Control.MOUSE_FILTER_PASS
	add_child(vBoxContainer)
	

func _physics_process(_delta: float) -> void:
	if not Engine.is_editor_hint():
		mouse_pos = get_global_mouse_position()
		if not is_holding_resize:
			_handel_panel_placement()
		
		_resize_panel()
		_set_focus_state()

func _set_focus_state() -> void:
	if Input.is_action_just_pressed("mouse_1"):
		is_focused = get_global_rect().has_point(mouse_pos)
	
	if is_focused or is_holding_resize:
		header_panel.add_theme_stylebox_override("panel", HEADER_STYLEBOX)
		z_index = 1
	else:
		header_panel.add_theme_stylebox_override("panel", HEADER_STYLEBOX_UNFOCUSED)
		z_index = 0

func _handel_panel_placement() -> void:
	var viewport_size: Vector2 = get_viewport_rect().size
	if Input.is_action_pressed("mouse_1"):
		if header_panel.get_global_rect().has_point(mouse_pos):
			is_grabbed = true
			if pickup_initial_position == Vector2.ZERO:
				pickup_initial_position = mouse_pos - global_position
	else:
		is_grabbed = false
		pickup_initial_position = Vector2.ZERO
	
	if is_grabbed:
		global_position = mouse_pos - pickup_initial_position
	
	global_position = global_position.clamp(Vector2.ZERO, viewport_size - Vector2(50,header_panel.size.y))


func _resize_panel() -> void:
	if Input.is_action_pressed("mouse_1"):
		if resize_icon.get_global_rect().has_point(mouse_pos):
			is_holding_resize = true
	if Input.is_action_just_released("mouse_1") and is_holding_resize:
		is_holding_resize = false
	
	if is_holding_resize and mouse_pos > global_position:
		var actual_size: Vector2 = mouse_pos - global_position
		size = (get_minimum_size() + actual_size.abs())
		header_panel.size.x = size.x

func _on_quit_window_btn_pressed() -> void:
	hide()

func _on_hide_tab_btn_pressed() -> void:
	hide()

func _on_minimize_btn_pressed() -> void:
	if not is_minimized:
		# make minimized
		global_position = (size / 2)- MINIMUM_SIZE/2
		size = MINIMUM_SIZE
		is_minimized = true
	else:
		# make fullscreen
		global_position = Vector2.ZERO
		size = get_parent().size
		is_minimized = false

I’ve also tried using the misc tab in the debugger but it dosen’t detect the scroll container:


Right here i clicked in the center which should show the scroll container node but it only detect the VBoxContainer that was created in the _ready() function when the window panel was added
I have no idea why this happens or how, so i hope i didn’t hit an engine limitation or something

Looks like your VBoxContainer is stopping the click instead of passing it. Check in the inspector to make sure the Mouse Filter is set to Pass.

I have set up all the nodes to pass the mouse input the vBoxContainer also have it at the end of the create_scene() function. But again, BaseWindow is just a script and all the nodes are created inside create_scene() which is called in the _ready() function but you are correct, something is eating up all the input for whatever reason because as soon as i moved the buttons outside the panel ore set “top level” to true it started picking up inputs again
i think this is an engine bug

Ok, well the VBox is getting the mouse input, so all that is working. Keep in mind that anything that does NOT inherit Container does not have mouse filter set to pass.

Then it’s in your code. But TBH, I don’t really want to read all your code. Because, well, there’s no realy point to it. You could just create a scene, test it and instantiate it. Lot less code, and a lot less debugging said code. So I recommend building a small version of it using Control nodes in the editor. I imagine in doing so, you’ll find the problem.

It’s unlikely to be an engine bug, but you can log it if you like. You’ll need to make a Minimal Reproduction Project. Which, again, means building it to prove that it’s an engine bug and not your code.

yea instead of brute-forcing this to make it work maybe i need to change the approach all together, i’ll post anything here if i manage to come up with something in case someone wants to code

i kinda found a “solution”, which is just a reworked logic instead of making it a full panel i just made it into a header which handles the window reposition, minimizing and closing but this has to mean i need to set up the window nodes manually each time i need to make a new window this is how the code look like:

@tool
class_name BaseWindow extends Panel

@export var window_node: Control
@export var resize_icon: TextureRect
@export var window_name: String = "Window"
@export var window_icon: Texture2D

var window_title: Label
var close_btn: TextureButton
var minimize_btn: TextureButton
var hide_tab_btn: TextureButton

var pickup_initial_position: Vector2 = Vector2.ZERO
var mouse_pos: Vector2 = Vector2.ZERO

var is_holding_resize: bool = false
var is_grabbed: bool = false
var is_focused: bool = true
var is_minimized: bool = true

const MINIMUM_SIZE: Vector2 = Vector2(300, 100)
const HEADER_MINIMUM_SIZE: Vector2 = Vector2(MINIMUM_SIZE.x, 40)
const BTN_MINIMUM_SIZE: Vector2 = Vector2(32, 32)
const BTN_CONTAINER_OFFSET: float = -112.0

const HEADER_STYLEBOX: StyleBoxTexture = preload("uid://cdip1tf3mffjj")
const HEADER_STYLEBOX_UNFOCUSED: StyleBoxTexture = preload("uid://c4xogvurfn8e3")

const CLOSE_BTN_STATES: ButtonStates = preload("uid://q4jcid8euesy")
const MINI_BTN_STATES: ButtonStates = preload("uid://bsbc206hb24vf")
const HIDE_BTN_STATES: ButtonStates = preload("uid://bbkkv7mr5opoq")
#======================================================#
func _ready() -> void:
	# clean up
	for child in get_children():
		child.queue_free()
	
	_initialize_scene()

func _initialize_scene() -> void:
	mouse_filter = Control.MOUSE_FILTER_PASS
	custom_minimum_size = HEADER_MINIMUM_SIZE
	add_theme_stylebox_override("panel", HEADER_STYLEBOX)
	
	#==================== Title Label + Icons ===================#
	var title_container: HBoxContainer = HBoxContainer.new()
	title_container.mouse_filter = Control.MOUSE_FILTER_PASS
	title_container.set_anchors_preset(Control.PRESET_CENTER_LEFT)
	title_container.grow_vertical = Control.GROW_DIRECTION_BOTH
	title_container.set_anchor_and_offset(SIDE_LEFT, 1, 5)
	
	var window_icon_node: TextureRect = TextureRect.new()
	window_icon_node.mouse_filter = Control.MOUSE_FILTER_PASS
	window_icon_node.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
	window_icon_node.texture = window_icon
	window_icon_node.custom_minimum_size = Vector2(24, 24)
	
	var title_lbl: Label = Label.new()
	title_lbl.mouse_filter = Control.MOUSE_FILTER_PASS
	title_lbl.add_theme_color_override("font_color", Color.WHITE)
	title_lbl.text = window_name
	
	title_container.add_child(window_icon_node)
	title_container.add_child(title_lbl)
	
	#==================== Buttons Label + Icons ===================#
	var btns_container: HBoxContainer = HBoxContainer.new()
	btns_container.mouse_filter = Control.MOUSE_FILTER_PASS
	btns_container.set_anchors_preset(Control.PRESET_CENTER_RIGHT)
	btns_container.grow_vertical = Control.GROW_DIRECTION_BOTH
	btns_container.grow_horizontal = Control.GROW_DIRECTION_BOTH
	btns_container.set_anchor_and_offset(SIDE_RIGHT, 1, BTN_CONTAINER_OFFSET)
	
	var quit_btn: TextureButton = TextureButton.new()
	quit_btn.mouse_filter = Control.MOUSE_FILTER_PASS
	quit_btn.stretch_mode = TextureButton.STRETCH_SCALE
	quit_btn.custom_minimum_size = BTN_MINIMUM_SIZE
	quit_btn.texture_normal = CLOSE_BTN_STATES.button_normal
	quit_btn.texture_hover = CLOSE_BTN_STATES.button_normal
	quit_btn.texture_pressed = CLOSE_BTN_STATES.button_pressed
	quit_btn.texture_disabled = CLOSE_BTN_STATES.button_disabled
	
	quit_btn.pressed.connect(_on_quit_window_btn_pressed)
	close_btn = quit_btn
	
	var mini_btn: TextureButton = TextureButton.new()
	mini_btn.mouse_filter = Control.MOUSE_FILTER_PASS
	mini_btn.stretch_mode = TextureButton.STRETCH_SCALE
	mini_btn.custom_minimum_size = BTN_MINIMUM_SIZE
	mini_btn.texture_normal = MINI_BTN_STATES.button_normal
	mini_btn.texture_hover = MINI_BTN_STATES.button_normal
	mini_btn.texture_pressed = MINI_BTN_STATES.button_pressed
	mini_btn.texture_disabled = MINI_BTN_STATES.button_disabled
	
	mini_btn.pressed.connect(_on_minimize_btn_pressed)
	minimize_btn = mini_btn
	
	var hide_btn: TextureButton = TextureButton.new()
	hide_btn.mouse_filter = Control.MOUSE_FILTER_PASS
	hide_btn.stretch_mode = TextureButton.STRETCH_SCALE
	hide_btn.custom_minimum_size = BTN_MINIMUM_SIZE
	hide_btn.texture_normal = HIDE_BTN_STATES.button_normal
	hide_btn.texture_hover = HIDE_BTN_STATES.button_normal
	hide_btn.texture_pressed = HIDE_BTN_STATES.button_pressed
	hide_btn.texture_disabled = HIDE_BTN_STATES.button_disabled
	
	hide_btn.pressed.connect(_on_hide_tab_btn_pressed)
	hide_tab_btn = hide_btn
	
	btns_container.add_child(hide_btn)
	btns_container.add_child(mini_btn)
	btns_container.add_child(quit_btn)
	
	#==============================================================#
	add_child(title_container)
	add_child(btns_container)

func _physics_process(_delta: float) -> void:
	if not Engine.is_editor_hint():
		mouse_pos = get_global_mouse_position()
		if not is_holding_resize:
			_handel_panel_placement()
		
		if resize_icon != null:
			_resize_panel()
		_set_focus_state()

func _set_focus_state() -> void:
	if Input.is_action_just_pressed("mouse_1"):
		is_focused = get_global_rect().has_point(mouse_pos)
	
	if is_focused or is_holding_resize:
		add_theme_stylebox_override("panel", HEADER_STYLEBOX)
		#z_index = 1
	else:
		add_theme_stylebox_override("panel", HEADER_STYLEBOX_UNFOCUSED)
		#z_index = 0

func _handel_panel_placement() -> void:
	var viewport_size: Vector2 = get_viewport_rect().size
	if Input.is_action_pressed("mouse_1"):
		if get_global_rect().has_point(mouse_pos):
			is_grabbed = true
			if pickup_initial_position == Vector2.ZERO:
				pickup_initial_position = mouse_pos - window_node.global_position
	else:
		is_grabbed = false
		pickup_initial_position = Vector2.ZERO
	
	if is_grabbed:
		window_node.global_position = mouse_pos - pickup_initial_position
	
	window_node.global_position = window_node.global_position.clamp(Vector2.ZERO, viewport_size - Vector2(50,size.y))

func _resize_panel() -> void:
	if Input.is_action_pressed("mouse_1"):
		if resize_icon.get_global_rect().has_point(mouse_pos):
			is_holding_resize = true
	if Input.is_action_just_released("mouse_1") and is_holding_resize:
		is_holding_resize = false
	
	if is_holding_resize and mouse_pos > window_node.global_position:
		var actual_size: Vector2 = mouse_pos - window_node.global_position
		window_node.size = (get_minimum_size() + actual_size.abs())
		size.x = window_node.size.x
	
	window_node.size = window_node.size.clamp(MINIMUM_SIZE, Vector2.INF)

func _on_quit_window_btn_pressed() -> void:
	window_node.hide()

func _on_hide_tab_btn_pressed() -> void:
	window_node.hide()

func _on_minimize_btn_pressed() -> void:
	if not is_minimized:
		# make minimized
		window_node.global_position = (size / 2)- MINIMUM_SIZE/2
		window_node.size = MINIMUM_SIZE
		is_minimized = true
	else:
		# make fullscreen
		window_node.global_position = Vector2.ZERO
		window_node.size = get_parent().size
		is_minimized = false

and again each time i have to make a new panel i need to set it up manually (just the background and the resizing nodes)


the BaseWindow (the code above) creates the header which is the blue part the rest i have to manually do each time a new window need to be created, i found that doing the background as a panel, then using a vbox to organize the content is cleaner
But yea i still don’t know why it didn’t pick up inputs before