word wrap

This commit is contained in:
airstruck
2015-11-26 08:53:42 -05:00
parent bbba7e1b3d
commit 2c81c0c293
18 changed files with 409 additions and 89 deletions

View File

@@ -29,6 +29,7 @@ local style = {
padding = 8,
background = { 255, 255, 255 },
icon = 'icon/32px/Box.png',
multiline = true,
},
}
@@ -71,8 +72,8 @@ local mainForm = { id = 'mainWindow', type = 'panel',
{ id = 'leftSideBox', width = 200, minwidth = 64,
{ text = 'Hi, I\'m centered middle. ', style = 'listThing',
align = 'middle center' },
{ text = 'Hi, I\'m centered bottom. ', style = 'listThing',
align = 'bottom center', slices = 'luigi/theme/light/button.png' },
{ text = 'Hi, I\'m right bottom.\nAlso two lines, woopdy woop.Hi, I\'m right bottom.\nAlso two lines, woopdy woop.Hi, I\'m right bottom.\nAlso two lines, woopdy woop.', style = 'listThing',
align = 'bottom right', slices = 'luigi/theme/light/button.png' },
{ text = 'Hi, I\'m centered top. ', style = 'listThing',
align = 'top center' },
{ text = 'A man, a plan, a canal: Panama!', style = 'listThing' },
@@ -154,13 +155,30 @@ end)
layout.mainCanvas.font = 'font/liberation/LiberationMono-Regular.ttf'
layout.mainCanvas.text = [[Abedede sdfsdf asfdsdfdsfs sdfsdfsdf
sfsdfdfbv db er erg rth tryj ty j fgh dfgv
wefwef rgh erh rth e rgs dvg eh tyj rt h erg
erge rg eg erg er ergs erg er ge rh erh rth]]
layout.mainCanvas.text = [[
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
One
two
Three
four
five
six
seven
eight
]]
layout.mainCanvas.align = 'top'
layout.mainCanvas.multiline = true
local Backend = require 'luigi.backend'
layout.menuQuit:onPress(function (event) Backend.quit() end)

View File

@@ -1,5 +1,7 @@
local ROOT = (...):gsub('[^.]*$', '')
local Backend
if _G.love and _G.love._version_minor > 8 then
return require(ROOT .. 'backend.love')
else

View File

@@ -4,16 +4,18 @@ local Hooker = require(ROOT .. 'hooker')
local ffi = require 'ffi'
local sdl = require((...) .. '.sdl')
local Image = require((...) .. '.image')
local Font = require((...) .. '.font')
local Keyboard = require((...) .. '.keyboard')
local Text = require((...) .. '.text')
local IntOut = ffi.typeof 'int[1]'
-- create window and renderer
local window = sdl.createWindow('', 0, 0, 800, 600,
sdl.WINDOW_SHOWN)
sdl.WINDOW_SHOWN + sdl.WINDOW_RESIZABLE)
if window == nil then
io.stderr:write(ffi.string(sdl.getError()))
@@ -58,13 +60,17 @@ Backend.run = function ()
return
elseif event.type == sdl.WINDOWEVENT
and event.window.event == sdl.WINDOWEVENT_RESIZED then
callback.resize(event.window.data1, event.window.data2)
local window = event.window
callback.resize(window.data1, window.data2)
elseif event.type == sdl.MOUSEBUTTONDOWN then
callback.mousepressed(event.button.x, event.button.y, event.button.button)
local button = event.button
callback.mousepressed(button.x, button.y, button.button)
elseif event.type == sdl.MOUSEBUTTONUP then
callback.mousereleased(event.button.x, event.button.y, event.button.button)
local button = event.button
callback.mousereleased(button.x, button.y, button.button)
elseif event.type == sdl.MOUSEMOTION then
callback.mousemoved(event.motion.x, event.motion.y)
local motion = event.motion
callback.mousemoved(motion.x, motion.y)
elseif event.type == sdl.KEYDOWN then
local key = Keyboard.stringByKeycode[event.key.keysym.sym]
callback.keypressed(key, event.key['repeat'])
@@ -94,6 +100,10 @@ Backend.Image = function (path)
return Image(renderer, path)
end
Backend.Text = function (...)
return Text(renderer, ...)
end
Backend.Quad = function (x, y, w, h)
return { x, y, w, h }
end
@@ -101,7 +111,28 @@ end
Backend.SpriteBatch = require((...) .. '.spritebatch')
Backend.draw = function (drawable, x, y, sx, sy)
return drawable:draw(x, y, sx, sy)
if drawable.draw then
return drawable:draw(x, y, sx, sy)
end
if drawable.sdlTexture == nil
or drawable.sdlRenderer == nil
or drawable.getWidth == nil
or drawable.getHeight == nil
then return
end
local w = drawable:getWidth() * (sx or 1)
local h = drawable:getHeight() * (sy or 1)
-- HACK. Somehow drawing something first prevents renderCopy from
-- incorrectly scaling up in some cases (after rendering slices).
-- For example http://stackoverflow.com/questions/28218906
sdl.renderDrawPoint(drawable.sdlRenderer, -1, -1)
-- Draw the image.
sdl.renderCopy(drawable.sdlRenderer, drawable.sdlTexture,
nil, sdl.Rect(x, y, w, h))
end
Backend.drawRectangle = function (mode, x, y, w, h)
@@ -118,7 +149,7 @@ local currentFont = Font()
Backend.print = function (text, x, y)
if not text or text == '' then return end
local font = currentFont.sdlFont
local color = sdl.Color(currentFont.color)
local color = sdl.Color(currentFont.color or { 0, 0, 0, 255 })
local write = Font.SDL2_ttf.TTF_RenderUTF8_Blended
local surface = write(font, text, color)
@@ -130,7 +161,9 @@ end
Backend.printf = Backend.print
Backend.getClipboardText = sdl.getClipboardText
Backend.getClipboardText = function ()
return ffi.string(sdl.getClipboardText())
end
Backend.setClipboardText = sdl.setClipboardText

