libevent: rename `bind_to` to the more precise `bind_tcp`
[nit.git] / lib / gamnit / bmfont.nit
index 8ad2dce..d052922 100644 (file)
@@ -24,7 +24,7 @@ module bmfont
 
 private import dom
 
-import font
+intrude import font
 
 # BMFont description, parsed with `Text::parse_bmfont` or loaded as a `BMFontAsset`
 #
@@ -87,6 +87,13 @@ class BMFont
        # Distance between certain characters
        var kernings = new HashMap2[Char, Char, Float]
 
+       # Additional distance between `prev_char` and `char`
+       fun kerning(prev_char: nullable Char, char: Char): Float
+       do
+               if prev_char == null then return 0.0
+               return kernings[prev_char, char] or else 0.0
+       end
+
        redef fun to_s do return "<{class_name} {face} at {size} pt, "+
                                 "{pages.length} pages, {chars.length} chars>"
 
@@ -132,13 +139,16 @@ class BMFontChar
        var xadvance: Float
 
        # Full texture contaning this character and others
-       var page: TextureAsset
+       var page: RootTexture
 
        # TODO Channel where the image is found
        #var chnl: Int
 
        # Subtexture with this character image only
-       var subtexture: Texture = page.subtexture(x, y, width, height) is lazy
+       var subtexture: Texture = page.subtexture(x, y, width, height) is lazy, writable
+
+       # Scale to apply to this char only
+       var scale = 1.0 is writable
 end
 
 redef class Text
@@ -177,7 +187,7 @@ redef class Text
        # assert fnt.to_s == "<BMFont arial at 72.0 pt, 1 pages, 3 chars>"
        # assert fnt.line_height == 80.0
        # assert fnt.kernings['A', 'C'] == -1.0
-       # assert fnt.chars['A'].page.path == "dir_in_assets/arial.png"
+       # assert fnt.chars['A'].page.as(TextureAsset).path == "dir_in_assets/arial.png"
        # ~~~
        fun parse_bmfont(dir: String): MaybeError[BMFont, Error]
        do
@@ -270,9 +280,10 @@ end
 # ~~~
 # redef class App
 #     var font = new BMFontAsset("arial.fnt")
-#     var ui_text = new TextSprites(font, ui_camera.top_left)
+#     var pos: Point3d[Float] = ui_camera.top_left.offset(128.0, -128.0, 0.0)
+#     var ui_text = new TextSprites(font, pos)
 #
-#     redef fun on_create
+#     redef fun create_scene
 #     do
 #         super
 #
@@ -354,34 +365,158 @@ class BMFontAsset
        do
                var dx = 0.0
                var dy = 0.0
+               var text_width = 0.0
+               var line_sprites = new Array[Sprite]
+               var height = 0.0
 
-               var line_height = desc.line_height
+               # Has the current line height been added to `height`?
+               var line_height_counted = false
+
+               # TextSprite customization
+               var max_width = text_sprites.max_width
+               var max_height = text_sprites.max_height
+               var scale = text_sprites.scale
+
+               # Font customization
+               var line_height = desc.line_height * scale
                var partial_line_skip = line_height * partial_line_mod.to_f
 
+               # Links data
+               text_sprites.links.clear
+               var in_link = false
+               var link_sprites = new Array[Sprite]
+               var link_name = ""
+
+               # Loop over all characters
                var prev_char = null
-               for c in text do
+               var i = -1
+               while i < text.length - 1 do
+                       i += 1
+                       var c = text[i]
 
                        # Special characters
+                       var word_break = false
                        if c == '\n' then
-                               dy -= line_height.to_f
-                               dx = 0.0 #advance/2.0
+                               justify(line_sprites, text_sprites.align, dx)
+                               dy -= line_height
+                               if max_height != null and max_height < -dy + line_height then break
+                               dx = 0.0
+                               if not line_height_counted then
+                                       # Force to account for empty lines
+                                       height += line_height
+                               end
+                               line_height_counted = false
                                prev_char = null
                                continue
                        else if c == pld then
                                dy -= partial_line_skip
-                               prev_char = null
+                               height += partial_line_skip
+                               word_break = true
                                continue
                        else if c == plu then
                                dy += partial_line_skip
-                               prev_char = null
+                               height -= partial_line_skip # We could keep two heights and return the max
+                               word_break = true
                                continue
                        else if c.is_whitespace then
-                               var advance = if desc.chars.keys.has(' ') then
+                               var space_advance = if desc.chars.keys.has(' ') then
                                                desc.chars[' '].xadvance
                                        else if desc.chars.keys.has('f') then
                                                desc.chars['f'].xadvance
                                        else 16.0
