richtext.lua 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. -- richtext library
  2. --[[
  3. Copyright (c) 2010 Robin Wellner
  4. Copyright (c) 2014 Florian Fischer (class changes, initial color, ...)
  5. This software is provided 'as-is', without any express or implied
  6. warranty. In no event will the authors be held liable for any damages
  7. arising from the use of this software.
  8. Permission is granted to anyone to use this software for any purpose,
  9. including commercial applications, and to alter it and redistribute it
  10. freely, subject to the following restrictions:
  11. 1. The origin of this software must not be misrepresented; you must not
  12. claim that you wrote the original software. If you use this software
  13. in a product, an acknowledgment in the product documentation would be
  14. appreciated but is not required.
  15. 2. Altered source versions must be plainly marked as such, and must not be
  16. misrepresented as being the original software.
  17. 3. This notice may not be removed or altered from any source
  18. distribution.
  19. ]]
  20. -- issues/bugs:
  21. -- * still under-tested
  22. -- * word wrapping might not be optimal
  23. -- * words keep their final space in wrapping, which may cause words to be wrapped too soon
  24. local rich = {}
  25. rich.__index = rich
  26. function rich:new(t, stdcolor) -- syntax: rt = rich.new{text, width, resource1 = ..., ...}
  27. local obj = setmetatable({parsedtext = {}, resources = {}}, rich)
  28. obj.width = t[2]
  29. obj.hardwrap = false
  30. obj:extract(t)
  31. obj:parse(t)
  32. -- set text standard color
  33. if stdcolor and type(stdcolor) =='table' then love.graphics.setColor( unpack(stdcolor) ) end
  34. if love.graphics.isSupported and love.graphics.isSupported('canvas') then
  35. obj:render()
  36. obj:render(true)
  37. end
  38. return obj
  39. end
  40. function rich:draw(x, y)
  41. local firstR, firstG, firstB, firstA = love.graphics.getColor()
  42. love.graphics.setColor(255, 255, 255, 255)
  43. local prevMode = love.graphics.getBlendMode()
  44. if self.framebuffer then
  45. love.graphics.setBlendMode("premultiplied")
  46. love.graphics.draw(self.framebuffer, x, y)
  47. love.graphics.setBlendMode(prevMode)
  48. else
  49. love.graphics.push()
  50. love.graphics.translate(x, y)
  51. self:render()
  52. love.graphics.pop()
  53. end
  54. love.graphics.setColor(firstR, firstG, firstB, firstA)
  55. end
  56. function rich:extract(t)
  57. if t[3] and type(t[3]) == 'table' then
  58. for key,value in pairs(t[3]) do
  59. local meta = type(value) == 'table' and value or {value}
  60. self.resources[key] = self:initmeta(meta) -- sets default values, does a PO2 fix...
  61. end
  62. else
  63. for key,value in pairs(t) do
  64. if type(key) == 'string' then
  65. local meta = type(value) == 'table' and value or {value}
  66. self.resources[key] = self:initmeta(meta) -- sets default values, does a PO2 fix...
  67. end
  68. end
  69. end
  70. end
  71. local function parsefragment(parsedtext, textfragment)
  72. -- break up fragments with newlines
  73. local n = textfragment:find('\n', 1, true)
  74. while n do
  75. table.insert(parsedtext, textfragment:sub(1, n-1))
  76. table.insert(parsedtext, {type='nl'})
  77. textfragment = textfragment:sub(n + 1)
  78. n = textfragment:find('\n', 1, true)
  79. end
  80. table.insert(parsedtext, textfragment)
  81. end
  82. function rich:parse(t)
  83. local text = t[1]
  84. if string.len(text) > 0 then
  85. -- look for {tags} or [tags]
  86. for textfragment, foundtag in text:gmatch'([^{]*){(.-)}' do
  87. parsefragment(self.parsedtext, textfragment)
  88. table.insert(self.parsedtext, self.resources[foundtag] or foundtag)
  89. end
  90. parsefragment(self.parsedtext, text:match('[^}]+$'))
  91. end
  92. end
  93. -- [[ since 0.8.0, no autopadding needed any more
  94. local log2 = 1/math.log(2)
  95. local function nextpo2(n)
  96. return math.pow(2, math.ceil(math.log(n)*log2))
  97. end
  98. local metainit = {}
  99. function metainit.Image(res, meta)
  100. meta.type = 'img'
  101. local w, h = res:getWidth(), res:getHeight()
  102. --[[ since 0.8.0, no autopadding needed any more
  103. if not rich.nopo2 then
  104. local neww = nextpo2(w)
  105. local newh = nextpo2(h)
  106. if neww ~= w or newh ~= h then
  107. local padded = love.image.newImageData(wp, hp)
  108. padded:paste(love.image.newImageData(res), 0, 0)
  109. meta[1] = love.graphics.newImage(padded)
  110. end
  111. end
  112. ]]
  113. meta.width = meta.width or w
  114. meta.height = meta.height or h
  115. end
  116. function metainit.Font(res, meta)
  117. meta.type = 'font'
  118. end
  119. function metainit.number(res, meta)
  120. meta.type = 'color'
  121. end
  122. function rich:initmeta(meta)
  123. local res = meta[1]
  124. local type = (type(res) == 'userdata') and res:type() or type(res)
  125. if metainit[type] then
  126. metainit[type](res, meta)
  127. else
  128. error("Unsupported type")
  129. end
  130. return meta
  131. end
  132. local function wrapText(parsedtext, fragment, lines, maxheight, x, width, i, fnt, hardwrap)
  133. if not hardwrap or (hardwrap and x > 0) then
  134. -- find first space, split again later if necessary
  135. local n = fragment:find(' ', 1, true)
  136. local lastn = n
  137. while n do
  138. local newx = x + fnt:getWidth(fragment:sub(1, n-1))
  139. if newx > width then
  140. break
  141. end
  142. lastn = n
  143. n = fragment:find(' ', n + 1, true)
  144. end
  145. n = lastn or (#fragment + 1)
  146. -- wrapping
  147. parsedtext[i] = fragment:sub(1, n-1)
  148. table.insert(parsedtext, i+1, fragment:sub((fragment:find('[^ ]', n) or (n+1)) - 1))
  149. lines[#lines].height = maxheight
  150. maxheight = 0
  151. x = 0
  152. table.insert(lines, {})
  153. end
  154. return maxheight, 0
  155. end
  156. local function renderText(parsedtext, fragment, lines, maxheight, x, width, i, hardwrap)
  157. local fnt = love.graphics.getFont() or love.graphics.newFont(12)
  158. if x + fnt:getWidth(fragment) > width then -- oh oh! split the text
  159. maxheight, x = wrapText(parsedtext, fragment, lines, maxheight, x, width, i, fnt, hardwrap)
  160. end
  161. -- hardwrap long words
  162. if hardwrap and x + fnt:getWidth(parsedtext[i]) > width then
  163. local n = #parsedtext[i]
  164. while x + fnt:getWidth(parsedtext[i]:sub(1, n)) > width do
  165. n = n - 1
  166. end
  167. local p1, p2 = parsedtext[i]:sub(1, n - 1), parsedtext[i]:sub(n)
  168. parsedtext[i] = p1
  169. if not parsedtext[i + 1] then
  170. parsedtext[i + 1] = p2
  171. elseif type(parsedtext[i + 1]) == 'string' then
  172. parsedtext[i + 1] = p2 .. parsedtext[i + 1]
  173. elseif type(parsedtext[i + 1]) == 'table' then
  174. table.insert(parsedtext, i + 2, p2)
  175. table.insert(parsedtext, i + 3, {type='nl'})
  176. end
  177. lines[#lines].height = maxheight
  178. maxheight = 0
  179. x = 0
  180. table.insert(lines, {})
  181. end
  182. local h = math.floor(fnt:getHeight(parsedtext[i]) * fnt:getLineHeight())
  183. maxheight = math.max(maxheight, h)
  184. 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])}
  185. end
  186. local function renderImage(fragment, lines, maxheight, x, width)
  187. local newx = x + fragment.width
  188. if newx > width and x > 0 then -- wrapping
  189. lines[#lines].height = maxheight
  190. maxheight = 0
  191. x = 0
  192. table.insert(lines, {})
  193. end
  194. maxheight = math.max(maxheight, fragment.height)
  195. return maxheight, newx, {fragment, x = x, type = 'img'}
  196. end
  197. local function doRender(parsedtext, width, hardwrap)
  198. local x = 0
  199. local lines = {{}}
  200. local maxheight = 0
  201. for i, fragment in ipairs(parsedtext) do -- prepare rendering
  202. if type(fragment) == 'string' then
  203. maxheight, x, fragment = renderText(parsedtext, fragment, lines, maxheight, x, width, i, hardwrap)
  204. elseif fragment.type == 'img' then
  205. maxheight, x, fragment = renderImage(fragment, lines, maxheight, x, width)
  206. elseif fragment.type == 'font' then
  207. love.graphics.setFont(fragment[1])
  208. elseif fragment.type == 'nl' then
  209. -- move onto next line, reset x and maxheight
  210. lines[#lines].height = maxheight
  211. maxheight = 0
  212. x = 0
  213. table.insert(lines, {})
  214. -- don't want nl inserted into line
  215. fragment = ''
  216. end
  217. table.insert(lines[#lines], fragment)
  218. end
  219. --~ for i,f in ipairs(parsedtext) do
  220. --~ print(f)
  221. --~ end
  222. lines[#lines].height = maxheight
  223. return lines
  224. end
  225. local function doDraw(lines)
  226. local y = 0
  227. local colorr,colorg,colorb,colora = love.graphics.getColor()
  228. for i, line in ipairs(lines) do -- do the actual rendering
  229. y = y + line.height
  230. for j, fragment in ipairs(line) do
  231. if fragment.type == 'string' then
  232. -- remove leading spaces, but only at the begin of a new line
  233. -- Note: the check for fragment 2 (j==2) is to avoid a sub for leading line space
  234. if j==2 and string.sub(fragment[1], 1, 1) == ' ' then
  235. fragment[1] = string.sub(fragment[1], 2)
  236. end
  237. love.graphics.print(fragment[1], fragment.x, y - fragment.height)
  238. if rich.debug then
  239. love.graphics.rectangle('line', fragment.x, y - fragment.height, fragment.width, fragment.height)
  240. end
  241. elseif fragment.type == 'img' then
  242. love.graphics.setColor(255,255,255)
  243. love.graphics.draw(fragment[1][1], fragment.x, y - fragment[1].height)
  244. if rich.debug then
  245. love.graphics.rectangle('line', fragment.x, y - fragment[1].height, fragment[1].width, fragment[1].height)
  246. end
  247. love.graphics.setColor(colorr,colorg,colorb,colora)
  248. elseif fragment.type == 'font' then
  249. love.graphics.setFont(fragment[1])
  250. elseif fragment.type == 'color' then
  251. love.graphics.setColor(unpack(fragment))
  252. colorr,colorg,colorb,colora = love.graphics.getColor()
  253. end
  254. end
  255. end
  256. end
  257. function rich:calcHeight(lines)
  258. local h = 0
  259. for _, line in ipairs(lines) do
  260. h = h + line.height
  261. end
  262. return h
  263. end
  264. function rich:render(usefb)
  265. local renderWidth = self.width or math.huge -- if not given, use no wrapping
  266. local firstFont = love.graphics.getFont() or love.graphics.newFont(12)
  267. local firstR, firstG, firstB, firstA = love.graphics.getColor()
  268. local lines = doRender(self.parsedtext, renderWidth, self.hardwrap)
  269. -- 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.
  270. self.height = self:calcHeight(lines) + math.floor((lines[#lines].height / 2) + 0.5)
  271. local fbWidth = math.max(nextpo2(math.max(love.graphics.getWidth(), self.width or 0)), nextpo2(math.max(love.graphics.getHeight(), self.height)))
  272. local fbHeight = fbWidth
  273. love.graphics.setFont(firstFont)
  274. if usefb then
  275. self.framebuffer = love.graphics.newCanvas(fbWidth, fbHeight)
  276. self.framebuffer:setFilter( 'nearest', 'nearest' )
  277. self.framebuffer:renderTo(function () doDraw(lines) end)
  278. else
  279. self.height = doDraw(lines)
  280. end
  281. love.graphics.setFont(firstFont)
  282. love.graphics.setColor(firstR, firstG, firstB, firstA)
  283. end
  284. return rich