View File

@@ -197,13 +197,10 @@ Font.SDL2_ttf = SDL2_ttf
local fontCache = {}
function Font:constructor (path, size, color)
function Font:constructor (path, size)
if not size then
size = 12
end
if not color then
color = { 0, 0, 0, 255 }
end
if not path then
path = REL:gsub('%.', '/') .. 'resource/DejaVuSans.ttf'
end
@@ -222,7 +219,6 @@ function Font:constructor (path, size, color)
end
self.sdlFont = fontCache[key]
self.color = color
end
function Font:setAlignment (align)

View File

@@ -14,10 +14,12 @@ end })
function Image:constructor (renderer, path)
self.sdlRenderer = renderer
self.sdlSurface = SDL2_image.IMG_Load(path)
ffi.gc(self.sdlSurface, sdl.freeSurface)
self.sdlTexture = sdl.createTextureFromSurface(renderer, self.sdlSurface)
ffi.gc(self.sdlTexture, sdl.destroyTexture)
self.sdlSurface = ffi.gc(
SDL2_image.IMG_Load(path),
sdl.freeSurface)
self.sdlTexture = ffi.gc(
sdl.createTextureFromSurface(renderer, self.sdlSurface),
sdl.destroyTexture)
self.width = self.sdlSurface.w
self.height = self.sdlSurface.h
end
@@ -30,17 +32,4 @@ function Image:getHeight ()
return self.height
end
function Image:draw (x, y, sx, sy)
local w = self.width * (sx or 1)
local h = self.height * (sy or 1)
-- HACK. Somehow drawing something first prevents renderCopy from
-- incorrectly scaling up in some cases (after rendering slices).
-- For example http://stackoverflow.com/questions/28218906
sdl.renderDrawPoint(self.sdlRenderer, -1, -1)
-- Draw the image.
sdl.renderCopy(self.sdlRenderer, self.sdlTexture, nil, sdl.Rect(x, y, w, h))
end
return Image

View File

@@ -1,7 +1,7 @@
local ROOT = (...):gsub('[^.]*$', '')
local REL = (...):gsub('[^.]*$', '')
local ffi = require 'ffi'
local sdl = require(ROOT .. 'sdl2.init')
local sdl = require(REL .. 'sdl2.init')
sdl.AudioCVT = ffi.typeof 'SDL_AudioCVT'
-- sdl.AudioDeviceEvent = ffi.typeof 'SDL_AudioDeviceEvent'

View File

