view mde/font/font.d @ 97:30470bc19ca4

Floating widgets now work nicely: customizable borders added, resizing, moving. gl.basic abstraction module removed (seemed pointless). Some changes to SimpleRenderer (largely to accomodate floating widgets).
author Diggory Hardy <diggory.hardy@gmail.com>
date Mon, 10 Nov 2008 16:44:44 +0000
parents 97e6dce08037
children 49e7cfed4b34
line wrap: on
line source

/* LICENSE BLOCK
Part of mde: a Modular D game-oriented Engine
Copyright © 2007-2008 Diggory Hardy

This program is free software: you can redistribute it and/or modify it under the terms
of the GNU General Public License as published by the Free Software Foundation, either
version 2 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>. */

/// Sets up freetype (in a basic way).
module mde.font.font;

public import mde.types.Colour;
import mde.lookup.Options;
import mde.font.FontTexture;
import mde.font.exception;

import mde.file.mergetag.Reader;
import mde.file.mergetag.DataSet;
import mde.setup.paths;
import mde.setup.exception;     // InitStage stuff

import derelict.freetype.ft;

import mde.file.deserialize;
import tango.stdc.stringz;
import Util = tango.text.Util;
import tango.util.log.Log : Log, Logger;

// "Publically import" this symbol:
alias mde.font.FontTexture.TextBlock TextBlock;

private Logger logger;
static this () {
    logger = Log.getLogger ("mde.font.font");
}

/** FontStyle class.
 *
 * Particular to a font and size, and any other effects like bold/italic if ever implemented.
 * 
 * Note: it is not currently intended to be thread-safe. */
class FontStyle : IDataSection
{
    //BEGIN Static: manager
    static {
        debug (drawGlyphCache) void drawTexture() {
            if (fontTex !is null)
                fontTex.drawTexture;
        }
        
        /** Load the freetype library with settings from the file fileName. */
        private const fileName = "fonts";
        StageState initialize () {
            if (FT_Init_FreeType (&library))
                throw new fontException ("error initialising the FreeType library");
            
            // Check version
            FT_Int maj, min, patch;
            FT_Library_Version (library, &maj, &min, &patch);
            if (maj != 2 || min != 3) {
                logger.warn ("Using an untested FreeType version: {}.{}.{}", maj, min, patch);
                logger.info ("The only tested version of freetype is 2.3.5");
            }
            
            // Set LCD filtering method if LCD rendering is enabled.
            const RMF = FT_LOAD_TARGET_LCD | FT_LOAD_TARGET_LCD_V;
            if (fontOpts.renderMode & RMF &&
                FT_Library_SetLcdFilter(library, cast(FT_LcdFilter)fontOpts.lcdFilter)) {
                /* An error occurred, presumably because LCD rendering support
                * is not compiled into the library. */
                logger.warn ("Bad/unsupported LCD filter option; disabling LCD font rendering.");
                logger.warn ("Your FreeType 2 library may be compiled without support for LCD/sub-pixel rendering.");
                
                // Reset the default filter (in case an invalid value was set in config files).
                fontOpts.set!(int) ("lcdFilter", FT_LcdFilter.FT_LCD_FILTER_DEFAULT);
                
                /* If no support for LCD filtering, then LCD rendering only emulates NORMAL with 3
                 * times wider glyphs. So disable and save the extra work. */
                fontOpts.set!(int) ("renderMode", FT_LOAD_TARGET_NORMAL);
            }
            
            /* Load font settings
             *
             * Each mergetag section corresponds to a font; each is loaded whether used or not
             * (however the actual font files are only loaded on use). A fallback id must be
             * provided in the header which must match a loaded font name; if a non-existant font
             * is requested a warning will be logged and this font returned. */
            char[] fallbackName;
            try {
                IReader reader;
                reader = confDir.makeMTReader (fileName, PRIORITY.LOW_HIGH, null, true);
                reader.dataSecCreator = delegate IDataSection(ID id) {
                    auto f = new FontStyle;
                    fonts[id] = f;
                    return f;
                };
                reader.read;
                
                // get fallback name
                char[]* p = "fallback" in reader.dataset.header._charA;
                if (p is null)
                    throw new fontException ("No fallback font style specified");
                fallbackName = *p;
            } catch (NoFileException) {
                throw new fontException ("No font settings file (fonts.[mtt|mtb])");
            } catch (Exception e) {
                throw new fontException ("Reading font settings failed: "~e.msg);
            }
            
            // Find the fallback
            FontStyle* p = fallbackName in fonts;
            if (p is null)
                throw new fontException ("Fallback font style specified is not found");
            fallback = *p;
            // Load the fallback now, to ensure it's available.
            // Also note that get() doesn't make sure the fallback is loaded before returning it.
            fallback.load;
            
            return StageState.ACTIVE;
        }
        
        /** Cleanup: delete all fonts. */
        StageState cleanup () {
            // Clear loaded fonts (each has an FT_Face object needing to be freed):
            foreach (fs; fonts)
                fs.freeFace;
            
            FT_Done_FreeType (library); // free the library
            
            return StageState.INACTIVE;
        }
        
        /** Get a FontStyle instance, for a section in the fonts.mtt file.
          *
          * Also loads the font if it's not already loaded, so the first call may take some time.
          *
          * Uses fallback font-style if the desired style isn't known about or fails to load, so
          * this function should never fail or throw, in theory (unless out of memory). The
          * fallback should already be loaded. */
        FontStyle get(char[] name) {
            FontStyle* p = name in fonts;
            if (p is null) {
                logger.warn ("Font style "~name~" requested but not found; reverting to the fallback style.");
                fonts[name] = fallback;	// set to prevent another warning getting logged
                return fallback;
            }
            // Got it, but we need to make sure it's loaded:
            try {
                p.load;
            } catch (Exception e) {
                logger.warn ("Font style "~name~" failed to load; reverting to the fallback style.");
                return fallback;
            }
            return *p;
        }
        
    private:
        FT_Library	library;
        FontTexture	fontTex;
        FontStyle[ID]	fonts;		// all font styles known about; not necessarily loaded
        FontStyle	fallback;	// used when requested font isn't in fonts
    }
    //END Static
    
