richtext.lua 10 KB

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