@@ -0,0 +1,106 @@
local ROOT = (...):gsub('[^.]*.[^.]*.[^.]*$', '')
local REL = (...):gsub('[^.]*$', '')
local ffi = require 'ffi'
local sdl = require(REL .. 'sdl')
local Font = require(REL .. 'font')
local ttf = Font.SDL2_ttf
local Multiline = require(ROOT .. 'multiline')
local Text = setmetatable({}, { __call = function (self, ...)
local object = setmetatable({}, { __index = self })
return object, self.constructor(object, ...)
end })
local function renderSingle (self, font, text, color)
local alphaMod = color and color[4]
color = sdl.Color(color or 0)
local surface = ffi.gc(
ttf.TTF_RenderUTF8_Blended(font.sdlFont, text, color),
sdl.freeSurface)
self.sdlSurface = surface
self.sdlTexture = ffi.gc(
sdl.createTextureFromSurface(self.sdlRenderer, surface),
sdl.destroyTexture)
if alphaMod then
sdl.setTextureAlphaMod(self.sdlTexture, alphaMod)
end
self.width, self.height = surface.w, surface.h
end
local function renderMulti (self, font, text, color, align, limit)
local alphaMod = color and color[4]
local lines = Multiline.wrap(font, text, limit)
local lineHeight = font:getLineHeight()
local height = #lines * lineHeight
color = sdl.Color(color or 0)
local r, g, b, a
if sdl.BYTEORDER == sdl.BIG_ENDIAN then
r, g, b, a = 0xFF000000, 0x00FF0000, 0x0000FF00, 0x000000FF
else
r, g, b, a = 0x000000FF, 0x0000FF00, 0x00FF0000, 0xFF000000
end
local surface = ffi.gc(
sdl.createRGBSurface(0, limit, height, 32, r, g, b, a),
sdl.freeSurface)
self.sdlSurface = surface
for index, line in ipairs(lines) do
local text = table.concat(line)
local lineSurface = ffi.gc(
ttf.TTF_RenderUTF8_Blended(font.sdlFont, text, color),
sdl.freeSurface)
if lineSurface ~= nil then
local w, h = lineSurface.w, lineSurface.h
local top = (index - 1) * lineHeight
if align == 'left' then
sdl.blitSurface(lineSurface, nil, surface,
sdl.Rect(0, top, w, h))
elseif align == 'right' then
sdl.blitSurface(lineSurface, nil, surface,
sdl.Rect(limit - line.width, top, w, h))
elseif align == 'center' then
sdl.blitSurface(lineSurface, nil, surface,
sdl.Rect((limit - line.width) / 2, top, w, h))
end
end
end
self.sdlTexture = ffi.gc(
sdl.createTextureFromSurface(self.sdlRenderer, surface),
sdl.destroyTexture)
if alphaMod then
sdl.setTextureAlphaMod(self.sdlTexture, alphaMod)
end
self.width, self.height = surface.w, surface.h
end
function Text:constructor (renderer, font, text, color, align, limit)
self.width, self.height = 0, 0
if not text or text == '' then return end
self.sdlRenderer = renderer
if limit then
renderMulti(self, font, text, color, align, limit)
else
renderSingle(self, font, text, color)
end
end
function Text:getWidth ()
return self.width
end
function Text:getHeight ()
return self.height
end
return Text

View File

@@ -11,13 +11,21 @@ Backend.Cursor = love.mouse.newCursor
Backend.Font = require(ROOT .. 'backend.love.font')
Backend.Text = require(ROOT .. 'backend.love.text')
Backend.Image = love.graphics.newImage
Backend.Quad = love.graphics.newQuad
Backend.SpriteBatch = love.graphics.newSpriteBatch
Backend.draw = love.graphics.draw
-- love.graphics.draw( drawable, x, y, r, sx, sy, ox, oy, kx, ky )
Backend.draw = function (drawable, ...)
if drawable.typeOf and drawable:typeOf 'Drawable' then
return love.graphics.draw(drawable, ...)
end
return drawable:draw(...)
end
Backend.drawRectangle = love.graphics.rectangle

View File

