Non-Editable Textbox on SurfaceGui shows cursor and prevents text from being painted

Users in my game are unable to paint the text on signs due to the textbox in the SurfaceGui always showing the cursor, preventing input from being detected.

I use textboxes because its easier to have a placeholder automatically swap out the text and colours whenever its empty rather than manually code it in.

Expected behavior

The textbox not to prevent input access and in the case of the textbox not being editable, not show the cursor.

3 Likes

Thanks for the report! I filed a ticket in our internal database.

1 Like

No need, this is actually working as intended. The whole reason why the TextEditable property of TextBoxes exist is so that the developer can allow users to still click into the textbox to highlight and copy text, being able to paste it elsewhere. In this case, Developer is using TextBoxes because they are relying off of the TextBox.PlaceholderText text to display when the TextBox is empty, which is bad practice. Ideally, Developer should use TextLabels combined with a check in Lua to replace empty text with the placeholder text. TextBoxes are designed for user input, not displaying text just for viewing. By requesting the user for input when clicked, they are doing their job as intended.

An example of how this can be implemented in Lua, assuming label references the TextLabel:

-- define our TextLabel. Sign is just a part in the workspace.
local label = workspace:WaitForChild("Sign"):WaitForChild("SurfaceGui"):WaitForChild("TextLabel")
-- define text strings and colors
-- these can also be StringValues/Color3Values under your TextLabel
local placeholderText = "Placeholder text"
local textColor = Color3.new(0,0,0)
local placeholderColor = Color3.new(1,0,1)
-- do some stuff to get to this point, of course
local textString = "asdasd"
label.Text = textString ~= "" and textString or placeholderText
label.TextColor3 = textString ~= "" and textColor or placeholderColor

In the above example, if the textString value is an empty string, the label’s text will be set to the value of placeholderText.

Another example of how this can be achieved, you can just put this anywhere in your script, and not have to worry about doing this every time you want to change the label’s text:

-- define our TextLabel
local label = workspace:WaitForChild("Sign"):WaitForChild("SurfaceGui"):WaitForChild("TextLabel")
-- define text strings and colors
local placeholderText = "Placeholder text"
local textColor = Color3.new(0,0,0)
local placeholderColor = Color3.new(1,0,1)
label:GetPropertyChangedSignal("Text"):Connect(function()
    local labelText = label.Text
    label.Text = labelText ~= "" and labelText or placeholderText
    label.TextColor3 = labelText ~= "" and textColor or placeholderColor
end)

Finally, a complex example of recursively searching the whole workspace for TextLabels with a StringValue named PlaceholderText. This allows each label to have it’s own unique placeholder text and (optionally) a Color3Value named PlaceholderColor3. Additionally, this handles unexpected parenting/reparenting and changing the placeholder text/color during gameplay (will not update in realtime, but rather the next time the label changes.) If the PlaceholderText value is removed, it will unbind the function. This also means that the color will stop updating, so if you had the placeholder text showing with a custom color, updating the label will not change it if the PlaceholderText is removed. This is intentional, as it allows you to do as they please with the label without getting any interference from the script if you remove the value from the label.

-- here, we have the ability to selectively put a StringValue on labels which we
-- want there to be a placeholder text on. For the optional, placeholder color,
-- we can include a Color3Value on the label as well

-- this table stores the labels' initial colors
-- we do this so that once it is changed to the placeholder color, it can go back
local initialColorStorage = {}

-- this function actually checks the text label and analyzes it on change,
-- giving it a placeholder text if empty
local function CheckTextLabel(label)
	local labelText = label.Text
	-- this code looks complicated because I don't want it to error
	-- if the values somehow get removed from the label
	local placeholderTextValue = label:FindFirstChild("PlaceholderText")
	label.Text = labelText ~= "" and labelText or (placeholderTextValue and placeholderTextValue.Value or "")
	if initialColorStorage[label] then
		local placeholderColorValue = label:FindFirstChild("PlaceholderColor3")
		label.TextColor3 = labelText ~= "" and initialColorStorage[label] or (placeholderColorValue and placeholderColorValue.Value or label.TextColor3)
	end
end

-- set up the event listeners based off of the value
local function SetupBindings(object, value)
	if value.Name == "PlaceholderText" then
		local binding = object:GetPropertyChangedSignal("Text"):Connect(function()
			CheckTextLabel(object)
		end)
		object.ChildRemoved:Connect(function(removed)
			if removed == value then
				binding:Disconnect()
			end
		end)
		object.Destroying:Connect(function()
			binding:Disconnect()
		end)
	elseif value.Name == "PlaceholderColor3" then
		initialColorStorage[object] = object.TextColor3
	end
end

-- add event listeners to when a new object is added
local function ListenForChildren(object)
	object.ChildAdded:Connect(function(child)
		SetupBindings(object, child)
	end)
end

-- check object to see if it is a TextLabel/TextButton, if it is, continue
-- added TextButton in case you want to make the sign's label clickable
local function CheckObject(object)
	if object.ClassName == "TextLabel" or object.ClassName == "TextButton" then
		if object:FindFirstChild("PlaceholderText") then
			if object:FindFirstChild("PlaceholderColor3") then
				initialColorStorage[object] = object.TextColor3
			end
			SetupBindings(object, object.PlaceholderText)
		end
		-- listen for new children due to replication lag or for changes
		ListenForChildren(object)
	end
end

-- function recursively searches workspace for any labels with PlaceholderText
-- string value. if found, it will continue with the script
local function RecursiveSearchTextLabels(object)
	local children = object:GetChildren()
	for i = 1, #children do
		local child = children[i]
		CheckObject(child)
		RecursiveSearchTextLabels(child)
	end
end

-- final function that sets up everything
local function SetupEverything(object)
	-- listen for descendants being added. this is due to replication lag
	object.DescendantAdded:Connect(function(object)
		if object.ClassName == "TextLabel" or object.ClassName == "TextButton" then
			CheckObject(object)
		end
	end)
	-- recursively search for already existing labels
	RecursiveSearchTextLabels(object)
end

-- where to search for labels. if you don't want it searching the entire
-- workspace, but rather a subsection of it, you can define where to search here
-- for example: workspace:WaitForChild("LandPlots"):WaitForChild("Signs")
SetupEverything(workspace)

I know that this isn’t as simple as using the TextBox hack, but sometimes, doing things the proper way is worth it in the long run for user experience. Say Roblox were to, in the future, add the ability for VR controllers to interact with TextBoxes on SurfaceGuis. Your users would probably be confused as to why a virtual keyboard pops up when they hover their controller over your TextBox sign. Doing things the proper way mitigates these issues and your users will thank you.

edit: Updated functions to be local instead of global (which is faster), had to reorder functions

1 Like

Closing this bug as this is more of a feature request to optionally not show cursors/highlights rather than a bug.