How to find screen position of textlabel letter

How would I find the screen position of a letter from a textlabel?

For example, positioning a frame over the “e” (shown below)

1 Like

It’s not really possible since text characters are different pixel sizes and AFAIK Roblox doesn’t have advanced UI support/pixel detection.

Assuming it’s a single line of horizontal text, you could get the X position of the desired letter. You’d have to use AutomaticSize with the preceding characters of the position you want.

For Example:
If you wanted the X position of “h”, you could make an AutomaticSized (X) TextLabel with “hello t” as the text (with the same text size), which you can then read the AbsoluteSize.X which will be the X position of the next letter (h).

You would still need to keep the new overlaying frame the same size and position as the Y axis since there isn’t really a way to detect the Y position/size AFAIK. The Y size, even with AbsoluteSize.Y, is still the same height as the tallest character in the font. The overlaying frame would give you a highlighter effect.

hello there

This is something missing that annoys me ALOT, I really wish roblox implemented a function to allow us to get the position of characters in text

3 Likes

I totally agree, this should definitely be a built in feature. I’ve actually totally forgotten about this topic and scrapped what I was working on. Today I’m going to try to find another way to do this and I’ll update the post if I find a way

1 Like

I made module that can help you to solve this problem

here it is:

local module = {}
function module.GetData(TextBox)
	local TextService = game:GetService("TextService") 
	local Height = 0
	local function CreateArray(box)
		local tb = Instance.new("TextBox")
		tb.Parent = script
		tb.FontFace = box.FontFace
		if box.TextScaled == true then
			tb.TextSize = box.TextBounds.Y
		else
			tb.TextSize = box.TextSize
		end
		Height = tb.TextSize
		tb.TextWrapped = false
		local Array = {}
		local symbols = box.Text
		Array = {}
		for i = 1,string.len(symbols),1 do
			local char = string.sub(symbols,i,i)
			tb.Text = char
			Array[char] = {X = tb.TextBounds.X}
		end
		tb.Text = string.char(32)
		Array[string.char(32)] = {X = tb.TextBounds.X}
		tb:Destroy()
		return Array
	end
	local function GetBound(text,Array)
		local Bound = 0
		for i = 1,string.len(text),1 do
			local char = string.sub(text,i,i)
			local xSize = Array[char]
			if xSize then
				Bound +=xSize.X
			end
		end
		return Bound
	end
	local function GetCharData(Text,pos,BoundsX,Array)
		local Lines = GetBound(Text,Array)/TextBox.AbsoluteSize.X
		if Lines %1 ~= 0 then
			Lines -= Lines% 1
			Lines += 1
		end
		local LineHeight = TextBox.LineHeight * Height
		local starterPos
		if TextBox.TextWrapped and Lines >=2 then
			starterPos = pos - UDim2.new(0,TextBox.AbsoluteSize.X/2,0,TextBox.TextBounds.Y/2 - LineHeight * 0.5)
		else
			starterPos = UDim2.new(0,TextBox.AbsolutePosition.X + TextBox.AbsoluteSize.X / 2,0,TextBox.AbsolutePosition.Y + TextBox.AbsoluteSize.Y / 2) -  UDim2.new(0,(BoundsX /2) ,0,0)
		end
		local Spacings = {}
		local Inc = 0
		for i = 1, string.len(Text),1 do
			local char = string.sub(Text,i,i)
			local xSize = Array[char]
			if xSize then
				table.insert(Spacings,{character = char,position = starterPos,size = UDim2.new(0,xSize.X,0,Height)})
				starterPos += UDim2.new(0, xSize.X,0,0)
				Inc += xSize.X
			end
			if Inc >= BoundsX and TextBox.TextWrapped == true then
				break
			end
		end
		
		return Spacings
	end
	local Array = CreateArray(TextBox)
	local Data = GetCharData(TextBox.Text,TextBox.Position,TextBox.TextBounds.X,Array)
	return Data
end

return module

How to use?
module.GetData(TextBox) returns array of data for each letter in TextBox.Text
example of data for textbox, which texbox.Text is “H”

data = {
 	{
		character = 'H',
		size = UDim2.new(0,9,0,8),
		position = UDim2.new(0,939,0,468)
	}
}

Code example:

local module = nil -- Enter here your path to module
local ScreenGui = Instance.new("ScreenGui") -- creates ScreenGui
ScreenGui.Parent = game:GetService("StarterGui")
local tb = Instance.new("TextBox") -- creates TextBox
tb.Size = UDim2.new(0.5,0,0.5,0)
tb.AnchorPoint = Vector2.new(0.5,0.5)
tb.Position = UDim2.new(0.5,0,0.5,0)
tb.Text = "Hooray"
tb.Parent = ScreenGui
local data = module.GetData(tb) 

for i,v in ipairs(data) do
	print(v.character,v.size,v.position)
end

Output:

5 Likes

Yo, does this only work if you aren’t using TextWrapped? Are there any rules to using it? I’ve been trying to get it to work and it doesn’t work quite right (even if TextWrapped is off).

Note that this module will only work on simple UIs like this:

Whenever I try to use it on more complex UI it just gives wrong position:

Also would be nice if module had VisualizeData method (like on your example), would be easier to debug any issues like mine.
I will try to rewrite this module, so it will be more usable on large projects and reply to this message when I will finish it.