@@ -0,0 +1,72 @@
local ROOT = (...):gsub('[^.]*.[^.]*.[^.]*$', '')
local REL = (...):gsub('[^.]*$', '')
local Multiline = require(ROOT .. 'multiline')
local Text = setmetatable({}, { __call = function (self, ...)
local object = setmetatable({}, { __index = self })
return object, self.constructor(object, ...)
end })
local function renderSingle (self, x, y, font, text, color)
love.graphics.push('all')
love.graphics.setColor(color or { 0, 0, 0 })
love.graphics.setFont(font.loveFont)
love.graphics.print(text, x, y)
love.graphics.pop()
self.height = font:getLineHeight()
self.width = font:getAdvance(text)
end
local function renderMulti (self, x, y, font, text, color, align, limit)
local lines = Multiline.wrap(font, text, limit)
local lineHeight = font:getLineHeight()
local height = #lines * lineHeight
love.graphics.push('all')
love.graphics.setColor(color or { 0, 0, 0 })
love.graphics.setFont(font.loveFont)
for index, line in ipairs(lines) do
local text = table.concat(line)
local top = (index - 1) * lineHeight
local w = line.width
if align == 'left' then
love.graphics.print(text, x, top + y)
elseif align == 'right' then
love.graphics.print(text, limit - w + x, top + y)
elseif align == 'center' then
love.graphics.print(text, (limit - w) / 2 + x, top + y)
end
end
love.graphics.pop()
self.height = height
self.width = limit
end
function Text:constructor (font, text, color, align, limit)
if limit then
function self:draw (x, y)
return renderMulti(self, x, y, font, text, color, align, limit)
end
else
function self:draw (x, y)
return renderSingle(self, x, y, font, text, color)
end
end
self:draw(-1000000, -1000000)
end
function Text:getWidth ()
return self.width
end
function Text:getHeight ()
return self.height
end
return Text

View File

@@ -1,5 +0,0 @@
local ROOT = (...):gsub('[^.]*$', '')
local Backend = require(ROOT .. 'backend')
return Backend.Font

View File