-                               dx += advance
+                               dx += space_advance * scale
+                               word_break = true
+                       else if c == '[' then
+                               # Open link?
+                               if i + 1 < text.length and text[i+1] == '[' then
+                                       # Escape if duplicated
+                                       i += 1
+                               else
+                                       in_link = true
+                                       continue
+                               end
+                       else if c == ']' then
+                               # Close link?
+                               if i + 1 < text.length and text[i+1] == ']' then
+                                       # Escape if duplicated
+                                       i += 1
+                               else
+                                       # If there's a () use it as link_name
+                                       var j = i + 1
+                                       if j < text.length and text[j] == '(' then
+                                               var new_name
+                                               new_name = ""
+                                               loop
+                                                       j += 1
+                                                       if j > text.length then
+                                                               # No closing ), abort
+                                                               new_name = null
+                                                               break
+                                                       end
+
+                                                       var l = text[j]
+                                                       if l == ')' then break
+                                                       new_name += l.to_s
+                                               end
+                                               if new_name != null then
+                                                       link_name = new_name
+                                                       i = j
+                                               end
+                                       end
+
+                                       # Register the link for the clients
+                                       text_sprites.links[link_name] = link_sprites
+
+                                       # Prepare next link
+                                       in_link = false
+                                       link_sprites = new Array[Sprite]
+                                       link_name = ""
+                                       continue
+                               end
+                       end
+
+                       if in_link then link_name += c.to_s
+
+                       # End of a word?
+                       if word_break then
+                               # If we care about line width, check for overflow
+                               if max_width != null then
+                                       # Calculate the length of the next word
+                                       var prev_w = null
+                                       var word_len = 0.0
+                                       for wi in [i+1..text.length[ do
+                                               var w = text[wi]
+
+                                               if w == '\n' or w == pld or w == plu or w.is_whitespace or (in_link and w == ']') then break
+
+                                               if not desc.chars.keys.has(w) then
+                                                       var rc = replacement_char
+                                                       if rc == null then continue
+                                                       w = rc
+                                               end
+
+                                               word_len += advance(prev_w, w) * scale
+                                               prev_w = w
+                                       end
+
+                                       # Would the line be too long?
+                                       if dx + word_len > max_width then
+                                               justify(line_sprites, text_sprites.align, dx)
+                                               dy -= line_height
+                                               if max_height != null and max_height < -dy + line_height then break
+                                               dx = 0.0
+                                               line_height_counted = false
+
+                                               if not text_sprites.wrap then
+                                                       # Cut short, skip everything until the next new line
+                                                       while c != '\n' and i < text.length - 1 do
+                                                               i += 1
+                                                               c = text[i]
+                                                       end
+                                               end
+                                       end
+                               end
+
                                prev_char = null
                                continue
                        end
@@ -395,27 +530,60 @@ class BMFontAsset
 
                        var char_info = desc.chars[c]
                        var advance = char_info.xadvance
+                       var kerning = desc.kerning(prev_char, c)
 
-                       var kerning = 0.0
-                       if prev_char != null then
-                               kerning = desc.kernings[prev_char, c] or else 0.0
-                       end
-
-                       var x = dx + char_info.width/2.0  + char_info.xoffset + kerning
-                       var y = dy - char_info.height/2.0 - char_info.yoffset
+                       var x = dx + (char_info.width/2.0  + char_info.xoffset + kerning) * scale
+                       var y = dy - (char_info.height/2.0 + char_info.yoffset) * scale
                        var pos = text_sprites.anchor.offset(x, y, 0.0)
-                       text_sprites.sprites.add new Sprite(char_info.subtexture, pos)
+                       var s = new Sprite(char_info.subtexture, pos)
+                       s.scale = scale * char_info.scale
+                       text_sprites.sprites.add s
+                       line_sprites.add s
+                       if in_link then link_sprites.add s
 
-                       dx += advance + kerning
+                       dx += (advance + kerning) * scale
                        prev_char = c
+
+                       text_width = text_width.max(dx)
+
+                       if not line_height_counted then
+                               # Increase `height` only once per line iff there's a caracter
+                               line_height_counted = true
+                               height += line_height
+                       end
                end
+
+               justify(line_sprites, text_sprites.align, dx)
+
+               # valign
+               if text_sprites.valign != 0.0 then
+                       var d = (-dy+line_height) * text_sprites.valign
+                       for s in text_sprites.sprites do s.center.y += d
+               end
+
+               text_sprites.width = text_width.max(dx)
+               text_sprites.height = height
        end
 
-       # Character replacing other charactesr missing from the font
+       # Character replacing other characters missing from the font
        private var replacement_char: nullable Char is lazy do
                for c in  "�?".chars do
                        if desc.chars.keys.has(c) then return c
                end
                return null
        end
+
+       private fun advance(prev_char: nullable Char, char: Char): Float
+       do
+               var char_info = desc.chars[char]
+               var kerning = desc.kerning(prev_char, char)
+               return char_info.xadvance + kerning
+       end
+
+       private fun justify(line_sprites: Array[Sprite], align: Float, line_width: Float)
+       do
+               var dx = -line_width*align
+               for s in line_sprites do s.center.x += dx
+               line_sprites.clear
+       end
 end