This is a read-only snapshot of the ComputerCraft forums, taken in April 2020.
DannySMc's profile picture

Wordwrap as you type

Started by DannySMc, 18 May 2015 - 10:30 AM
DannySMc #1
Posted 18 May 2015 - 12:30 PM
Hey guys,

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 &amp; CSS I wish to do a normal text box and use an overflow-y: height, so yeah, any ideas? :D/>/>…?

EDIT:
Even have a scroll function to scroll through the text? That would be super awesome! :D/>
Edited on 18 May 2015 - 10:32 AM
InDieTasten #2
Posted 18 May 2015 - 03:30 PM
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.
KingofGamesYami #3
Posted 18 May 2015 - 03:38 PM
I don't have a text box, but I can provide a simple pattern to match the last word on a line.


str:match( "%S+$" )

This matches for non-space characters (%S) from the right side of the string ($).

I'd store separate lines in a table, easily allowing for scrolling / cursor moving within your text box.
MKlegoman357 #4
Posted 18 May 2015 - 04:27 PM
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 :lol:/>
DannySMc #5
Posted 18 May 2015 - 04:40 PM
Thanks guys, Errr it needs to be able to like:

+ 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?

:P/>.

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.
TheOddByte #6
Posted 18 May 2015 - 05:13 PM
If you'd like you could check this out https://github.com/TheOddByte/CLove/blob/master/classes/Text.lua
DannySMc #7
Posted 18 May 2015 - 05:32 PM

I already have a word wrapper,
Spoiler

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…
MKlegoman357 #8
Posted 18 May 2015 - 07:24 PM
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
Edited on 18 May 2015 - 05:28 PM
DannySMc #9
Posted 18 May 2015 - 10:04 PM
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

Well thank you for the explanation, I do not use bedrock, but I will look for something similar online:)