1--------------------------------------------------------------------------------
2--------------------------------------------------------------------------------
3--
4--  file:    fonts.lua
5--  brief:   font handler, with automatic texture atlas generation
6--  author:  Dave Rodgers
7--
8--  Copyright (C) 2007.
9--  Licensed under the terms of the GNU GPL, v2 or later.
10--
11--------------------------------------------------------------------------------
12--------------------------------------------------------------------------------
13
14if (fontHandler ~= nil) then
15  return fontHandler
16end
17
18-- ":n:" sets it to nearest texture filtering
19local DefaultFontName = ":n:" .. LUAUI_DIRNAME .. "Fonts/FreeMonoBold_12"
20
21Spring.CreateDir(LUAUI_DIRNAME .. 'Fonts')
22
23
24--------------------------------------------------------------------------------
25--------------------------------------------------------------------------------
26
27-- Automatically generated local definitions
28
29local glCallList             = gl.CallList
30local glColor                = gl.Color
31local glCreateList           = gl.CreateList
32local glDeleteList           = gl.DeleteList
33local glDeleteTexture        = gl.DeleteTexture
34local glPopMatrix            = gl.PopMatrix
35local glPushMatrix           = gl.PushMatrix
36local glTexRect              = gl.TexRect
37local glTexture              = gl.Texture
38local glTranslate            = gl.Translate
39local spGetLastUpdateSeconds = Spring.GetLastUpdateSeconds
40
41
42--------------------------------------------------------------------------------
43--------------------------------------------------------------------------------
44--
45--  Local speedups
46--
47
48local floor     = math.floor
49local strlen    = string.len
50local strsub    = string.sub
51local strbyte   = string.byte
52local strchar   = string.char
53local strfind   = string.find
54local strgmatch = string.gmatch
55
56
57--------------------------------------------------------------------------------
58--------------------------------------------------------------------------------
59
60local fonts = {}
61local activeFont  = nil
62local defaultFont = nil
63
64local caching = true
65local useFloor = true
66
67local timeStamp  = 0
68local lastUpdate = 0
69
70local debug = false
71local origPrint = print
72local print = function(...)
73  if (debug) then
74    origPrint(...)
75  end
76end
77
78
79--------------------------------------------------------------------------------
80--------------------------------------------------------------------------------
81
82local function HaveFontFiles(fontName)
83  if (VFS.FileExists(fontName .. '.lua') and
84      VFS.FileExists(fontName .. '.png')) then
85    return true
86  end
87  return false
88end
89
90
91local function CreateFontFiles(fontName)
92  local _, _, name, size = string.find(fontName, '^(.*)_(%d*)$')
93  if ((not name) or (not size)) then
94    return false
95  end
96
97  local fullName = name .. '.ttf'
98  local inData = VFS.LoadFile(fullName)
99  if (not inData) then
100    fullName = name .. '.otf'
101    inData = VFS.LoadFile(fullName)
102  end
103  if (not inData) then
104    return false
105  end
106
107  print('CreateFontFiles = ' .. fullName .. ', ' .. size)
108
109  return
110    Spring.MakeFont(fullName, {
111      inData = inData,
112      height = tonumber(size),
113      minChar = 0,
114      maxChar = 255,
115    --[[
116      texWidth =
117      outlineMode =
118      outlineRadius =
119      outlineWeight =
120      padding =
121      spacing =
122      debug =
123    --]]
124    })
125end
126
127
128local function LoadFontSpecs(fontName)
129  local specFile = fontName .. ".lua"
130  local text = VFS.LoadFile(specFile)
131  if (text == nil) then
132    return nil
133  end
134  local chunk, err = loadstring(text, specFile)
135  if (not chunk) then
136    return nil
137  end
138  local fontSpecs = chunk()
139
140  print('fontSpecs.srcFile  = ' .. fontSpecs.srcFile)
141  print('fontSpecs.family   = ' .. fontSpecs.family)
142  print('fontSpecs.style    = ' .. fontSpecs.style)
143  print('fontSpecs.size     = ' .. fontSpecs.height)
144  print('fontSpecs.yStep    = ' .. fontSpecs.yStep)
145  print('fontSpecs.xTexSize = ' .. fontSpecs.xTexSize)
146  print('fontSpecs.yTexSize = ' .. fontSpecs.yTexSize)
147
148  return fontSpecs
149end
150
151
152--------------------------------------------------------------------------------
153--------------------------------------------------------------------------------
154
155local function MakeDisplayLists(fontSpecs)
156  local lists = {}
157  local xs = fontSpecs.xTexSize
158  local ys = fontSpecs.yTexSize
159  for _,gi in pairs(fontSpecs.glyphs) do
160    local list = glCreateList(function ()
161      glTexRect(gi.oxn, gi.oyn, gi.oxp, gi.oyp,
162                 gi.txn / xs, 1.0 - (gi.tyn / ys),
163                 gi.txp / xs, 1.0 - (gi.typ / ys))
164      glTranslate(gi.adv, 0, 0)
165    end)
166    lists[gi.num] = list
167  end
168  return lists
169end
170
171
172local function MakeOutlineDisplayLists(fontSpecs)
173  local lists = {}
174  local tw = fontSpecs.xTexSize
175  local th = fontSpecs.yTexSize
176
177  for _,gi in pairs(fontSpecs.glyphs) do
178    local w = gi.xmax - gi.xmin
179    local h = gi.ymax - gi.ymin
180    local txn = gi.xmin / tw
181    local tyn = gi.ymax / th
182    local txp = gi.xmax / tw
183    local typ = gi.ymin / th
184
185    local list = glCreateList(function ()
186      glTranslate(gi.initDist, 0, 0)
187
188      glColor(0, 0, 0, 0.75)
189      local o = 2
190      glTexRect( o,  o, w, h, txn, tyn, txp, typ)
191      glTexRect(-o,  o, w, h, txn, tyn, txp, typ)
192      glTexRect( o,  0, w, h, txn, tyn, txp, typ)
193      glTexRect(-o,  0, w, h, txn, tyn, txp, typ)
194      glTexRect( o, -o, w, h, txn, tyn, txp, typ)
195      glTexRect(-o, -o, w, h, txn, tyn, txp, typ)
196      glTexRect( 0,  o, w, h, txn, tyn, txp, typ)
197      glTexRect( 0, -o, w, h, txn, tyn, txp, typ)
198
199      glColor(1, 1, 1, 1)
200      glTexRect( 0,  0, w, h, txn, tyn, txp, typ)
201
202      glTranslate(gi.width + gi.whitespace, 0, 0)
203    end)
204
205    lists[gi.num] = list
206  end
207  return lists
208end
209
210
211--------------------------------------------------------------------------------
212
213local function StripColorCodes(text)
214  local stripped = ""
215  for txt, color in strgmatch(text, "([^\255]*)(\255?.?.?.?)") do
216    if (strlen(txt) > 0) then
217      stripped = stripped .. txt
218    end
219  end
220  return stripped
221end
222
223
224--------------------------------------------------------------------------------
225
226local function RawGetTextWidth(text)
227  local specs = activeFont.specs
228  local w = 0
229  for i = 1, strlen(text) do
230    local c = strbyte(text, i)
231    local glyphInfo = specs.glyphs[c]
232    if (not glyphInfo) then
233      glyphInfo = specs.glyphs[32]
234    end
235    if (glyphInfo) then
236      w = w + glyphInfo.adv
237    end
238  end
239  return w
240end
241
242
243local function GetTextWidth(text)
244  -- return the cached value if available
245  local cacheTextData = activeFont.cache[text]
246  if (cacheTextData) then
247    local width = cacheTextData[3]
248    if (width) then
249      return width
250    end
251  end
252  local stripped = StripColorCodes(text)
253  return RawGetTextWidth(stripped)
254end
255
256
257local function CalcTextHeight(text)
258  return activeFont.specs.height
259end
260
261
262--------------------------------------------------------------------------------
263
264local function RawDraw(text)
265  local lists = activeFont.lists
266  for i = 1, strlen(text) do
267    local c = strbyte(text, i)
268    local list = lists[c]
269    if (list) then
270      glCallList(list)
271    else
272      glCallList(lists[strbyte(" ", 1)])
273    end
274  end
275end
276
277
278local function RawColorDraw(text)
279  for txt, color in strgmatch(text, "([^\255]*)(\255?.?.?.?)") do
280    if (strlen(txt) > 0) then
281      RawDraw(txt)
282    end
283    if (strlen(color) == 4) then
284      glColor(strbyte(color, 2) / 255,
285              strbyte(color, 3) / 255,
286              strbyte(color, 4) / 255)
287    end
288  end
289end
290
291
292local function DrawNoCache(text, x, y)
293  if (not x) then
294    RawDraw(text)
295  else
296    glPushMatrix()
297    glTranslate(x, y, 0)
298    glTexture(activeFont.image)
299    RawColorDraw(text)
300    glTexture(false)
301    glPopMatrix()
302  end
303end
304
305
306local function Draw(text, x, y)
307  if (not caching) then
308    DrawNoCache(text, x, y)
309    return
310  end
311
312  local cacheTextData = activeFont.cache[text]
313  if (not cacheTextData) then
314    glTexture(activeFont.image) -- else we would _recreate_ the texture each call to the displaylist!
315    local textList = glCreateList(function()
316      glTexture(activeFont.image)
317      RawColorDraw(text)
318      glTexture(false)
319    end)
320    cacheTextData = { textList, timeStamp }  -- param [3] is the width
321    activeFont.cache[text] = cacheTextData
322  else
323    cacheTextData[2] = timeStamp  --  refresh the timeStamp
324  end
325
326  if (not x) then
327    glCallList(cacheTextData[1])
328  else
329    glPushMatrix()
330    if (useFloor) then
331      glTranslate(floor(x), floor(y), 0)
332    else
333      glTranslate(x, y, 0)
334    end
335    glCallList(cacheTextData[1])
336    glPopMatrix()
337  end
338end
339
340
341--------------------------------------------------------------------------------
342--------------------------------------------------------------------------------
343
344local function DrawRight(text, x, y)
345  local width = GetTextWidth(text)
346  glPushMatrix()
347  glTranslate(-width, 0, 0)
348  Draw(text, x, y)
349  glPopMatrix()
350end
351
352
353local function DrawCentered(text, x, y)
354  local width = GetTextWidth(text)
355  local halfWidth
356  if (useFloor) then
357    halfWidth = floor(width * 0.5)
358  else
359    halfWidth = width * 0.5
360  end
361  glPushMatrix()
362  glTranslate(-halfWidth, 0, 0)
363  Draw(text, x, y)
364  glPopMatrix()
365end
366
367
368--------------------------------------------------------------------------------
369--------------------------------------------------------------------------------
370
371local function LoadFont(fontName)
372  print('LoadFont:  ' .. fontName)
373
374  if (fonts[fontName]) then
375    return nil  -- already loaded
376  end
377
378  local baseName = fontName
379  local _,_,options,bn = strfind(fontName, "(:.-:)(.*)")
380  if (options) then
381    baseName = bn
382  else
383    options = ''
384  end
385
386  if (not HaveFontFiles(baseName)) then
387    CreateFontFiles(baseName)
388  end
389
390  local fontSpecs = LoadFontSpecs(baseName)
391  if (not fontSpecs) then
392    return nil  -- bad specs
393  end
394
395  if (not VFS.FileExists(baseName .. ".png")) then
396    return nil  -- missing texture
397  end
398
399  local fontLists
400  if (strfind(options, "o")) then
401    fontLists = MakeOutlineDisplayLists(fontSpecs)
402  else
403    fontLists = MakeDisplayLists(fontSpecs)
404  end
405  if (not fontLists) then
406    return nil  -- bad display lists
407  end
408
409  local font = {}
410  font.name  = fontName
411  font.base  = baseName
412  font.opts  = options
413  font.specs = fontSpecs
414  font.lists = fontLists
415  font.cache = {}
416  font.image = options .. baseName .. ".png"
417
418  fonts[fontName] = font
419
420  return font
421end
422
423
424local function UseFont(fontName)
425  local font = fonts[fontName]
426  if (font) then
427    activeFont = font
428    return true
429  end
430
431  font = LoadFont(fontName)
432  if (font) then
433    activeFont = font
434    print("Loaded font: " .. fontName)
435    return true
436  end
437
438  return false
439end
440
441
442local function UseDefaultFont()
443  activeFont = defaultFont
444end
445
446
447local function SetDefaultFont(name)
448  local tmpFont = activeFont
449  if (UseFont(name)) then
450    DefaultFontName = name
451    defaultFont = activeFont
452  end
453  activeFont = tmpFont
454end
455
456
457--------------------------------------------------------------------------------
458--------------------------------------------------------------------------------
459
460local function FreeCache(fontName)
461  local font = (fontName == nil) and activeFont or fonts[fontName]
462  if (not font) then
463    return
464  end
465  for text,data in pairs(font.cache) do
466    glDeleteList(data[1])
467  end
468end
469
470
471local function FreeFont(fontName)
472  local font = (fontName == nil) and activeFont or fonts[fontName]
473  if (not font) then
474    return
475  end
476
477  for _,list in pairs(font.lists) do
478    glDeleteList(list)
479  end
480  for text,data in pairs(font.cache) do
481    glDeleteList(data[1])
482  end
483  glDeleteTexture(font.image)
484
485  fonts[font.name] = nil
486end
487
488
489local function FreeFonts()
490  for fontName in pairs(fonts) do
491    FreeFont(fontName)
492  end
493end
494
495
496local function Update()
497  timeStamp = timeStamp + spGetLastUpdateSeconds()
498  if (timeStamp < (lastUpdate + 1.0)) then
499    return  -- only update every 1.0 seconds
500  end
501
502  local killTime = (timeStamp - 3.0)
503  for fontName, font in pairs(fonts) do
504    local killList = {}
505    for text,data in pairs(font.cache) do
506      if (data[2] < killTime) then
507        glDeleteList(data[1])
508        table.insert(killList, text)
509        print(fontName .. " removed string list(" .. data[1] .. ") " .. text)
510      end
511    end
512    for _,text in ipairs(killList) do
513      font.cache[text] = nil
514    end
515  end
516
517  lastUpdate = timeStamp
518end
519
520
521--------------------------------------------------------------------------------
522--------------------------------------------------------------------------------
523
524UseFont(DefaultFontName)
525defaultFont = activeFont
526
527
528local FH = {}
529
530FH.Update = Update
531
532FH.UseFont        = UseFont
533FH.UseDefaultFont = UseDefaultFont
534FH.SetDefaultFont = SetDefaultFont
535
536FH.GetFontName  = function() return activeFont.name         end
537FH.GetFontSize  = function() return activeFont.specs.height end
538FH.GetFontYStep = function() return activeFont.specs.yStep  end
539FH.GetTextWidth = GetTextWidth
540
541FH.Draw         = Draw
542FH.DrawRight    = DrawRight
543FH.DrawCentered = DrawCentered
544
545FH.StripColors = StripColorCodes
546
547FH.FreeFont  = FreeFont
548FH.FreeFonts = FreeFonts
549FH.FreeCache = FreeCache
550
551FH.CacheState   = function() return caching  end
552FH.EnableCache  = function() caching = true  end
553FH.DisableCache = function() caching = false end
554
555fontHandler = FH  -- make it global
556
557return FH
558
559--------------------------------------------------------------------------------
560--------------------------------------------------------------------------------
561