    this() {}
    
    //BEGIN Mergetag code
    //NOTE: would it be better not to use a new mergetag file for this?
    //FIXME: revise when gui can set options
    void addTag (char[] tp, ID id, char[] dt) {
        if (tp == "char[]") {
            if (id == "path")
                path = parseTo!(char[]) (dt);
        }
        else if (tp == "int") {
            if (id == "size")
                size = parseTo!(int) (dt);
        }
    }
    void writeAll (ItemDelg) {}		// no writing the config for now
    //END Mergetag code
    
    /** Load the font file.
     *
     * Even if the same font is used at multiple sizes, multiple copies of FT_Face are used.
     * Sharing an FT_Face would require calling FT_Set_Pixel_Sizes each time a glyph is rendered or
     * swapping the size information (face.size)? */
    void load ()
    in {
        assert (library !is null, "font: library is null");
    } body {
        if (FT_New_Face (library, toStringz(path), 0, &face))
            throw new fontLoadException ("Unable to read font: "~path);
        
        if (!FT_IS_SCALABLE (face))
            throw new fontLoadException ("Currently no support for non-scalable fonts (which " ~
                    path ~ " is). Please report if you want to see support.");
        /* The following will need to be addressed when adding support for non-scalables:
         *	Use of face.size.metrics.height property.
         */
        
        if (FT_Set_Pixel_Sizes (face, 0,size))
            throw new fontLoadException ("Unable to set pixel size");
        
        // Create if necessary:
        if (fontTex is null)
            fontTex = new FontTexture;
    }
    
