| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315 |
- -- richtext library
- --[[
- Copyright (c) 2010 Robin Wellner
- Copyright (c) 2014 Florian Fischer (class changes, initial color, ...)
- This software is provided 'as-is', without any express or implied
- warranty. In no event will the authors be held liable for any damages
- arising from the use of this software.
- Permission is granted to anyone to use this software for any purpose,
- including commercial applications, and to alter it and redistribute it
- freely, subject to the following restrictions:
- 1. The origin of this software must not be misrepresented; you must not
- claim that you wrote the original software. If you use this software
- in a product, an acknowledgment in the product documentation would be
- appreciated but is not required.
- 2. Altered source versions must be plainly marked as such, and must not be
- misrepresented as being the original software.
- 3. This notice may not be removed or altered from any source
- distribution.
- ]]
- -- issues/bugs:
- -- * still under-tested
- -- * word wrapping might not be optimal
- -- * words keep their final space in wrapping, which may cause words to be wrapped too soon
- local rich = {}
- rich.__index = rich
- function rich:new(t, stdcolor) -- syntax: rt = rich.new{text, width, resource1 = ..., ...}
- rich.canvas = rich.canvas or love.graphics.newCanvas(love.graphics.getWidth() * .5, love.graphics.getHeight() * .5)
- local obj = setmetatable({parsedtext = {}, resources = {}}, rich)
- obj.width = t[2]
- obj.hardwrap = false
- obj:extract(t)
- obj:parse(t)
- -- set text standard color
- if stdcolor and type(stdcolor) =='table' then love.graphics.setColor( unpack(stdcolor) ) end
- if love.graphics.isSupported and love.graphics.isSupported('canvas') then
- obj:render()
- obj:render(true)
- end
- return obj
- end
- function rich:draw(x, y)
- local firstR, firstG, firstB, firstA = love.graphics.getColor()
- love.graphics.setColor(255, 255, 255, 255)
- local prevMode = love.graphics.getBlendMode()
- if self.framebuffer then
- love.graphics.setBlendMode("premultiplied")
- love.graphics.draw(self.framebuffer, x, y)
- love.graphics.setBlendMode(prevMode)
- else
- love.graphics.push()
- love.graphics.translate(x, y)
- self:render()
- love.graphics.pop()
- end
- love.graphics.setColor(firstR, firstG, firstB, firstA)
- end
- function rich:extract(t)
- if t[3] and type(t[3]) == 'table' then
- for key,value in pairs(t[3]) do
- local meta = type(value) == 'table' and value or {value}
- self.resources[key] = self:initmeta(meta) -- sets default values, does a PO2 fix...
- end
- elseif t[3] then
- for key,value in pairs(t) do
- if type(key) == 'string' then
- local meta = type(value) == 'table' and value or {value}
- self.resources[key] = self:initmeta(meta) -- sets default values, does a PO2 fix...
- end
- end
- end
- end
- local function parsefragment(parsedtext, textfragment)
- -- break up fragments with newlines
- local n = textfragment:find('\n', 1, true)
- while n do
- table.insert(parsedtext, textfragment:sub(1, n-1))
- table.insert(parsedtext, {type='nl'})
- textfragment = textfragment:sub(n + 1)
- n = textfragment:find('\n', 1, true)
- end
- table.insert(parsedtext, textfragment)
- end
- function rich:parse(t)
- local text = t[1]
- if string.len(text) > 0 then
- -- look for {tags} or [tags]
- for textfragment, foundtag in text:gmatch'([^{]*){(.-)}' do
- parsefragment(self.parsedtext, textfragment)
- table.insert(self.parsedtext, self.resources[foundtag] or foundtag)
- end
- parsefragment(self.parsedtext, text:match('[^}]+$'))
- end
- end
- -- [[ since 0.8.0, no autopadding needed any more
- local log2 = 1/math.log(2)
- local function nextpo2(n)
- return math.pow(2, math.ceil(math.log(n)*log2))
- end
- local metainit = {}
- function metainit.Image(res, meta)
- meta.type = 'img'
- local w, h = res:getWidth(), res:getHeight()
- --[[ since 0.8.0, no autopadding needed any more
- if not rich.nopo2 then
- local neww = nextpo2(w)
- local newh = nextpo2(h)
- if neww ~= w or newh ~= h then
- local padded = love.image.newImageData(wp, hp)
- padded:paste(love.image.newImageData(res), 0, 0)
- meta[1] = love.graphics.newImage(padded)
- end
- end
- ]]
- meta.width = meta.width or w
- meta.height = meta.height or h
- end
- function metainit.Font(res, meta)
- meta.type = 'font'
- end
- function metainit.number(res, meta)
- meta.type = 'color'
- end
- function rich:initmeta(meta)
- local res = meta[1]
- local type = (type(res) == 'userdata') and res:type() or type(res)
- if metainit[type] then
- metainit[type](res, meta)
- else
- error("Unsupported type")
- end
- return meta
- end
- local function wrapText(parsedtext, fragment, lines, maxheight, x, width, i, fnt, hardwrap)
- if not hardwrap or (hardwrap and x > 0) then
- -- find first space, split again later if necessary
- local n = fragment:find(' ', 1, true)
- local lastn = n
- while n do
- local newx = x + fnt:getWidth(fragment:sub(1, n-1))
- if newx > width then
- break
- end
- lastn = n
- n = fragment:find(' ', n + 1, true)
- end
- n = lastn or (#fragment + 1)
- -- wrapping
- parsedtext[i] = fragment:sub(1, n-1)
- table.insert(parsedtext, i+1, fragment:sub((fragment:find('[^ ]', n) or (n+1)) - 1))
- lines[#lines].height = maxheight
- maxheight = 0
- x = 0
- table.insert(lines, {})
- end
- return maxheight, 0
- end
- local function renderText(parsedtext, fragment, lines, maxheight, x, width, i, hardwrap)
- local fnt = love.graphics.getFont() or love.graphics.newFont(12)
- if x + fnt:getWidth(fragment) > width then -- oh oh! split the text
- maxheight, x = wrapText(parsedtext, fragment, lines, maxheight, x, width, i, fnt, hardwrap)
- end
- -- hardwrap long words
- if hardwrap and x + fnt:getWidth(parsedtext[i]) > width then
- local n = #parsedtext[i]
- while x + fnt:getWidth(parsedtext[i]:sub(1, n)) > width do
- n = n - 1
- end
- local p1, p2 = parsedtext[i]:sub(1, n - 1), parsedtext[i]:sub(n)
- parsedtext[i] = p1
- if not parsedtext[i + 1] then
- parsedtext[i + 1] = p2
- elseif type(parsedtext[i + 1]) == 'string' then
- parsedtext[i + 1] = p2 .. parsedtext[i + 1]
- elseif type(parsedtext[i + 1]) == 'table' then
- table.insert(parsedtext, i + 2, p2)
- table.insert(parsedtext, i + 3, {type='nl'})
- end
- lines[#lines].height = maxheight
- maxheight = 0
- x = 0
- table.insert(lines, {})
- end
- local h = math.floor(fnt:getHeight(parsedtext[i]) * fnt:getLineHeight())
- maxheight = math.max(maxheight, h)
- return maxheight, x + fnt:getWidth(parsedtext[i]), {parsedtext[i], x = x > 0 and x or 0, type = 'string', height = h, width = fnt:getWidth(parsedtext[i])}
- end
- local function renderImage(fragment, lines, maxheight, x, width)
- local newx = x + fragment.width
- if newx > width and x > 0 then -- wrapping
- lines[#lines].height = maxheight
- maxheight = 0
- x = 0
- table.insert(lines, {})
- end
- maxheight = math.max(maxheight, fragment.height)
- return maxheight, newx, {fragment, x = x, type = 'img'}
- end
- local function doRender(parsedtext, width, hardwrap)
- local x = 0
- local lines = {{}}
- local maxheight = 0
- for i, fragment in ipairs(parsedtext) do -- prepare rendering
- if type(fragment) == 'string' then
- maxheight, x, fragment = renderText(parsedtext, fragment, lines, maxheight, x, width, i, hardwrap)
- elseif fragment.type == 'img' then
- maxheight, x, fragment = renderImage(fragment, lines, maxheight, x, width)
- elseif fragment.type == 'font' then
- love.graphics.setFont(fragment[1])
- elseif fragment.type == 'nl' then
- -- move onto next line, reset x and maxheight
- lines[#lines].height = maxheight
- maxheight = 0
- x = 0
- table.insert(lines, {})
- -- don't want nl inserted into line
- fragment = ''
- end
- table.insert(lines[#lines], fragment)
- end
- --~ for i,f in ipairs(parsedtext) do
- --~ print(f)
- --~ end
- lines[#lines].height = maxheight
- return lines
- end
- local function doDraw(lines)
- local y = 0
- local colorr,colorg,colorb,colora = love.graphics.getColor()
- for i, line in ipairs(lines) do -- do the actual rendering
- y = y + line.height
- for j, fragment in ipairs(line) do
- if fragment.type == 'string' then
- -- remove leading spaces, but only at the begin of a new line
- -- Note: the check for fragment 2 (j==2) is to avoid a sub for leading line space
- if j==2 and string.sub(fragment[1], 1, 1) == ' ' then
- --print(i,y,j,fragment[1])
- fragment[1] = string.sub(fragment[1], 2)
- end
- love.graphics.print(fragment[1], fragment.x, y - fragment.height)
- if rich.debug then
- love.graphics.rectangle('line', fragment.x, y - fragment.height, fragment.width, fragment.height)
- end
- elseif fragment.type == 'img' then
- love.graphics.setColor(255,255,255)
- love.graphics.draw(fragment[1][1], fragment.x, y - fragment[1].height)
- if rich.debug then
- love.graphics.rectangle('line', fragment.x, y - fragment[1].height, fragment[1].width, fragment[1].height)
- end
- love.graphics.setColor(colorr,colorg,colorb,colora)
- elseif fragment.type == 'font' then
- love.graphics.setFont(fragment[1])
- elseif fragment.type == 'color' then
- love.graphics.setColor(unpack(fragment))
- colorr,colorg,colorb,colora = love.graphics.getColor()
- end
- end
- end
- end
- function rich:calcHeight(lines)
- local h = 0
- for _, line in ipairs(lines) do
- h = h + line.height
- end
- return h
- end
- function rich:render(usefb)
- local renderWidth = self.width or math.huge -- if not given, use no wrapping
- local firstFont = love.graphics.getFont() or love.graphics.newFont(12)
- local firstR, firstG, firstB, firstA = love.graphics.getColor()
- local lines = doRender(self.parsedtext, renderWidth, self.hardwrap)
- -- dirty hack, add half height of last line to bottom of height to ensure tails of y's and g's, etc fit in properly.
- self.height = self:calcHeight(lines) + math.floor((lines[#lines].height / 2) + 0.5)
- local fbWidth = math.max(nextpo2(math.max(love.graphics.getWidth(), width or 0)), nextpo2(math.max(love.graphics.getHeight(), self.height)))
- local fbHeight = fbWidth
- love.graphics.setFont(firstFont)
- if usefb then
- self.framebuffer = rich.canvas --love.graphics.newCanvas(fbWidth, fbHeight)
- self.framebuffer:setFilter( 'nearest', 'nearest' )
- self.framebuffer:clear(0, 0, 0, 0)
- self.framebuffer:renderTo(function () doDraw(lines) end)
- else
- self.height = doDraw(lines)
- end
- love.graphics.setFont(firstFont)
- love.graphics.setColor(firstR, firstG, firstB, firstA)
- end
- return rich
|