just wondering if any of you wonderful fellow have got any solutions to a wordwrap API as you type. I already have a word wrap function, but it it only wraps static content, I need something that will wrap as I type, so I give it a x, y, height and width, and then it will stay between that section and allow me to type and it shall auto line, and if I get to the bottom of the box I give it, it will move a line up and hide the line. So for example:
I have a box that is 1, 10 across and only goes two lines down (so two lines to type on), I want to be able to type like so:
Hello worl <- at this point it needs to auto line me down, and do this:
Hello
World
Now if I try and add to that like so:
Hello
all you gu <- here it needs to move a line up and append the current word to the next line so it looks like this:
all you
guys
As hello is not in the text box, I wish to do something like that? Any ideas if it has already been done because i really do not wish to try this xD I won't get far.
For you guys who know HTML & CSS I wish to do a normal text box and use an overflow-y: height, so yeah, any ideas? />...?
EDIT:
Even have a scroll function to scroll through the text? That would be super awesome!
I would just go for the conventional approach, and check for every new character added(I don't know wether you are wanting to allow changes/deletions), if it fits into the current line. If not, take all the characters in front of it until you hit a space and wrap it at that point to the new line.
Whenever a new line doesn't fit into the current box, you would scroll up your text.
Clamping the text so that it wraps to the correct X coordinate and also hides lines out of the boundary should be fairly easy to implement also
I hope that is to some extent the answer you are looking for. If you are requesting a fully-featured textbox as exmple code, I cannot help you with that. I'm sure though, that there are already plenty of textboxes out in the wild working similar to what you described.
I actually wrote one few days ago for the Bedrock framework for my own SocialClient (a GUI for Danny's SocialNet). I will try to explain how it works when I get home. But one note: the wrapped lines should only be an 'illusion', the textbox on the code side should see the lines as one line.
BTW, I posted a tweet about the textbox on the SocialNet through my client which (the tweet) I wrote in that textbox
+ backspace, space, enter etc, and it needs to just be like a text box in html, but it will have an overflow so when you say you go over the max height then it will not show the first few lines and only show the last lines that fit in the box, and it auto lines when you press enter and then yeah idk, like OEED's ink can do? But it is buggy as hell so maybe a better version?
.
I don't want it to be awkward to use or have new characters I want it to be lua just it displays each line as a table entry..
{
"line1",
"line2",
}
and each line is wrapped so it will fit inside the width length.
function data.wordwrap(str, limit)
limit = limit or 72
local here = 1
local buf = ""
local t = {}
str:gsub("(%s*)()(%S+)()",
function(sp, st, word, fi)
if fi-here > limit then
--# Break the line
here = st
table.insert(t, buf)
buf = word
else
buf = buf..sp..word --# Append
end
end)
--# Tack on any leftovers
if(buf ~= "") then
table.insert(t, buf)
end
return t
end
I need one as I type... which works so it auto lines as I type and text stays between a set width and height, like a text box in html...
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:
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
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 . 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
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:
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
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 . 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
Well thank you for the explanation, I do not use bedrock, but I will look for something similar online:)