    /** Update a TextBlock cache, as used by the textBlock function.
     *
     * The only use of this is to get the text block's size ahead of rendering, via TextBlock's w
     * and h properties.
     *
     * This function will only actually update the cache if it is invalid, caused either by the
     * font being changed or if cache.cacheVer < 0. */
    void updateBlock (char[] str, ref TextBlock cache)
    in {
        debug assert (face, "FontStyle: face is null");
    } body {
        try {
            fontTex.updateCache (face, str, cache);
        } catch (Exception e) {
            logger.warn ("Exception while drawing text: "~e.msg);
        }
    }
    
    /** Draw a block of text (may inlcude new-lines).
     *
     * The text block is drawn with top-left corner at x,y. To put the text's baseline at a given
     * y coordinate would require some changes. Line height is fixed based on largest glyph.
     * Due to hinter, glyphs are not guaranteed to lie within the "bounding box" defined by cache.
     * Can be changed to test size of each glyph if necessary.
     *
     * Specify the text's colour with col. Use textBlockA() instead  for transparent text.
     *
     * As a CPU-side code optimisation, store a TextBlock (unique to str) and pass a reference as
     * the cache argument. This is the recommended method, although for one-time calls when you
     * don't need to know the size, the other version of textBlock may be used.
     * ---------------------------------
     * char[] str;
     * TextBlock strCache;
     * textBlock (x, y, str, strCache);
     * ---------------------------------
     * The TextBlock cache will be updated as necessary. Besides the initial update, this will only
     * be if the font changes, or it is manually invalidated. This can be done by setting the
     * TextBlock's cacheVer property to -1, which should be done if str is changed.
     *
     * The TextBlock's w and h properties are set to the size (in pixels) of the text block; other
     * than this cache only serves as a small optimisation. However, the only way to get the size
     * of a text block is to use a TextBlock cache and update it, either with this function or with
     * the updateBlock function. */
    void textBlock (int x, int y, char[] str, ref TextBlock cache, Colour col)
    in {
        assert (face, "FontStyle: face is null");
    } body {
        try {
            fontTex.drawCache (face, str, cache, x, y, col);
        } catch (Exception e) {
            logger.warn ("Exception while drawing text: "~e.msg);
        }
    }
    /** ditto */
    void textBlock (int x, int y, char[] str, Colour col)
    in {
        assert (face, "FontStyle: face is null");
    } body {
        try {
            // Using the cache method for one-time use is slightly less than optimal, but doing so
            // isn't really recommended anyway (and maintaining two versions of fontTex.drawText
            // would be horrible).
            TextBlock cache;
            fontTex.drawCache (face, str, cache, x, y, col);
        } catch (Exception e) {
            logger.warn ("Exception while drawing text: "~e.msg);
        }
    }
    
    /** A variation of textBlock for transparency.
     *
     * Set the alpha by calling glColor*() first. See FontTexture.drawCacheA()'s documentation for
     * details. */
    void textBlockA (int x, int y, char[] str, ref TextBlock cache, Colour col) 
    in {
        assert (face, "FontStyle: face is null");
    } body {
        try {
            fontTex.drawCacheA (face, str, cache, x, y, col);
        } catch (Exception e) {
            logger.warn ("Exception while drawing text: "~e.msg);
        }
    }
    
    /** The font-specified vertical distance between the baseline of consecutive lines. */
    int getLineSeparation ()
    in {
        assert (face, "FontStyle: face is null");
    } body {
        return face.size.metrics.height >> 6;
    }
    
    void freeFace () {
        FT_Done_Face (face);
        face = null;    // functions using face use assertions on face to check its validity.
    }
    
private:
    char[]	path;	// path to font file
    int		size;	// font size
    
    FT_Face	face;
    
    debug(mdeUnitTest) unittest {
        // Don't do a unittest since font relies on loading the freetype library dynamically,
        // normally done by Init. Also font is mostly visual and many problems will be obvious.
    }
}

/+class OptionsFont : Options {
    alias store!(+/