BMFont from the assets folder

redef class App
    var font = new BMFontAsset("arial.fnt")
    var pos: Point3d[Float] = ui_camera.top_left.offset(128.0, -128.0, 0.0)
    var ui_text = new TextSprites(font, pos)

    redef fun create_scene
    do
        super

        font.load
        assert font.error == null

        ui_text.text = "Hello world!"
    end
end

Introduced properties

fun desc: BMFont

gamnit :: BMFontAsset :: desc

Font description
fun error: nullable Error

gamnit :: BMFontAsset :: error

Error at loading
protected fun error=(error: nullable Error)

gamnit :: BMFontAsset :: error=

Error at loading
fun load

gamnit :: BMFontAsset :: load

Load font description and textures from the assets folder

Redefined properties

redef type SELF: BMFontAsset

gamnit $ BMFontAsset :: SELF

Type of this instance, automatically specialized in every class
redef fun write_into(text_sprites: TextSprites, text: Text)

gamnit $ BMFontAsset :: write_into

Backend writing service, clients should use TextSprites.text=

All properties

fun !=(other: nullable Object): Bool

core :: Object :: !=

Have self and other different values?
fun ==(other: nullable Object): Bool

core :: Object :: ==

Have self and other the same value?
type CLASS: Class[SELF]

core :: Object :: CLASS

The type of the class of self.
type SELF: Object

core :: Object :: SELF

Type of this instance, automatically specialized in every class
protected fun class_factory(name: String): CLASS

core :: Object :: class_factory

Implementation used by get_class to create the specific class.
fun class_name: String

core :: Object :: class_name

The class name of the object.
init defaultinit(path: String)

app :: Asset :: defaultinit

fun desc: BMFont

gamnit :: BMFontAsset :: desc

Font description
fun error: nullable Error

gamnit :: BMFontAsset :: error

Error at loading
protected fun error=(error: nullable Error)

gamnit :: BMFontAsset :: error=

Error at loading
fun get_class: CLASS

core :: Object :: get_class

The meta-object representing the dynamic type of self.
fun hash: Int

core :: Object :: hash

The hash code of the object.
init init

core :: Object :: init

fun inspect: String

core :: Object :: inspect

Developer readable representation of self.
protected fun inspect_head: String

core :: Object :: inspect_head

Return "CLASSNAME:#OBJECTID".
intern fun is_same_instance(other: nullable Object): Bool

core :: Object :: is_same_instance

Return true if self and other are the same instance (i.e. same identity).
fun is_same_serialized(other: nullable Object): Bool

core :: Object :: is_same_serialized

Is self the same as other in a serialization context?
intern fun is_same_type(other: Object): Bool

core :: Object :: is_same_type

Return true if self and other have the same dynamic type.
fun load

gamnit :: BMFontAsset :: load

Load font description and textures from the assets folder
intern fun object_id: Int

core :: Object :: object_id

An internal hash code for the object based on its identity.
fun output

core :: Object :: output

Display self on stdout (debug only).
intern fun output_class_name

core :: Object :: output_class_name

Display class name on stdout (debug only).
fun partial_line_mod: Numeric

gamnit :: Font :: partial_line_mod

Line spacing modifier for pld and plu
fun partial_line_mod=(partial_line_mod: Numeric)

gamnit :: Font :: partial_line_mod=

Line spacing modifier for pld and plu
fun path: String

app :: Asset :: path

Path to this asset within the assets folder
protected fun path=(path: String)

app :: Asset :: path=

Path to this asset within the assets folder
fun serialization_hash: Int

core :: Object :: serialization_hash

Hash value use for serialization
intern fun sys: Sys

core :: Object :: sys

Return the global sys object, the only instance of the Sys class.
abstract fun to_jvalue(env: JniEnv): JValue

core :: Object :: to_jvalue

fun to_s: String

core :: Object :: to_s

User readable representation of self.
protected abstract fun write_into(text_sprites: TextSprites, text: Text)

gamnit :: Font :: write_into

Backend writing service, clients should use TextSprites.text=
package_diagram gamnit::BMFontAsset BMFontAsset app::Asset Asset gamnit::BMFontAsset->app::Asset gamnit::Font Font gamnit::BMFontAsset->gamnit::Font core::Object Object app::Asset->core::Object gamnit::Font->core::Object ...core::Object ... ...core::Object->core::Object

Ancestors

interface Object

core :: Object

The root of the class hierarchy.

Parents

abstract class Asset

app :: Asset

Resource from the assets folder
abstract class Font

gamnit :: Font

Abstract font, drawn by a TextSprites

Class definitions

gamnit $ BMFontAsset
# BMFont from the assets folder
#
# ~~~
# redef class App
#     var font = new BMFontAsset("arial.fnt")
#     var pos: Point3d[Float] = ui_camera.top_left.offset(128.0, -128.0, 0.0)
#     var ui_text = new TextSprites(font, pos)
#
#     redef fun create_scene
#     do
#         super
#
#         font.load
#         assert font.error == null
#
#         ui_text.text = "Hello world!"
#     end
# end
# ~~~
class BMFontAsset
	super Asset
	super Font

	# Font description
	#
	# Require: `error == null`
	fun desc: BMFont
	do
		# Cached results
		var cache = desc_cache
		if cache != null then return cache
		var error = error
		assert error == null else print_error error

		# Load on first access
		load
		error = self.error
		assert error == null else print_error error

		return desc_cache.as(not null)
	end

	private var desc_cache: nullable BMFont = null

	# Error at loading
	var error: nullable Error = null

	# XML description in the assets folder
	private var text_asset = new TextAsset(path) is lateinit

	# Load font description and textures from the assets folder
	#
	# Sets `error` if an error occurred, otherwise
	# the font description can be accessed via `desc`.
	fun load
	do
		var text_asset = text_asset
		text_asset.load
		var error = text_asset.error
		if error != null then
			self.error = error
			return
		end

		var desc = text_asset.to_s
		var fnt_or_error = desc.parse_bmfont(path.dirname)
		if fnt_or_error.is_error then
			self.error = fnt_or_error.error
			return
		end

		var fnt = fnt_or_error.value
		self.desc_cache = fnt

		# Load textures too
		for page_name, texture in fnt.pages do
			texture.load

			# Move up any texture loading error.
			# This algo keeps only the latest error,
			# but this isn't a problem on single page fonts.
			error = texture.error
			if error != null then self.error = error
		end
	end

	redef fun write_into(text_sprites, text)
	do
		var dx = 0.0
		var dy = 0.0
		var text_width = 0.0
		var line_sprites = new Array[Sprite]
		var height = 0.0

		# 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
		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
				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
				height += partial_line_skip
				word_break = true
				continue
			else if c == plu then
				dy += partial_line_skip
				height -= partial_line_skip # We could keep two heights and return the max
				word_break = true
				continue
			else if c.is_whitespace 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 += 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

			# Replace or skip unknown characters
			if not desc.chars.keys.has(c) then
				var rc = replacement_char
				if rc == null then continue
				c = rc
			end

			var char_info = desc.chars[c]
			var advance = char_info.xadvance
			var kerning = desc.kerning(prev_char, c)

			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)
			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) * 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 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
lib/gamnit/bmfont.nit:278,1--589,3