a Rich Text library for LÖVE

Showcase your libraries, tools and other projects that help your fellow love users.

Which license should richtext use?

Poll ended at Sat Oct 09, 2010 4:36 pm

Public Domain
4
22%
BSD
1
6%
MIT/X11
5
28%
LPCL
0
No votes
zlib
6
33%
LGPL
1
6%
Other (please specify in replies)
1
6%
 
Total votes: 18

User avatar
Robin
The Omniscient
Posts: 6506
Joined: Fri Feb 20, 2009 4:29 pm
Location: The Netherlands
Contact:

Re: a Rich Text library for LÖVE

Post by Robin »

Thanks for the feedback.
luminosity wrote:1. If colormode is set to replace by the love app, text won't be drawn with colours. I can see the justification for going with the user's choice, but when asking for a colour, I suspect people want a colour. In any case, I just chucked the colour modes I wanted in to the library and it all works for me, so no problem if you disagree.
Oh, that's very good. I didn't think about color modes at all.
luminosity wrote:2. Text lines are rendered very close, after hitting a new line, and seem to ignore any attempt to set line height on the font. I'd like to space my text out a little more, any chance that there'd be an easy way to specify this?
Not as it currently stands, but it would be a good thing to implement.

EDIT: implemented them, but on the experimental branch (by accident).
Help us help you: attach a .love.
luminosity
Prole
Posts: 34
Joined: Fri Sep 24, 2010 5:46 am

Re: a Rich Text library for LÖVE

Post by luminosity »

Colours work fantastically. Line height doesn't seem to be working though.
Line height 1
Line height 1
log-1-line-height.png (5.46 KiB) Viewed 2737 times
Line height 3
Line height 3
log-3-line-height.png (6.34 KiB) Viewed 2737 times
I put a print into the rich text library where it's calculating height to ensure that the write font was being used / line height detected.

P.S. Thanks for all the work you're putting into this. Hope I'm not too much of a pain.
User avatar
vrld
Party member
Posts: 917
Joined: Sun Apr 04, 2010 9:14 pm
Location: Germany
Contact:

Re: a Rich Text library for LÖVE

Post by vrld »

luminosity wrote:Line height doesn't seem to be working though.
Could be this bug.
I have come here to chew bubblegum and kick ass... and I'm all out of bubblegum.

hump | HC | SUIT | moonshine
luminosity
Prole
Posts: 34
Joined: Fri Sep 24, 2010 5:46 am

Re: a Rich Text library for LÖVE

Post by luminosity »

vrld wrote:
luminosity wrote:Line height doesn't seem to be working though.
Could be this bug.
Can't say for sure, but a quick glance at the source seems to indicate that the library does all spacing / laying out itself.

Another possible bug! Woo.

Wrapping seems inconsistent. Sometimes it seems to work perfectly, sometimes rich text seems to ignore my newlines, and sometimes it just gets a bit messy. Like this:
richtext-wrap-issues.png
richtext-wrap-issues.png (6.68 KiB) Viewed 2725 times
The only newlines in the text fed to richtext occur after the full stops.
User avatar
Robin
The Omniscient
Posts: 6506
Joined: Fri Feb 20, 2009 4:29 pm
Location: The Netherlands
Contact:

Re: a Rich Text library for LÖVE

Post by Robin »

Thanks for the feedback, it's very useful.
luminosity wrote:Can't say for sure, but a quick glance at the source seems to indicate that the library does all spacing / laying out itself.
Yes, it does.
luminosity wrote:Wrapping seems inconsistent. Sometimes it seems to work perfectly, sometimes rich text seems to ignore my newlines, and sometimes it just gets a bit messy.
Wrapping has always been something of a pain. Are you using the source from the "master" branch or the "experimental" branch?
Help us help you: attach a .love.
luminosity
Prole
Posts: 34
Joined: Fri Sep 24, 2010 5:46 am

Re: a Rich Text library for LÖVE

Post by luminosity »

Experimental.
User avatar
Robin
The Omniscient
Posts: 6506
Joined: Fri Feb 20, 2009 4:29 pm
Location: The Netherlands
Contact:

Re: a Rich Text library for LÖVE

Post by Robin »

Yeah, it's worse in experimental. I really should find a good way of doing the wrapping, such that it wraps correctly for:
  • normal words
  • newlines
  • really long words
  • images
  • and white space.
If anyone has a good idea, or better yet, working code, please let me know.
Help us help you: attach a .love.
luminosity
Prole
Posts: 34
Joined: Fri Sep 24, 2010 5:46 am

Re: a Rich Text library for LÖVE

Post by luminosity »

Haven't looked at wrapping yet, but I did fix another issue I was having, and thought you might be interested in the code.

When my combat log reached a certain height it stopped rendering new text, and just had vertical lines going straight down from the bottom-most pixels that showed properly. It turns out this is what happens when the rich text's height is greater than the framebuffer's height, which by default is the same height as the game. So I changed some code so that height is calculated before rendering, and if it's greater than the screenheight, the framebuffer is initialised with that height. I didn't bother rounding it up to the nearest Po2 though.

Anyway, my diff is

Code: Select all

Index: libraries/richtext.lua
===================================================================
--- libraries/richtext.lua      (revision 368)
+++ libraries/richtext.lua      (working copy)
@@ -33,7 +33,6 @@

 function rich.new(t) -- syntax: rt = rich.new{text, width, resource1 = ..., ...
}
        local obj = setmetatable({parsedtext = {}, resources = {}}, rich)