@@ -34,8 +34,7 @@ function Input:handleKeyPress (layout, key, x, y)
local result = widget:bubbleEvent('KeyPress', {
key = key,
modifierFlags = self:getModifierFlags(),
x = x,
y = y
x = x, y = y
})
if result ~= nil then return result end
end
@@ -45,8 +44,7 @@ function Input:handleKeyRelease (layout, key, x, y)
local result = widget:bubbleEvent('KeyRelease', {
key = key,
modifierFlags = self:getModifierFlags(),
x = x,
y = y
x = x, y = y
})
if result ~= nil then return result end
end
@@ -55,7 +53,8 @@ function Input:handleTextInput (layout, text, x, y)
local widget = layout.focusedWidget or layout.root
local result = widget:bubbleEvent('TextInput', {
hit = hit,
text = text, x = x, y = y
text = text,
x = x, y = y
})
if result ~= nil then return result end
end
@@ -193,7 +192,8 @@ function Input:handlePressEnd (layout, button, x, y, widget, accelerator)
hit = hit,
origin = originWidget,
accelerator = accelerator,
button = button, x = x, y = y
button = button,
x = x, y = y
})
if (widget == originWidget) then
widget:bubbleEvent('Press', {

View File

@@ -140,7 +140,7 @@ function Layout:show ()
Backend.hide(self)
self.isShown = nil
end
self.isShown = true
if not self.input then

54
luigi/multiline.lua Normal file
View File

@@ -0,0 +1,54 @@
local Multiline = {}
function Multiline.wrap (font, text, limit)
local lines = {{ width = 0 }}
local advance = 0
local function append (word, space)
local wordAdvance = font:getAdvance(word)
local spaceAdvance = font:getAdvance(space)
local words = lines[#lines]
if advance + wordAdvance > limit then
advance = wordAdvance + spaceAdvance
lines[#lines + 1] = { width = advance, word, space }
else
advance = advance + wordAdvance + spaceAdvance
words.width = advance
words[#words + 1] = word
words[#words + 1] = space
end
end
local function appendFrag (frag, isFirst)
if isFirst then
append(frag, '')
else
local wordAdvance = font:getAdvance(frag)
lines[#lines + 1] = { width = wordAdvance, frag }
advance = wordAdvance
end
end
local leadSpace = text:match '^ +'
if leadSpace then
append('', leadSpace)
end
for word, space in text:gmatch '([^ ]+)( *)' do
if word:match '\n' then
local isFirst = true
for frag in (word .. '\n'):gmatch '([^\n]*)\n' do
appendFrag(frag, isFirst)
isFirst = false
end
append('', space)
else
append(word, space)
end
end
return lines
end
return Multiline

View File

@@ -3,7 +3,8 @@ local ROOT = (...):gsub('[^.]*$', '')
local Backend = require(ROOT .. 'backend')
local Base = require(ROOT .. 'base')
local Event = require(ROOT .. 'event')
local Font = require(ROOT .. 'font')
local Font = Backend.Font
local Text = Backend.Text
local Renderer = Base:extend()
@@ -18,6 +19,8 @@ function Renderer:loadImage (path)
return imageCache[path]
end
-- TODO: make slices a seperate drawable
function Renderer:loadSlices (path)
local slices = sliceCache[path]
@@ -141,39 +144,40 @@ end
-- returns text coordinates
function Renderer:positionText (widget, x1, y1, x2, y2)
if not widget.text then
if not widget.text or x1 >= x2 then
return nil, nil, x1, y1, x2, y2
end
if not widget.fontData then
widget.fontData = Font(widget.font, widget.fontSize, widget.textColor)
widget.fontData = Font(widget.font, widget.fontSize)
end
local font = widget.fontData
local align = widget.align or ''
local padding = widget.padding or 0
font:setWidth(x2 - x1)
local horizontal = 'left'
-- horizontal alignment
if align:find('right') then
font:setAlignment('right')
elseif align:find('center') then
font:setAlignment('center')
elseif align:find('justify') then
font:setAlignment('justify')
else -- if align:find('left') then
font:setAlignment('left')
if align:find 'right' then
horizontal = 'right'
elseif align:find 'center' then
horizontal = 'center'
elseif align:find 'justify' then
horizontal = 'justify'
end
if not widget.textData then
local limit = widget.multiline and x2 - x1 or nil
widget.textData = Text(
font, widget.text, widget.textColor, horizontal, limit)
end
local textHeight = widget.textData:getHeight()
local y
-- vertical alignment
if align:find('bottom') then
local textHeight = font:getWrappedHeight(widget.text)
y = y2 - textHeight
elseif align:find('middle') then
local textHeight = font:getWrappedHeight(widget.text)
y = y2 - (y2 - y1) / 2 - textHeight / 2
else -- if align:find('top') then
y = y1
@@ -210,22 +214,32 @@ function Renderer:renderIconAndText (widget)
if icon and text and align:find('center') then
local iconHeight = icon:getHeight()
if align:find('middle') then
local textHeight = font:getWrappedHeight(text)
if align:find 'middle' then
local textHeight = widget.textData:getHeight()
local contentHeight = textHeight + padding + iconHeight
local offset = ((y2 - y1) - contentHeight) / 2
iconY = y1 + offset
textY = y1 + offset + padding + iconHeight
elseif align:find('top') then
elseif align:find 'top' then
iconY = y1
textY = y1 + padding + iconHeight
else -- if align:find('bottom')
local textHeight = font:getWrappedHeight(text)
else -- if align:find 'bottom'
local textHeight = widget.textData:getHeight()
textY = y2 - textHeight
iconY = textY - padding - iconHeight
end
end
-- horizontal alignment for non-multiline
-- TODO: handle this in Backend.Text
if text and not widget.multiline then
if align:find 'right' then
textX = textX + ((x2 - x1) - widget.textData:getWidth())
elseif align:find 'center' then
textX = textX + ((x2 - x1) - widget.textData:getWidth()) / 2
end
end
-- draw the icon
if icon then
iconX, iconY = math.floor(iconX), math.floor(iconY)
@@ -238,9 +252,7 @@ function Renderer:renderIconAndText (widget)
-- draw the text
if text and x2 > x1 then
textX, textY = math.floor(textX), math.floor(textY)
Backend.setFont(font)
Backend.setColor(font.color)
Backend.printf(text, textX, textY, x2 - x1, font.align)
Backend.draw(widget.textData, textX, textY)
end
Backend.pop()

View File

@@ -1,3 +1,5 @@
-- modified for partial compatibility with Lua 5.3
--utf8 module (Cosmin Apreutesei, public domain).
--byte indices are i's, char (codepoint) indices are ci's.
--invalid characters are counted as 1-byte chars so they don't get lost. validate/sanitize beforehand as needed.
@@ -56,11 +58,12 @@ function utf8.byte_index(s, target_ci)
end
end
assert(target_ci > ci, 'invalid index')
return #s + 1
end
--char index given byte index. nil if the index is outside the string.
function utf8.char_index(s, target_i)
if target_i < 1 or target_i > #s then return end
if target_i < 1 or target_i > #s + 1 then return end
local ci = 0
for i in utf8.byte_indices(s) do
ci = ci + 1
@@ -68,7 +71,8 @@ function utf8.char_index(s, target_i)
return ci
end
end
error'invalid index'
return ci + 1
-- error'invalid index'
end
--byte index of the prev. char before the char at byte index i, which defaults to #s + 1.
@@ -306,11 +310,28 @@ function utf8.sanitize(s, repl_char)
return utf8.replace(s, replace_invalid, repl_char)
end
-- added for partial compatibility with lua 5.3
-- Returns the position (in bytes) where the encoding of the n-th character
-- of s (counting from position i) starts.
function utf8.offset(s, n, i)
function utf8.offset(s, char_offset, byte_index)
local ci = utf8.char_index(s, byte_index)
return utf8.byte_index(s, ci + char_offset)
-- The default for i is 1 when n is non-negative and #s + 1 otherwise
if not i then
i = n < 0 and #s + 1 or 1
end
local ci = utf8.char_index(s, i)
-- As a special case, when n is 0 the function returns the start of
-- the encoding of the character that contains the i-th byte of s.
if n == 0 then
return ci
end
if n > 0 then
n = n - 1
end
return utf8.byte_index(s, ci + n)
end
utf8.codes = utf8.byte_indices

View File

@@ -8,7 +8,7 @@ local ROOT = (...):gsub('[^.]*$', '')
local Backend = require(ROOT .. 'backend')
local Event = require(ROOT .. 'event')
local Font = require(ROOT .. 'font')
local Font = Backend.Font
local Widget = {}
@@ -50,17 +50,25 @@ end
-- setting shadow properties causes special behavior
local function metaNewIndex (self, property, value)
if property == 'font'
or property == 'fontSize'
or property == 'textColor' then
if property == 'font' or property == 'fontSize' then
self.shadowProperties[property] = value
self.fontData = Font(self.font, self.fontSize, self.textColor)
self.fontData = nil
self.textData = nil
end
if property == 'text' or property == 'textColor'
or property == 'align' or property == 'multiline' then
self.shadowProperties[property] = value
self.textData = nil
return
end
if property == 'width' then
value = value and math.max(value, self.minwidth or 0)
self.shadowProperties[property] = value
if self.multiline then
self.textData = nil
end
Widget.reshape(self.parent or self)
return
end
@@ -114,8 +122,13 @@ local function metaNewIndex (self, property, value)
end
local shadowKeys = {
'font', 'fontSize', 'textColor', 'width', 'height', 'value', 'key', 'id'
'id', 'key', 'value',
'width', 'height',
'font', 'fontSize',
'text', 'textColor',
'align', 'multiline',
}
--[[--
Widget pseudo-constructor.
@@ -603,6 +616,7 @@ function Widget:reshape ()
if self.isReshaping then return end
self.isReshaping = true
self.needsReshape = true
self.textData = nil
Event.Reshape:emit(self, {
target = self
})

View File

@@ -125,7 +125,7 @@ local function initialize (self)
local textWidth = self.fontData:getAdvance(text) + pad * 2
if isSubmenu then
local tc = self.textColor or { 0, 0, 0 }
local tc = self.textColor or { 0, 0, 0, 255 }
local keyColor = { tc[1], tc[2], tc[3], 0x90 }
local edgeType
if #self.items > 0 then

View File

@@ -230,7 +230,7 @@ return function (self)
local x1, y1, x2, y2 = self:getRectangle(true, true)
local width, height = endX - startX, y2 - y1
local font = self.fontData
local textColor = font.color
local textColor = self.textColor or { 0, 0, 0, 255 }
local textTop = math.floor(y1 + ((y2 - y1) - font:getLineHeight()) / 2)
Backend.push()