*(had to heavily downscale videos since roblox 10mb limit)*

Forgot a bit about this thread, but here’s a new module:

local characterInfo = {}

--[[

|- Name:
TextObjects Characters Info

|- Author(s):
^ kercig: Wrote original module
^ frostxoff: Slightly rewrote module

|- Description:
Get characters data from TextObject (TextBoxes, TextLabels, TextButtons)

|- Methods:
- characterInfo:GetDataFromTextObject(TextBox: TextBox | TextLabel | TextButton): {characterdata}
^ Returns an array of characterdata for each letter in TextBox.Text
- characterInfo:VisualizeCharacterData(data: {characterdata}, gui: ScreenGui)
^ Visualizes characters data inside `gui`

]]--

local Debris = game:GetService("Debris")

export type characterdata = {
	character: string,
	size: UDim2,
	position: UDim2
}

function characterInfo:VisualizeCharacterData(data: {characterdata}, gui: ScreenGui, deleteafter: number | nil): nil
	for i, cdata: characterdata in pairs(data) do
		local newFrame = Instance.new("Frame")
		newFrame.Transparency = .75
		newFrame.Position = cdata.position
		newFrame.Size = cdata.size
		newFrame.Name = cdata.character
		
		newFrame.Parent = gui
	
		if deleteafter then
			Debris:AddItem(newFrame, deleteafter)
		end
	end
end

function characterInfo:GetDataFromTextObject(TextObject: TextBox | TextLabel | TextButton): {characterdata}
	local Height = 0
	
	local function CreateArray(box)
		local tb = Instance.new("TextBox")
		tb.Parent = script
		tb.FontFace = box.FontFace
		if box.TextScaled == true then
			tb.TextSize = box.TextBounds.Y
		else
			tb.TextSize = box.TextSize
		end
		Height = tb.TextSize
		tb.TextWrapped = false
		local Array = {}
		local symbols = box.Text
		Array = {}
		for i = 1,string.len(symbols),1 do
			local char = string.sub(symbols,i,i)
			tb.Text = char
			Array[char] = {X = tb.TextBounds.X}
		end
		tb.Text = string.char(32)
		Array[string.char(32)] = {X = tb.TextBounds.X}
		tb:Destroy()
		
		return Array
	end
	local function GetBound(text,Array)
		local Bound = 0
		for i = 1,string.len(text),1 do
			local char = string.sub(text,i,i)
			local xSize = Array[char]
			if xSize then
				Bound += xSize.X
			end
		end
		
		return Bound
	end
	
	local function GetCharData(Text,pos,BoundsX,Array)
		local Lines = GetBound(Text,Array)/TextObject.AbsoluteSize.X
		if Lines %1 ~= 0 then
			Lines -= Lines% 1
			Lines += 1
		end
		local LineHeight = TextObject.LineHeight * Height
		local starterPos
		if TextObject.TextWrapped and Lines >=2 then
			starterPos = pos - UDim2.new(0,TextObject.AbsoluteSize.X/2,0,TextObject.TextBounds.Y/2 - LineHeight * 0.5)
		else
			local textxalignment = TextObject.TextXAlignment
			local textyalignment = TextObject.TextYAlignment
			
			local xPos = TextObject.AbsolutePosition.X + TextObject.AbsoluteSize.X/2 - TextObject.TextBounds.X/2
			if textxalignment == Enum.TextXAlignment.Left then
				xPos = TextObject.AbsolutePosition.X
			elseif textxalignment == Enum.TextXAlignment.Right then
				xPos = TextObject.AbsolutePosition.X + TextObject.AbsoluteSize.X - TextObject.TextBounds.X
			end
			
			local yPos = TextObject.AbsolutePosition.Y + TextObject.AbsoluteSize.Y/2 - TextObject.TextBounds.Y/2
			if textyalignment == Enum.TextYAlignment.Top then
				yPos = TextObject.AbsolutePosition.Y
			elseif textyalignment == Enum.TextYAlignment.Bottom then
				yPos = TextObject.AbsolutePosition.Y + TextObject.AbsoluteSize.Y - TextObject.TextBounds.Y
			end
			
			starterPos = UDim2.new(0, xPos, 0, yPos)
		end
		local Spacings = {}
		local Inc = 0
		for i = 1, string.len(Text),1 do
			local char = string.sub(Text,i,i)
			local xSize = Array[char]
			if xSize then
				table.insert(Spacings,{character = char,position = starterPos,size = UDim2.new(0,xSize.X,0,Height)})
				starterPos += UDim2.new(0, xSize.X,0,0)
				Inc += xSize.X
			end
			if Inc >= BoundsX and TextObject.TextWrapped == true then
				break
			end
		end

		return Spacings
	end
	
	local Array = CreateArray(TextObject)
	local Data = GetCharData(TextObject.Text,TextObject.Position,TextObject.TextBounds.X,Array)
	
	return Data
end

return characterInfo

Now it supports TextXAlignment, TextYAlignment, uses TextObject (TextBoxes, TextLabels, TextButtons) instead of TextBox and has VisualizeCharacterData method, make sure that both visualization GUI and GUI that have your TextObject have same IgnoreGuiInset property value.

2 Likes