So here I'm going to explain how my multiline textbox works with a text wrapping feature.
First off, don't think about wrapping text yet. Actually the wrapping of text is only needed when
displaying the text, most of textbox's functions are independent of the text wrapping! To store the text we store each separate line as a separate index in a table. For example:
local Text = {
"line 1",
"this is line 2"
}
Note: each line is not an actual line of already wrapped text. Each line is more like a paragraph - at the end of each line (paragraph) there's a newline (\n) character. The text would look like this:
line 1
this is line 2
Now we'll need two variables to mark where our cursor is. One marks the line on which the cursor is (CursorY) and the other marks the character on which the cursor is (CursorX). So if the cursor is on the second line and just after the word 'line' (imagine that '_' is the cursor):
line 1
this is line_2
then our cursor's position would be: CursorY = 2; CursorX = 13. I'm counting this starting from the number 1, so the top-left corner would be: CursorY = 1; CursorX = 1.
Now when drawing the text, then we need to wrap it. Imagine the textbox's width is 8. So this is what it would look like when drawn:
+--------+
|line 1 |
|this is |
|line 2 |
+--------+
I made a function which returned two tables:
local SeparateLines, AllLines = GetWrappedText()
SeparateLines = {
{
"line 1"
},
{
"this is ",
"line 2"
}
}
AllLines = {
"line 1",
"this is ",
"line 2"
}
So the first, SeparateLines, table holds all the lines as they were in the original Text table, but returns each line wrapped. We can see that the first line's table holds only one string - that means the text wasn't wrapped because there was no need for it. In the second's line table we can see that there are two strings - that means that the second line's text was wrapped into two lines.
The second, AllLines, table just holds all the lines that we will display in order.
Now as for where to put the cursor on the display and how to react to keys like [enter] or [backspace] I'm not going to get into that. It's simply math, I'm not even sure how my textbox reacts to keys exactly :lol:/>
I suggest for the logic just take out a piece of paper, a pen and then just draw or write everything you need. Even though I wrote my textbox in only about a day it still was a challenge, especially when the cursor goes out of the textbox and then you type something in, the letters don't appear, but the cursor comes back onto the screen, and you try to write again, but then the character is written on the wrong spot, etc… You'll just have to write and test, write and test. Then sleep, eat, come back to the code and realize what you were doing wrong.
This is how my sheets of paper look after making my textbox:
It's a mess, isn't it :P/>. And here's the source code of my textbox (it's made to work with Bedrock). It uses my own text wrapping function which only wraps but doesn't truncate the string. That why there's a space in my example above in the string "this is ".
Spoiler
TextColour = colors.black
BackgroundColour = colors.lightGray
PlaceholderTextColour = colors.gray
Placeholder = ""
ScrollX = 0
ScrollY = 0
CursorX = 1
CursorY = 1
WrapText = true
Text = nil
OnInitialise = function (self)
self.Text = self.Text or {""}
if type(self.Text) ~= "table" then
self.Text = self.Bedrock.Helpers.Split(tostring(self.Text), "\n")
end
self.CursorY = #self.Text
self.CursorX = #self.Text[#self.Text] + 1
end
OnDraw = function (self, x, y)
local text, lines = self:GetDrawnText()
local cx, cy = self:GetAbsoluteCursorPosition()
Drawing.DrawBlankArea(x, y, self.Width, self.Height, self.BackgroundColour)
if #table.concat(self.Text, "\n") > 0 then
for i = 1 + self.ScrollY, math.min(#lines, self.Height + self.ScrollY) do
Drawing.DrawCharacters(x, y + i - self.ScrollY - 1, lines[i]:sub(self.ScrollX + 1), self.TextColour, self.BackgroundColour)
end
else
local placeholderText = Utils.WrapText(self.Placeholder, self.Width)
for i = 1 + self.ScrollY, math.min(#placeholderText, self.Height + self.ScrollY) do
Drawing.DrawCharacters(x, y + i - self.ScrollY - 1, placeholderText[i]:sub(self.ScrollX + 1), self.PlaceholderTextColour, self.BackgroundColour)
end
end
if self.Bedrock:GetActiveObject() == self and cx <= self.Width and cy <= self.Height and cx > 0 and cy > 0 then
self.Bedrock.CursorPos = {x + cx - 1, y + cy - 1}
self.Bedrock.CursorColour = self.TextColour
elseif self.Bedrock:GetActiveObject() == self then
self.Bedrock.CursorPos = nil
self.Bedrock.CursorColour = nil
end
end
OnClick = function (self, event, b, x, y)
self.Bedrock:SetActiveObject(self)
local lines, separatedLines = self:GetDrawnText()
x, y = x + self.ScrollX, y + self.ScrollY
local temp_clickY = math.min(#separatedLines, y)
local clickX = math.min(#separatedLines[temp_clickY] + 1, x)
local clickY, temp_y = 0, 0
for i, line in ipairs(lines) do
clickY = clickY + 1
temp_y = temp_y + #line
if temp_y >= temp_clickY then
for j = 1, temp_clickY - temp_y + #line - 1 do
clickX = clickX + #line[j]
end
break
end
end
self.CursorX = clickX
self.CursorY = clickY
end
OnScroll = function (self, event, dir, x, y)
if self.Bedrock:GetActiveObject() ~= self then
return false
end
local lines, separatedLines = self:GetDrawnText()
local maxHeight = #separatedLines
self.ScrollY = Utils.Clamp(self.ScrollY + dir, 0, maxHeight - 1)
end
OnKeyChar = function (self, event, k)
local prevText = table.concat(self.Text, "\n")
local line = self.Text[self.CursorY]
local lines, separatedLines = self:GetDrawnText()
local cx, cy = self:GetAbsoluteCursorPosition()
cx, cy = cx + self.ScrollX, cy + self.ScrollY
if event == "key" then
if k == keys.enter or k == keys.numPadEnter then
table.insert(self.Text, self.CursorY + 1, line:sub(self.CursorX))
self.Text[self.CursorY] = line:sub(0, self.CursorX - 1)
self.CursorY = self.CursorY + 1
self.CursorX = 1
elseif k == keys.backspace then
if self.CursorX <= 1 and self.CursorY > 1 then
self.CursorY = self.CursorY - 1
self.CursorX = #self.Text[self.CursorY] + 1
self.Text[self.CursorY] = self.Text[self.CursorY] .. table.remove(self.Text, self.CursorY + 1)
elseif self.CursorX > 1 then
self.Text[self.CursorY] = line:sub(0, self.CursorX - 2) .. line:sub(self.CursorX)
self.CursorX = self.CursorX - 1
end
elseif k == keys.delete then
if self.CursorX > #line and self.Text[self.CursorY + 1] then
self.Text[self.CursorY] = line .. table.remove(self.Text, self.CursorY + 1)
elseif self.CursorX <= #line then
self.Text[self.CursorY] = line:sub(0, self.CursorX - 1) .. line:sub(self.CursorX + 1)
end
elseif k == keys.right then
if self.CursorX > #line and self.Text[self.CursorY + 1] then
self.CursorX = 1
self.CursorY = self.CursorY + 1
elseif self.CursorX <= #line then
self.CursorX = self.CursorX + 1
end
elseif k == keys.left then
if self.CursorX == 1 and self.CursorY > 1 then
self.CursorY = self.CursorY - 1
self.CursorX = #self.Text[self.CursorY] + 1
elseif self.CursorX > 1 then
self.CursorX = self.CursorX - 1
end
elseif k == keys.up or k == keys.down then
local _cy = cy
for i = 1, self.CursorY - 1 do
_cy = _cy - #lines[i]
end
if k == keys.up and cy > 1 then
if _cy > 1 then
local w = 0
for i = 1, _cy - 2 do
w = w + #lines[self.CursorY][i]
end
self.CursorX = w + math.min(cx, #lines[self.CursorY][_cy - 1])
else
self.CursorY = self.CursorY - 1
local w = 0
for i = 1, #lines[self.CursorY] - 1 do
w = w + #lines[self.CursorY][i]
end
self.CursorX = w + math.min(cx, #lines[self.CursorY][#lines[self.CursorY]] + 1)
end
elseif k == keys.down and cy < #separatedLines then
if _cy < #lines[self.CursorY] then
local w = 0
for i = 1, _cy do
w = w + #lines[self.CursorY][i]
end
self.CursorX = w + math.min(cx, #lines[self.CursorY][_cy + 1])
else
self.CursorY = self.CursorY + 1
self.CursorX = math.min(cx, #lines[self.CursorY][1] + 1)
end
end
elseif k == keys.home then
self.CursorX = 1
elseif k == keys["end"] then
self.CursorX = #self.Text[self.CursorY] + 1
end
elseif event == "char" then
local line = self.Text[self.CursorY]
self.Text[self.CursorY] = line:sub(0, self.CursorX - 1) .. k .. line:sub(self.CursorX)
self.CursorX = self.CursorX + 1
end
local newText = table.concat(self.Text, "\n")
if newText ~= prevText and self.OnChange then
self:OnChange(newText)
end
self:UpdateScroll()
self:ForceDraw()
end
function UpdateScroll (self)
local cx, cy = self:GetAbsoluteCursorPosition()
cx, cy = cx + self.ScrollX, cy + self.ScrollY
if not self.WrapText then
if self.CursorX > self.Width + self.ScrollX then
self.ScrollX = self.CursorX - self.Width
elseif self.CursorX <= self.ScrollX + 1 then
self.ScrollX = math.max(self.CursorX - 2, 0)
end
end
if cy > self.Height + self.ScrollY then
self.ScrollY = cy - self.Height
elseif cy <= self.ScrollY + 1 then
self.ScrollY = math.max(cy - 1, 0)
end
end
function GetDrawnText (self)
local lines = {}
local separatedLines = {}
for i = 1, #self.Text do
lines[i] = {}
local wrapped = self.WrapText and Utils.WrapText(self.Text[i], self.Width - 1) or {self.Text[i]}
for j, line in ipairs(wrapped) do
line = line:gsub("\t", " ")
lines[i][j] = line
separatedLines[#separatedLines + 1] = line
end
end
return lines, separatedLines
end
function GetAbsoluteCursorPosition (self)
local lines = self:GetDrawnText()
local y = -self.ScrollY
local x = self.CursorX - self.ScrollX
for i = 1, self.CursorY - 1 do
y = y + #lines[i]
end
for i, line in ipairs(lines[self.CursorY]) do
y = y + 1
if x > #line + 1 then
x = x - #line
else
if i < #lines[self.CursorY] and x > #line then
y = y + 1
x = 1
end
break
end
end
return x, y
end