-       obj.framebuffer = love.graphics.newFramebuffer()
        obj:extract(t)
        obj:parse(t)
        obj:render(t[2])
@@ -199,17 +198,29 @@
                        end
                end
        end
-       return y
 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(width, nofb)
-       width = width or math.huge -- if not given, use no wrapping
+       local renderWidth = 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, width)
+       local lines = doRender(self.parsedtext, renderWidth)
+       -- dirty hack, add half height of last line to bottom of height to ensur
e 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(love.graphics.getWidth(), width or 0)
+       local fbHeight = math.max(love.graphics.getHeight(), self.height)
+       self.framebuffer = love.graphics.newFramebuffer(fbWidth, fbHeight)
        love.graphics.setFont(firstFont)
        if not nofb then
-               self.framebuffer:renderTo(function () self.height = doDraw(lines
) end)
+               self.framebuffer:renderTo(function () doDraw(lines) end)
        else
                self.height = doDraw(lines)
        end

User avatar
Robin
The Omniscient
Posts: 6506
Joined: Fri Feb 20, 2009 4:29 pm
Location: The Netherlands
Contact:

Re: a Rich Text library for LÖVE

Post by Robin »

Cool, I'll review it when I have some time.
Help us help you: attach a .love.
luminosity
Prole
Posts: 34
Joined: Fri Sep 24, 2010 5:46 am

Re: a Rich Text library for LÖVE

Post by luminosity »

Sat down to have a good look at the wrapping issues I was having, since my game is nearing finished (well, barring art anyway), and managed to fix the problems I was having. Turns out it was all caused by the rich text library not handling newline characters itself. While I was in there I also changed the framebuffer sizing code I put in to pad it to the next po2, stealing your image size padding code to do it.

The only thing I haven't managed to figure out is why I'm getting the slightly distorted text in this framebuffer, but not in others. Anyway, code with changes is below in case you're interested in the changes.

Code: Select all

-- richtext library

--[[
Copyright (c) 2010 Robin Wellner

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

rich = {}
rich.__index = rich

function rich.new(t) -- syntax: rt = rich.new{text, width, resource1 = ..., ...}
	local obj = setmetatable({parsedtext = {}, resources = {}}, rich)
	obj:extract(t)
	obj:parse(t)
	obj:render(t[2])
	return obj
end

function rich:draw(x, y)
	local firstR, firstG, firstB, firstA = love.graphics.getColor()
	love.graphics.setColor(255, 255, 255, 255)
	love.graphics.draw(self.framebuffer, x, y)
	love.graphics.setColor(firstR, firstG, firstB, firstA)
end

function rich:extract(t)
	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

function rich:parse(t)
	local text = t[1]
	-- look for {tags} or [tags]
	for textfragment, foundtag in text:gmatch'([^{]*){(.-)}' do
		-- break up fragments with newlines
		local n = textfragment:find('\n', 1, true)
		while n do
			table.insert(self.parsedtext, textfragment:sub(1, n-1))
			table.insert(self.parsedtext, {type='nl'})
			textfragment = textfragment:sub(n + 1)
			n = textfragment:find('\n', 1, true)
		end
		table.insert(self.parsedtext, textfragment)
		table.insert(self.parsedtext, self.resources[foundtag] or foundtag)
	end
	table.insert(self.parsedtext, text:match('[^}]+$'))
end

local metainit = {}

local log2 = 1/math.log(2)
local function nextpo2(n)
	return math.pow(2, math.ceil(math.log(n)*log2))
end

function metainit.Image(res, meta)
	meta.type = 'img'
	local w, h = res:getWidth(), res:getHeight()
	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)
	-- find first space, split again later if necessary
	if x > 0 then
		local n = fragment:find(' ', 1, true)
		while n do
			local newx = x + fnt:getWidth(fragment:sub(1, n-1))
			if newx > width then
				break
			end
			n = fragment:find(' ', n + 1, true)
		end
		n = n or (#fragment + 1)
		-- wrapping
		parsedtext[i] = fragment:sub(1, n-1)
		table.insert(parsedtext, i+1, fragment:sub(n))
		lines[#lines].height = maxheight
		maxheight = 0
		x = 0
		table.insert(lines, {})
	end
	return maxheight, x > 0 and x + fnt:getWidth' ' or 0
end

local function renderText(parsedtext, fragment, lines, maxheight, x, width, i)
	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)
	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)
	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)
		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
	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
				local colorMode = love.graphics.getColorMode()
				love.graphics.setColorMode('modulate')
				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
				love.graphics.setColorMode(colorMode)
			elseif fragment.type == 'img' then
				local colorMode = love.graphics.getColorMode()
				love.graphics.setColorMode('replace')
				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.setColorMode(colorMode)
			elseif fragment.type == 'font' then
				love.graphics.setFont(fragment[1])
			elseif fragment.type == 'color' then
				love.graphics.setColor(unpack(fragment))
			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(width, nofb)
	local renderWidth = 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)
	-- 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 = nextpo2(math.max(love.graphics.getWidth(), width or 0))
	local fbHeight = nextpo2(math.max(love.graphics.getHeight(), self.height))
	self.framebuffer = love.graphics.newFramebuffer(fbWidth, fbHeight)
	love.graphics.setFont(firstFont)
	if not nofb then
		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

Post Reply

Who is online

Users browsing this forum: No registered users and 0 guests