Commit d004468
Rename text rendering directory
Changed files (19)
modifiedbirdfont/GtkWindow.valamodifiedlibbirdfont/GlyphSequence.valamodifiedlibbirdfont/Path.valadeletedlibbirdfont/Renderer/CachedFont.valadeletedlibbirdfont/Renderer/Drawing.valadeletedlibbirdfont/Renderer/FallbackFont.valadeletedlibbirdfont/Renderer/FontCache.valadeletedlibbirdfont/Renderer/LineTextArea.valadeletedlibbirdfont/Renderer/Text.valadeletedlibbirdfont/Renderer/TextArea.valadeletedlibbirdfont/Renderer/fontconfig.caddedlibbirdfont/TextRendering/CachedFont.valaaddedlibbirdfont/TextRendering/Drawing.valaaddedlibbirdfont/TextRendering/FallbackFont.valaaddedlibbirdfont/TextRendering/FontCache.valaaddedlibbirdfont/TextRendering/LineTextArea.valaaddedlibbirdfont/TextRendering/Text.valaaddedlibbirdfont/TextRendering/TextArea.valaaddedlibbirdfont/TextRendering/fontconfig.c
Diff
@@ -1,5 +1,5 @@ /*- Copyright (C) 2012s 2013 2014 Johan Mattsson+ Copyright (C) 2012 2013 2014 2015 Johan Mattsson 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@@ -111,7 +111,7 @@ public class GlyphSequence : GLib.Object { g = font.get_glyph_by_name (name); - if (g != null) {+ if (likely (g != null)) { old.add (g); if (a.alternates.size > 0) {@@ -119,7 +119,7 @@ public class GlyphSequence : GLib.Object { string alt_name = a.alternates.get (0); Glyph? alt = font.get_glyph_by_name (alt_name); - if (alt != null) {+ if (likely (alt != null)) { GlyphSequence replacement = new GlyphSequence (); replacement.add (alt); ligature_sequence.replace (old, replacement);@@ -90,7 +90,7 @@ public class Path : GLib.Object { /** The stroke of an outline when the path is not filled. */ public static double stroke_width = 0; public static bool show_all_line_handles = true;- public static bool fill_open_path {get; set;}+ public static bool fill_open_path { get; set; } public double rotation = 0; public double skew = 0;deleted file mode 100644@@ -1,99 +0,0 @@-/*- Copyright (C) 2015 Johan Mattsson-- This library is free software; you can redistribute it and/or modify - it under the terms of the GNU Lesser General Public License as - published by the Free Software Foundation; either version 3 of the - License, or (at your option) any later version.-- This library 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 - Lesser General Public License for more details.-*/--using Gee;--namespace BirdFont {- -public class CachedFont : GLib.Object {- public Font? font;-- public double top_limit {- get { return _top_limit; }- set { _top_limit = value; }- }-- public double bottom_limit {- get { return _bottom_limit; }- set { _bottom_limit = value; }- }- - public double base_line = 0;- double _top_limit = 92.77; // FIXME: load before first glyph- double _bottom_limit = -24.4;- - static FallbackFont fallback_font {- get {- if (_fallback_font == null) {- _fallback_font = new FallbackFont ();- }- - return (!) _fallback_font;- }- }- static FallbackFont? _fallback_font = null;-- public CachedFont (Font? font) {- Glyph? g;- Glyph glyph;- - this.font = font;-- g = get_glyph_by_name ("a");- if (g != null) {- glyph = (!) g;- base_line = glyph.baseline;- top_limit = glyph.top_limit;- bottom_limit = glyph.bottom_limit;- } else {- warning("No default chararacter found in font.");- }- }- - public Glyph? get_glyph_by_name (string name) {- Glyph? g = null;- Font f;- Glyph glyph;- - if (font != null) {- f = (!) font;- g = f.get_glyph_by_name (name);- - if (g != null) {- glyph = (!) g;- glyph.top_limit = f.top_limit;- glyph.baseline = f.base_line;- glyph.bottom_limit = f.bottom_limit;- }- }- - if (g == null && name.char_count () == 1) {- f = fallback_font.get_single_glyph_font (name.get_char (0));- g = f.get_glyph_by_name (name);- - if (g == null) {- return null;- }- - glyph = (!) g;- glyph.top_limit = f.top_limit;- glyph.baseline = f.base_line;- glyph.bottom_limit = f.bottom_limit; - }- - return g;- }-}--}deleted file mode 100644@@ -1,50 +0,0 @@-/*- Copyright (C) 2014 Johan Mattsson-- This library is free software; you can redistribute it and/or modify - it under the terms of the GNU Lesser General Public License as - published by the Free Software Foundation; either version 3 of the - License, or (at your option) any later version.-- This library 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 - Lesser General Public License for more details.-*/--namespace BirdFont {--/** Interface for creating drawing callbacks. */-[Compact]-[CCode (ref_function = "bird_font_drawing_ref", unref_function = "bird_font_drawing_unref")]-public class Drawing {- - public int iterator_refcount = 1;- - public Drawing () {- }-- public void new_path (double x, double y) {- }-- public void curve_to (double xb, double yb, double xc, double yc, double xd, double yd) {- }-- public void close_path (double x, double y) {- }-- public unowned Drawing @ref () {- iterator_refcount++;- return this;- }- - public void unref () {- if (--iterator_refcount == 0) {- this.free ();- }- }- - private extern void free ();-}--}deleted file mode 100644@@ -1,337 +0,0 @@-/*- Copyright (C) 2015 Johan Mattsson-- This library is free software; you can redistribute it and/or modify - it under the terms of the GNU Lesser General Public License as - published by the Free Software Foundation; either version 3 of the - License, or (at your option) any later version.-- This library 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 - Lesser General Public License for more details.-*/--using Gee;--[SimpleType]-[CCode (has_type_id = false)]-public extern struct FcConfig {-}--[CCode (cname = "FcInitLoadConfigAndFonts")]-public extern FcConfig* FcInitLoadConfigAndFonts ();--[CCode (cname = "FcConfigAppFontAddDir")]-public extern string* FcConfigAppFontAddDir (FcConfig* config, string path);--[CCode (cname = "FcConfigSetSysRoot")]-public extern void FcConfigSetSysRoot (FcConfig* config, string path);--[CCode (cname = "FcConfigParseAndLoad")]-public extern bool FcConfigParseAndLoad (FcConfig* config, string path, bool complain);--[CCode (cname = "FcConfigSetCurrent")]-public extern void FcConfigSetCurrent (FcConfig* config);--[CCode (cname = "FcConfigCreate")]-public extern FcConfig* FcConfigCreate ();--[CCode (cname = "FcConfigFilename")]-public extern string FcConfigFilename (string path);--[CCode (cname = "find_font")]-public extern string? find_font (FcConfig* font_config, string characters);--[CCode (cname = "find_font_family")]-public extern string? find_font_family (FcConfig* font_config, string characters);--[CCode (cname = "find_font_file")]-public extern string? find_font_file (FcConfig* font_config, string font_name);--namespace BirdFont {--// TODO: use font config-public class FallbackFont : GLib.Object {- Gee.ArrayList<File> font_directories;- - FontFace* default_font = null;- public static FcConfig* font_config = null;- static bool font_config_started = false;- - string default_font_file_name = "Roboto-Regular.ttf";- string default_font_family_name = "Roboto";-- Gee.HashMap<unichar, CachePair> glyphs;- Gee.ArrayList<CachePair> cached;-- public int max_cached_fonts = 300;- - string? default_font_file = null;- - public FallbackFont () {- string home = Environment.get_home_dir ();- font_directories = new Gee.ArrayList<File> ();-- if (!font_config_started) {- font_config_started = true;- - IdleSource idle = new IdleSource ();- idle.set_callback (() => {- Task t = new Task (init_font_config);- MainWindow.native_window.run_non_blocking_background_thread (t);- return false;- });- idle.attach (null);- }- - add_font_folder ("/usr/share/fonts/");- add_font_folder ("/usr/local/share/fonts/");- add_font_folder (home + "/.local/share/fonts");- add_font_folder (home + "/.fonts");- add_font_folder ("C:\\Windows\\Fonts");- add_font_folder (home + "/Library/Fonts");- add_font_folder ("/Library/Fonts");- add_font_folder ("/Network/Library/Fonts");- add_font_folder ("/System/Library/Fonts");- add_font_folder ("/System Folder/Fonts");- - glyphs = new Gee.HashMap<unichar, CachePair> ();- cached = new Gee.ArrayList<CachePair> ();- - open_default_font ();- }- - ~FallbackFont () {- if (default_font != null) {- close_font (default_font);- }- }-- public void init_font_config () {- FcConfig* config;- -#if MAC- config = FcConfigCreate();- - string bundle = (!) BirdFont.get_settings_directory ().get_path ();- FcConfigSetSysRoot(config, bundle);- - string path = FcConfigFilename((!) SearchPaths.search_file(null, "fontconfig.settings").get_path ());- bool loaded = FcConfigParseAndLoad(config, path, true);- - if (!loaded) {- warning ("Fontconfig initialization failed.");- }- - FcConfigSetCurrent (config);-#else- config = FcInitLoadConfigAndFonts ();-#endif-- IdleSource idle = new IdleSource ();- - idle.set_callback (() => {- font_config = config;- printd("Fontconfig loaded\n");- return false;- });- idle.attach (null);- }-- public Font get_single_glyph_font (unichar c) {- Font f;- unichar last; - CachePair p;- - if (likely (glyphs.has_key (c))) {- p = glyphs.get (c);- - if (p.referenced < int.MAX) {- p.referenced++;- }- - return p.font;- }-- // remove glyphs from cache if it is full- if (cached.size > max_cached_fonts - 100) {- - cached.sort ((a, b) => {- CachePair pa = (CachePair) a;- CachePair pb = (CachePair) b;- return pb.referenced - pa.referenced;- });- - int j = 0;- for (int i = cached.size - 1; i > 0; i--) {- if (j > 100) {- break;- }- - j++;- - last = cached.get (i).character;- glyphs.unset (last);- cached.remove_at (i);- }- }- - f = get_single_fallback_glyph_font (c);- p = new CachePair (f, c);- - glyphs.set (c, p);- cached.add (p);- - return (Font) f;- }-- Font get_single_fallback_glyph_font (unichar c) {- string? font_file;- BirdFontFile bf_parser;- Font bf_font;- StringBuilder? glyph_data;- FontFace* font;-- bf_font = new Font ();- font_file = null;- glyph_data = null;-- // don't use fallback font in private use area- if (0xe000 <= c <= 0xf8ff) {- return bf_font;- }- - // control characters- if (c <= 0x001f || (0x007f <= c <= 0x008d)) {- return bf_font;- }- - // check if glyph is available in roboto- if (default_font != null) {- glyph_data = get_glyph_in_font ((!) default_font, c);- }- - // use fontconfig to find a fallback font- if (glyph_data == null) {- font_file = find_font (font_config, (!) c.to_string ());- if (font_file != null) {- font = open_font ((!) font_file);- glyph_data = get_glyph_in_font (font, c);- close_font (font);- }- }- - if (glyph_data != null) {- bf_parser = new BirdFontFile (bf_font);- bf_parser.load_data (((!) glyph_data).str);- }-- return bf_font; - }-- public StringBuilder? get_glyph_in_font (FontFace* font, unichar c) {- StringBuilder? glyph_data = null;- GlyphCollection gc;-- gc = new GlyphCollection (c, (!)c.to_string ()); - glyph_data = load_glyph (font, (uint) c);-- return glyph_data;- }- - void add_font_folder (string f) {- File folder = File.new_for_path (f);- FileInfo? file_info;- string fn;- string file_attributes;- try {- if (folder.query_exists ()) {- font_directories.add (folder);- - file_attributes = FileAttribute.STANDARD_NAME;- file_attributes += ",";- file_attributes += FileAttribute.STANDARD_TYPE;- var enumerator = folder.enumerate_children (file_attributes, 0);- - while ((file_info = enumerator.next_file ()) != null) {- fn = ((!) file_info).get_name ();-- if (((!)file_info).get_file_type () == FileType.DIRECTORY) {- add_font_folder ((!) get_child (folder, fn).get_path ());- }- }- }- } catch (GLib.Error e) {- warning (e.message);- }- }-- File search_font_file (string font_file) {- File d, f;- - for (int i = font_directories.size - 1; i >= 0; i--) {- d = font_directories.get (i);- f = get_child (d, font_file);- - if (f.query_exists ()) {- return f;- }- }- - warning (@"The font $font_file not found");- return File.new_for_path (font_file);- }-- public string? get_default_font_file () {- File font_file;- string? fn = null;- - if (likely (default_font_file != null)) {- return default_font_file;- }- - font_file = SearchPaths.search_file (null, default_font_file_name);- - if (font_file.query_exists ()) {- fn = (!) font_file.get_path ();- } else {- font_file = search_font_file (default_font_file_name);- - if (font_file.query_exists ()) {- fn = (!) font_file.get_path ();- } else {- fn = find_font_file (font_config, default_font_family_name);- }- }- - if (likely (fn != null)) {- default_font_file = fn;- return fn;- }- - warning(default_font_family_name + " not found");- return null;- }- - void open_default_font () {- string? fn = get_default_font_file ();- - if (fn != null) {- default_font = open_font ((!) fn);- }- }- - class CachePair : GLib.Object {- public Font font;- public unichar character;- public int referenced = 1;- - public CachePair (Font f, unichar c) {- font = f;- character = c;- }- }-}--}deleted file mode 100644@@ -1,83 +0,0 @@-/*- Copyright (C) 2014 Johan Mattsson-- This library is free software; you can redistribute it and/or modify - it under the terms of the GNU Lesser General Public License as - published by the Free Software Foundation; either version 3 of the - License, or (at your option) any later version.-- This library 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 - Lesser General Public License for more details.-*/--using Gee;--namespace BirdFont {--/** Thread specific font cache. */ -public class FontCache { - public static FallbackFont fallback_font;- - static FontCache? default_cache = null;- Gee.HashMap<string, CachedFont> fonts;- CachedFont fallback;- - public FontCache () {- if (is_null (fallback_font)) {- fallback_font = new FallbackFont ();- }- - fallback = new CachedFont (null);- fonts = new Gee.HashMap<string, CachedFont> ();- }- - public CachedFont get_font (string file_name) {- CachedFont c;- Font f;- bool ok;-- if (file_name == "") {- return fallback;- }- - if (fonts.has_key (file_name)) {- c = fonts.get (file_name);- return c;- }- - f = new Font ();- f.set_file (file_name);- ok = f.load ();- if (!ok) {- stderr.printf ("Can't load %s\n", file_name);- return new CachedFont (null);- }- - c = new CachedFont (f);- - if (file_name == "") {- warning ("No file.");- return c;- }- - fonts.set (file_name, c);- return c;- }-- public static FontCache get_default_cache () {- if (default_cache == null) {- default_cache = new FontCache ();- }- - return (!) default_cache;- }-- public CachedFont get_fallback () {- return fallback;- }--}--}deleted file mode 100644@@ -1,33 +0,0 @@-/*- Copyright (C) 2014 Johan Mattsson-- This library is free software; you can redistribute it and/or modify - it under the terms of the GNU Lesser General Public License as - published by the Free Software Foundation; either version 3 of the - License, or (at your option) any later version.-- This library 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 - Lesser General Public License for more details.-*/--using Cairo;-using Math;--namespace BirdFont {--public class LineTextArea : TextArea {-- public LineTextArea (double size) {- base (size);- - single_line = true;- min_height = size;- height = min_height;- - layout ();- }-}--}deleted file mode 100644@@ -1,584 +0,0 @@-/*- Copyright (C) 2014 2015 Johan Mattsson-- This library is free software; you can redistribute it and/or modify - it under the terms of the GNU Lesser General Public License as - published by the Free Software Foundation; either version 3 of the - License, or (at your option) any later version.-- This library 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 - Lesser General Public License for more details.-*/--using Cairo;-using Math;--namespace BirdFont {- -/** Test implementation of a birdfont rendering engine. */-public class Text : Widget {- FontCache font_cache;- public CachedFont cached_font;- - Surface? cache = null;- - public string text;- - private bool use_cache = true;- - GlyphSequence glyph_sequence {- get {- if (gs == null) {- gs = generate_glyphs ();- }- - return (!) gs;- }- }- - Gee.ArrayList<string> glyph_names;- GlyphSequence? gs = null;- - public delegate void Iterator (Glyph glyph, double kerning, bool last);- public double font_size;- double sidebearing_extent = 0;-- public double r = 0;- public double g = 0;- public double b = 0;- public double a = 1;- double truncated_width = -1;- - double margin_left = 0;- - public Text (string text = "", double size = 17, double margin_bottom = 0) {- this.margin_bottom = margin_bottom;- font_cache = FontCache.get_default_cache ();- cached_font = font_cache.get_fallback ();- - set_text (text);- set_font_size (size);- }- - public void set_use_cache (bool cache) {- use_cache = cache;- }- - public string get_text () {- return text;- }- - /** Set font for this text area.- * @param font_absolute path to the font file or a file name for one of the font files in search paths.- * @return true if the font was found- */- public bool load_font (string font_file) {- File path;- File f;- FontCache fc;- - f = File.new_for_path (font_file);- path = (f.query_exists ()) ? f : SearchPaths.find_file (null, font_file);- - fc = FontCache.get_default_cache ();- cached_font = fc.get_font ((!) path.get_path ());- gs = generate_glyphs ();- - return cached_font.font != null;- }- - public void set_font_size (double height_in_pixels) {- font_size = height_in_pixels;- sidebearing_extent = 0;- - if (gs == null) { // ensure height is loaded for the font- gs = generate_glyphs ();- }- }-- public void set_font_cache (FontCache font_cache) {- this.font_cache = font_cache;- }- - public void set_text (string text) { - this.text = text;- gs = null;- sidebearing_extent = 0;- cache = null;- }-- private GlyphSequence generate_glyphs () {- int index;- unichar c;- string name;- Glyph? g;- GlyphSequence gs;- - gs = new GlyphSequence ();- - glyph_names = new Gee.ArrayList<string> ();- index = 0;- while (text.get_next_char (ref index, out c)) {- name = (!) c.to_string ();- g = cached_font.get_glyph_by_name (name);- - gs.glyph.add (g);- glyph_names.add (name);- }- - return gs;- }-- public void iterate (Iterator iter) {- Glyph glyph;- double w, kern;- int wi;- Glyph? prev;- Glyph? g;- GlyphSequence word_with_ligatures;- GlyphRange? gr_left, gr_right;- GlyphSequence word;- KerningClasses kc;- Font empty = Font.empty;- - glyph = new Glyph.no_lines ("", '\0');-- w = 0;- prev = null;- kern = 0;- - word = glyph_sequence;- wi = 0;- - if (cached_font.font != null) {- word_with_ligatures = word.process_ligatures ((!) cached_font.font);- } else {- word_with_ligatures = word.process_ligatures (new Font ());- }- - gr_left = null;- gr_right = null;- - if (cached_font.font != null) {- kc = ((!) cached_font.font).get_kerning_classes ();- } else {- kc = new KerningClasses (empty);- } -- if (word_with_ligatures.glyph.size > 0) {- g = word_with_ligatures.glyph.get (0);- if (g != null) {- margin_left = ((!) g).get_left_side_bearing ();- - if (margin_left < 0) {- margin_left = -margin_left;- } else {- margin_left = 0;- }- }- }- - for (int i = 0; i < word_with_ligatures.glyph.size; i++) {- g = word_with_ligatures.glyph.get (i);- - if (g == null || prev == null || wi == 0) {- kern = 0;- } else {- return_if_fail (wi < word_with_ligatures.ranges.size);- return_if_fail (wi - 1 >= 0);- - gr_left = word_with_ligatures.ranges.get (wi - 1);- gr_right = word_with_ligatures.ranges.get (wi);-- kern = kc.get_kerning_for_pair (((!) prev).get_name (), ((!) g).get_name (), gr_left, gr_right);- }- - // process glyph- if (g == null && (0 <= i < glyph_names.size)) {- g = cached_font.get_glyph_by_name (glyph_names.get (i));- }- - glyph = (g == null) ? new Glyph ("") : (!) g;- iter (glyph, kern, i + 1 == word_with_ligatures.glyph.size); - prev = g;- wi++;- }- }-- // FIXME: some fonts doesn't have on curve extrema- public double get_extent () {- double x = 0;- - iterate ((glyph, kerning, last) => {- double x1, y1, x2, y2;- double lsb;- - lsb = glyph.left_limit;- - if (!last) {- x += (glyph.get_width () + kerning) * get_scale (glyph);- } else {- glyph.boundaries (out x1, out y1, out x2, out y2);- x += (x2 - lsb) * get_scale (glyph);- }- });- - return x;- }-- public double get_sidebearing_extent () {- double x ;- - if (likely (sidebearing_extent > 0)) {- return sidebearing_extent;- }- - x = 0;- - iterate ((glyph, kerning, last) => {- double lsb;- lsb = glyph.left_limit;- x += (glyph.get_width () + kerning) * get_scale (glyph);- });- - sidebearing_extent = x;- return x;- }-- public override double get_height () {- return font_size;- }- - public double get_acender () {- double max_height = 0;- - iterate ((glyph, kerning, last) => {- double x1, y1, x2, y2;- double h;- glyph.boundaries (out x1, out y1, out x2, out y2);- h = Math.fmax (y1, y2) - Math.fmin (y1, y2);- h *= get_scale (glyph) - glyph.baseline * get_scale (glyph);- if (h > max_height) {- max_height = h;- }- });- - return max_height;- } -- public override double get_width () {- double x = 0;- bool first = true;- - iterate ((glyph, kerning, last) => {- double x1, y1, x2, y2;- double lsb;- - lsb = glyph.left_limit;- - if (first) {- glyph.boundaries (out x1, out y1, out x2, out y2);- x += (glyph.get_width () + kerning - Math.fmin (x1, x2)) * get_scale (glyph);- first = false;- } else if (!last) {- x += (glyph.get_width () + kerning) * get_scale (glyph);- } else {- glyph.boundaries (out x1, out y1, out x2, out y2);- x += (x2 - lsb) * get_scale (glyph);- }- });- - return x;- }-- public double get_decender () {- double decender_max = get_max_decender ();- return decender_max > 0 ? decender_max : 0; - } -- private double get_max_decender () {- double decender = 0;- double decender_max = 0;- - iterate ((glyph, kerning, last) => {- double x1, y1, x2, y2;- double y;- glyph.boundaries (out x1, out y1, out x2, out y2);- y = Math.fmin (y1, y2);- decender = (glyph.baseline - y) * get_scale (glyph);- if (decender > decender_max) {- decender_max = decender;- }- });- - return decender_max; - }- - public override void draw (Context cr) {- double descender = cached_font.bottom_limit + cached_font.base_line;- double y = widget_y + get_height () + get_font_scale () * descender; // FIXME:- draw_at_baseline (cr, widget_x, y);- }- - public void draw_at_top (Context cr, double px, double py, string cacheid = "") {- double s = get_font_scale ();- double y = py + s * (cached_font.top_limit - cached_font.base_line);- draw_at_baseline (cr, px, y, cacheid);- }- - public void set_source_rgba (double r, double g, double b, double a) {- if (this.r != r || - this.g != g || - this.b != b || - this.a != a) {- - this.r = r;- this.g = g;- this.b = b;- this.a = a;- cache = null;- }- }- - public string get_cache_id (int offset_x, int offset_y) {- string key;- int64 c;- - c = (((int64) (r * 255)) << 24)- | (((int64) (g * 255)) << 16)- | (((int64) (b * 255)) << 8)- | (((int64) (a * 255)) << 0);- - // FIXME: use binary key- key = @"$font_size $c $offset_x $offset_y";- - return key;- }- - public void draw_at_baseline (Context cr, double px, double py, string cacheid = "") {- if (cache == null) {- cache = draw_on_cache_surface (cacheid);- }- - double screen_scale = Screen.get_scale ();- double font_scale = get_font_scale ();- double cache_y = py - font_scale * (cached_font.top_limit - cached_font.base_line);-- cr.save();- cr.scale (1 / screen_scale, 1 / screen_scale);- double scaled_x = (px - margin_left) * screen_scale;- double scaled_y = cache_y * screen_scale;- cr.set_source_surface ((!) cache, (int) rint (scaled_x), (int) rint (scaled_y));- cr.paint ();- cr.restore();- }- - Surface draw_on_cache_surface (string cacheid) {- double y;- double ratio;- double cc_y;- Context cr;- Surface cache_surface;- double screen_scale = Screen.get_scale();- double h = font_size * screen_scale + 1;-- ratio = get_font_scale ();- cc_y = (cached_font.top_limit - cached_font.base_line) * ratio;-- // double x = margin_left * ratio;- double x = 0;- double w = get_sidebearing_extent () * screen_scale + x + margin_left + 1;- - cache_surface = Screen.create_background_surface ((int) w, (int) h);- cr = new Context (cache_surface);- cr.scale (screen_scale, screen_scale);-- y = cc_y;-- if (unlikely (cached_font.base_line != 0)) {- warning ("Base line not zero.");- }- - iterate ((glyph, kerning, last) => {- double end;- - x += kerning * ratio;- end = x + glyph.get_width () * ratio;- - // truncation- if (truncated_width > 0 && end > truncated_width) {- return;- }-- if (use_cache) {- draw_chached (cr, glyph, kerning, last, x, y, cc_y, - ratio, cacheid);- } else {- draw_without_cache (cr, glyph, kerning, last, x, y, cc_y, ratio);- }- - x = end;- });- - return cache_surface;- }-- void draw_without_cache (Context cr, Glyph glyph, double kerning, bool last, - double x, double y, double cc_y, double ratio) {- - double lsb;- - cr.save ();- cr.set_source_rgba (r, g, b, a);- cr.new_path ();-- lsb = glyph.left_limit;-- foreach (Path path in glyph.get_visible_paths ()) {- draw_path (cr, glyph, path, lsb, x, y, ratio);- }-- cr.fill ();- cr.restore ();- - }- - void draw_chached (Context cr, Glyph glyph, double kerning, bool last, - double x, double y, double cc_y, double ratio,- string cacheid = "") {- - double lsb;- Surface cache;- Surface cached_glyph;- Context cc;- string cache_id;- double glyph_margin_left = glyph.get_left_side_bearing ();- - if (glyph_margin_left < 0) {- glyph_margin_left = -glyph_margin_left;- } else {- glyph_margin_left = 0;- }- - double xp = (x - glyph_margin_left * ratio) * Screen.get_scale ();- double yp = (y - cc_y) * Screen.get_scale ();- int offset_x, offset_y;-- offset_x = (int) (10 * (xp - (int) xp));- offset_y = (int) (10 * (yp - (int) yp));- - cache_id = (cacheid == "") ? get_cache_id (offset_x, offset_y) : cacheid;- - if (!glyph.has_cache (cache_id)) {- int w = (int) ((2 * glyph_margin_left * ratio + glyph.get_width ()) * ratio) + 2;- int h = (int) font_size + 2;- cache = Screen.create_background_surface (w, h);- cc = new Context (cache);- - cc.scale(Screen.get_scale (), Screen.get_scale ());- - lsb = glyph.left_limit - glyph_margin_left;-- cc.save ();- cc.set_source_rgba (r, g, b, a);- cc.new_path ();-- foreach (Path path in glyph.get_visible_paths ()) {- draw_path (cc, glyph, path, lsb, glyph_margin_left * ratio + offset_x / 10.0, cc_y + offset_y / 10.0, ratio);- }- - cc.fill ();- cc.restore ();-- if (use_cache) {- glyph.set_cache (cache_id, cache);- }- - cached_glyph = cache;- } else {- cached_glyph = glyph.get_cache (cache_id);- }-- cr.save ();- cr.set_antialias (Cairo.Antialias.NONE);- cr.scale(1 / Screen.get_scale (), 1 / Screen.get_scale ());- cr.set_source_surface (cached_glyph,- (int) xp,- (int) yp);- cr.paint ();- cr.restore ();- }- - void draw_path (Context cr, Glyph glyph, Path path,- double lsb, double x, double y, double scale) {- - EditPoint e, prev;- double xa, ya, xb, yb, xc, yc, xd, yd;- double by;- double s = get_scale (glyph);- - if (path.points.size > 0) {- if (unlikely (path.is_open ())) {- warning (@"Path is open in $(glyph.get_name ()).");- }- - //path.add_hidden_double_points (); // FIXME: this distorts shapes- - prev = path.points.get (path.points.size - 1);- xa = (prev.x - lsb) * s + x;- ya = y - prev.y * s;- cr.move_to (xa, ya);- - by = (y - cached_font.base_line * s);- for (int i = 0; i < path.points.size; i++) {- e = path.points.get (i).copy ();- - PenTool.convert_point_segment_type (prev, e, PointType.CUBIC);- - xb = (prev.get_right_handle ().x - lsb) * s + x;- yb = by - prev.get_right_handle ().y * s;-- xc = (e.get_left_handle ().x - lsb) * s + x;- yc = by - e.get_left_handle ().y * s;- - xd = (e.x - lsb) * s + x;- yd = by - e.y * s;-- cr.curve_to (xb, yb, xc, yc, xd, yd);- cr.line_to (xd, yd);- - prev = e;- }- }- }-- public double get_baseline_to_bottom (Glyph g) {- return get_scale (g) * (-g.baseline - g.bottom_limit);- }-- public double get_scale (Glyph g) {- double s = g.top_limit - g.bottom_limit;- - if (s == 0) {- s = cached_font.top_limit - cached_font.bottom_limit;- }- - return font_size / s;- }-- public double get_font_scale () {- return font_size / (cached_font.top_limit - cached_font.bottom_limit);- }-- public double get_baseline_to_bottom_for_font () {- return get_font_scale () * (-cached_font.base_line - cached_font.bottom_limit);- }- - public void truncate (double max_width) {- truncated_width = max_width;- }-}--}deleted file mode 100644@@ -1,1665 +0,0 @@-/*- Copyright (C) 2014 Johan Mattsson-- This library is free software; you can redistribute it and/or modify - it under the terms of the GNU Lesser General Public License as - published by the Free Software Foundation; either version 3 of the - License, or (at your option) any later version.-- This library 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 - Lesser General Public License for more details.-*/--using Cairo;-using Math;--namespace BirdFont {--public class TextArea : Widget {- - public double min_width = 500;- public double min_height = 100;- public double font_size;- public double padding = 3.3;- public bool single_line = false;- public Color text_color = Color.black ();- - public bool draw_carret {- get { return carret_is_visible; }- set { - carret_is_visible = value; - if (!value) {- update_selection = false;- selection_end = carret.copy ();- }- }- }- public bool carret_is_visible = false;- public bool draw_border = true;-- public double width;- public double height;- - Carret carret = new Carret ();- Carret selection_end = new Carret ();- bool update_selection = false;- public bool show_selection = false;- - public signal void scroll (double pixels);- public signal void text_changed (string text);- public signal void enter (string text);- - Gee.ArrayList<Paragraph> paragraphs = new Gee.ArrayList<Paragraph> ();- private static const int DONE = -2; - - int last_paragraph = 0;- string text;- int text_length;- - Gee.ArrayList<TextUndoItem> undo_items = new Gee.ArrayList<TextUndoItem> ();- Gee.ArrayList<TextUndoItem> redo_items = new Gee.ArrayList<TextUndoItem> ();- - bool store_undo_state_at_next_event = false;- - public bool editable;- - public TextArea (double font_size = 20, Color? c = null) {- this.font_size = font_size;- width = min_width;- height = min_height;- editable = true;- - if (c != null) {- text_color = (!) c;- }- }-- public override void focus (bool focus) {- draw_carret = focus;- }-- public override double get_height () {- return height + 2 * padding;- }-- public override double get_width () {- return width + 2 * padding;- }-- public void set_font_size (double z) {- font_size = z;- }- - bool generate_paragraphs () {- Paragraph paragraph;-- int next_paragraph = -1;- - if (is_null (text)) {- warning ("No text");- return false;- }- - if (last_paragraph == DONE) {- return false;- }- - next_paragraph = text.index_of ("\n", last_paragraph);- - if (next_paragraph == -1) {- paragraph = new Paragraph (text.substring (last_paragraph), font_size, paragraphs.size, text_color);- paragraphs.add (paragraph);- last_paragraph = DONE;- } else {- next_paragraph += "\n".length;- paragraph = new Paragraph (text.substring (last_paragraph, next_paragraph - last_paragraph), font_size, paragraphs.size, text_color);- paragraphs.add (paragraph);- last_paragraph = next_paragraph;- }- - return last_paragraph != DONE;- }- - void generate_all_paragraphs () {- while (generate_paragraphs ()) {- }- }- - public override void key_press (uint keyval) {- unichar c;- TextUndoItem ui;- - if (!editable) {- return;- }- - c = (unichar) keyval;- - switch (c) {- case ' ':- store_undo_edit_state ();- add_character (keyval);- break;- case 'a':- if (KeyBindings.has_ctrl () || KeyBindings.has_logo ()) {- select_all ();- } else {- add_character (keyval);- }- break;- case 'c':- if (KeyBindings.has_ctrl () || KeyBindings.has_logo ()) {- ClipTool.copy_text (this);- } else {- add_character (keyval);- }- break;- case 'v':- if (KeyBindings.has_ctrl () || KeyBindings.has_logo ()) {- ClipTool.paste_text (this);- store_undo_state_at_next_event = true;- } else {- add_character (keyval);- }- break;- case 'y':- if (KeyBindings.has_ctrl () || KeyBindings.has_logo ()) {- redo ();- } else {- add_character (keyval);- }- break;- case 'z':- if (KeyBindings.has_ctrl () || KeyBindings.has_logo ()) {- undo ();- } else {- add_character (keyval);- }- break;- case Key.RIGHT:- check_selection ();- move_carret_next ();- break;- case Key.LEFT:- check_selection ();- move_carret_previous ();- break;- case Key.DOWN:- check_selection ();- move_carret_next_row ();- break;- case Key.UP:- check_selection ();- move_carret_previous_row ();- break;- case Key.END:- check_selection ();- move_carret_to_end_of_line ();- break;- case Key.HOME:- check_selection ();- move_carret_to_beginning_of_line ();- break;- case Key.BACK_SPACE:- if (has_selection ()) {- ui = delete_selected_text ();- undo_items.add (ui);- redo_items.clear ();- store_undo_state_at_next_event = true;- } else {- ui = remove_last_character ();- undo_items.add (ui);- redo_items.clear ();- store_undo_state_at_next_event = true;- }- text_changed (get_text ());- break;- case Key.ENTER:- store_undo_edit_state ();- insert_text ("\n");- - if (single_line) {- enter (get_text ());- }- break;- case Key.DEL:- if (has_selection ()) {- ui = delete_selected_text ();- undo_items.add (ui);- redo_items.clear ();- store_undo_state_at_next_event = true;- } else {- ui = remove_next_character ();- undo_items.add (ui);- redo_items.clear ();- store_undo_state_at_next_event = true;- }- text_changed (get_text ());- break; - default:- if (!KeyBindings.has_ctrl () && !KeyBindings.has_logo ()) {- add_character (keyval);- } - break;- }- - GlyphCanvas.redraw ();- }- - void check_selection () {- if (!has_selection () && KeyBindings.has_shift ()) {- show_selection = true;- selection_end = carret.copy ();- }- - if (!KeyBindings.has_shift ()) {- show_selection = false;- }- }- - private void add_character (uint keyval) {- unichar c = (unichar) keyval;- string s;-- if (!is_modifier_key (keyval) - && !KeyBindings.has_ctrl ()- && !KeyBindings.has_alt ()) {- - s = (!) c.to_string (); - - if (s.validate ()) { - if (store_undo_state_at_next_event) {- store_undo_edit_state ();- store_undo_state_at_next_event = false;- }- - insert_text (s);- }- }- }-- Paragraph get_current_paragraph () {- Paragraph p;- - if (unlikely (!(0 <= carret.paragraph < paragraphs.size))) {- warning (@"No paragraph, index: $(carret.paragraph), size: $(paragraphs.size)");- p = new Paragraph ("", 0, 0, text_color);- paragraphs.add (p);- return p;- }- - p = paragraphs.get (carret.paragraph);- return p;- }-- public void set_text (string t) {- int tl;- - if (single_line) {- text = t.replace ("\n", "").replace ("\r", "");- } else {- text = t;- }- - tl = t.length; - text_length += tl;- - paragraphs.clear ();- generate_paragraphs ();- - return_if_fail (paragraphs.size != 0);- - carret.paragraph = paragraphs.size - 1;- carret.character_index = paragraphs.get (paragraphs.size - 1).text.length;- selection_end = carret.copy ();- show_selection = false;- - text_changed (get_text ());- }- - Carret get_selection_start () {- if (carret.paragraph == selection_end.paragraph) {- return carret.character_index < selection_end.character_index ? carret : selection_end;- }- - return carret.paragraph < selection_end.paragraph ? carret : selection_end; - }-- Carret get_selection_stop () {- if (carret.paragraph == selection_end.paragraph) {- return carret.character_index > selection_end.character_index ? carret : selection_end;- }- - return carret.paragraph > selection_end.paragraph ? carret : selection_end; - }- - public string get_selected_text () {- Carret selection_start, selection_stop;- int i;- Paragraph pg;- StringBuilder sb;- - sb = new StringBuilder ();- - if (!has_selection ()) {- return "".dup ();- }- - selection_start = get_selection_start ();- selection_stop = get_selection_stop ();- - if (selection_start.paragraph == selection_stop.paragraph) {- pg = paragraphs.get (selection_start.paragraph);- return pg.text.substring (selection_start.character_index, selection_stop.character_index - selection_start.character_index);- }- - pg = paragraphs.get (selection_start.paragraph);- sb.append (pg.text.substring (selection_start.character_index));- - for (i = selection_start.paragraph + 1; i < selection_stop.paragraph; i++) {- return_val_if_fail (0 <= i < paragraphs.size, "".dup ());- pg = paragraphs.get (i);- sb.append (pg.text);- }-- pg = paragraphs.get (selection_stop.paragraph);- sb.append (pg.text.substring (0, selection_stop.character_index));- - return sb.str;- }- - public void select_all () {- while (last_paragraph != DONE) {- generate_paragraphs ();- }- - if (paragraphs.size > 0) {- carret.paragraph = 0;- carret.character_index = 0;- selection_end.paragraph = paragraphs.size - 1;- selection_end.character_index = paragraphs.get (paragraphs.size - 1).text_length;- show_selection = true;- }- }- - public TextUndoItem delete_selected_text () {- Carret selection_start, selection_stop;- int i;- Paragraph pg, pge;- string e, s, n;- bool same;- TextUndoItem ui;- - ui = new TextUndoItem (carret);- - e = "";- s = "";- n = "";- - if (!has_selection ()) {- warning ("No selected text.");- return ui;- }- - selection_start = get_selection_start ();- selection_stop = get_selection_stop ();- - same = selection_start.paragraph == selection_stop.paragraph;- - if (!same) {- return_val_if_fail (0 <= selection_start.paragraph < paragraphs.size, ui);- pg = paragraphs.get (selection_start.paragraph);- s = pg.text.substring (0, selection_start.character_index);-- return_val_if_fail (0 <= selection_stop.paragraph < paragraphs.size, ui);- pge = paragraphs.get (selection_stop.paragraph);- e = pge.text.substring (selection_stop.character_index);- - if (!s.has_suffix ("\n")) {- ui.deleted.add (pge.copy ());- ui.edited.add (pg.copy ());- - pg.set_text (s + e);- pge.set_text ("");- } else {- ui.edited.add (pg.copy ());- ui.edited.add (pge.copy ());- - pg.set_text (s);- pge.set_text (e);- }- } else {- return_val_if_fail (0 <= selection_start.paragraph < paragraphs.size, ui);- - pg = paragraphs.get (selection_start.paragraph);- n = pg.text.substring (0, selection_start.character_index);- n += pg.text.substring (selection_stop.character_index);- - if (n == "") {- ui.deleted.add (pg.copy ());- paragraphs.remove_at (selection_start.paragraph);- } else {- ui.edited.add (pg.copy ());- }- - pg.set_text (n);- }- - if (e == "" && !same) {- paragraphs.remove_at (selection_stop.paragraph);- }- - for (i = selection_stop.paragraph - 1; i > selection_start.paragraph; i--) {- return_val_if_fail (0 <= i < paragraphs.size, ui);- ui.deleted.add (paragraphs.get (i));- paragraphs.remove_at (i);- }- - if (s == "" && !same) {- return_val_if_fail (0 <= selection_start.paragraph < paragraphs.size, ui);- paragraphs.remove_at (selection_start.paragraph);- }- - carret = selection_start.copy ();- selection_end = carret.copy ();- - show_selection = false;- update_paragraph_index ();- layout ();- - return ui;- }- - void update_paragraph_index () {- int i = 0;- foreach (Paragraph p in paragraphs) {- p.index = i;- i++;- }- }- - public TextUndoItem remove_last_character () {- TextUndoItem ui;- move_carret_previous ();- ui = remove_next_character ();- return ui;- }- - public TextUndoItem remove_next_character () {- Paragraph paragraph;- Paragraph next_paragraph;- int index;- unichar c;- string np;- TextUndoItem ui;- - ui = new TextUndoItem (carret);- - return_val_if_fail (0 <= carret.paragraph < paragraphs.size, ui);- paragraph = paragraphs.get (carret.paragraph);- - index = carret.character_index;- - paragraph.text.get_next_char (ref index, out c);- - if (index >= paragraph.text_length) {- np = paragraph.text.substring (0, carret.character_index);- - if (carret.paragraph + 1 < paragraphs.size) {- next_paragraph = paragraphs.get (carret.paragraph + 1);- paragraphs.remove_at (carret.paragraph + 1);- - np = np + next_paragraph.text;- - ui.deleted.add (next_paragraph);- }- - paragraph.set_text (np);- ui.edited.add (paragraph);- } else {- np = paragraph.text.substring (0, carret.character_index) + paragraph.text.substring (index);- paragraph.set_text (np);-- if (np == "") {- return_val_if_fail (carret.paragraph > 0, ui);- carret.paragraph--;- paragraph = paragraphs.get (carret.paragraph);- carret.character_index = paragraph.text_length;- - ui.deleted.add (paragraphs.get (carret.paragraph + 1));- - paragraphs.remove_at (carret.paragraph + 1);- } else {- ui.edited.add (paragraph);- }- }- - update_paragraph_index ();- layout ();-- return ui;- }- - public void move_carret_next () {- unichar c;- - move_carret_one_character ();- - if (KeyBindings.has_ctrl ()) {- while (true) {- c = move_carret_one_character ();- - if (c == '\0' || c == ' ') {- break;- }- }- }- }- - unichar move_carret_one_character () {- Paragraph paragraph;- int index;- unichar c;- - return_val_if_fail (0 <= carret.paragraph < paragraphs.size, '\0');- paragraph = paragraphs.get (carret.paragraph);- - index = carret.character_index;- - paragraph.text.get_next_char (ref index, out c);- - if (index >= paragraph.text_length && carret.paragraph + 1 < paragraphs.size) {- carret.paragraph++;- carret.character_index = 0;- c = ' ';- } else {- carret.character_index = index;- }- - return c; - }-- public void move_carret_previous () {- unichar c;- - move_carret_back_one_character ();- - if (KeyBindings.has_ctrl ()) {- while (true) {- c = move_carret_back_one_character ();- - if (c == '\0' || c == ' ') {- break;- }- }- }- }- - unichar move_carret_back_one_character () {- Paragraph paragraph;- int index, last_index;- unichar c;- - return_val_if_fail (0 <= carret.paragraph < paragraphs.size, '\0');- paragraph = paragraphs.get (carret.paragraph);- - index = 0;- last_index = -1;- - while (paragraph.text.get_next_char (ref index, out c) && index < carret.character_index) {- last_index = index;- }- - if (last_index <= 0 && carret.paragraph > 0) {- carret.paragraph--;- - return_val_if_fail (0 <= carret.paragraph < paragraphs.size, '\0');- paragraph = paragraphs.get (carret.paragraph);- carret.character_index = paragraph.text_length;- - if (paragraph.text.has_suffix ("\n")) {- carret.character_index -= "\n".length;- }- - c = ' ';- } else if (last_index > 0) {- carret.character_index = last_index;- } else {- carret.character_index = 0;- c = ' ';- }- - return_val_if_fail (0 <= carret.paragraph < paragraphs.size, '\0');- - return c;- }- - public void move_carret_next_row () {- double nr = font_size;- - if (carret.desired_y + 2 * font_size >= allocation.height) {- scroll (2 * font_size);- nr = -font_size;- }- - if (carret.desired_y + nr < widget_y + height - padding) {- carret = get_carret_at (carret.desired_x - widget_x - padding, carret.desired_y + nr);- }- }-- public void move_carret_to_end_of_line () {- carret = get_carret_at (widget_x + padding + width, carret.desired_y, false); - }-- public void move_carret_to_beginning_of_line () {- carret = get_carret_at (widget_x, carret.desired_y, false); - }- - public void move_carret_previous_row () {- double nr = -font_size;- - if (carret.desired_y - 2 * font_size < 0) {- scroll (-2 * font_size);- nr = font_size;- }- - if (carret.desired_y + nr > widget_y + padding) {- carret = get_carret_at (carret.desired_x, carret.desired_y + nr);- }- }- - public bool has_selection () {- return show_selection && selection_is_visible ();- }- - private bool selection_is_visible () {- return carret.paragraph != selection_end.paragraph || carret.character_index != selection_end.character_index;- }- - public void insert_text (string t) {- string s;- Paragraph paragraph;- TextUndoItem ui;- Gee.ArrayList<string> pgs;- bool u = false;- - pgs = new Gee.ArrayList<string> ();- - if (single_line) {- s = t.replace ("\n", "").replace ("\r", "");- pgs.add (s);- } else {- if (t.last_index_of ("\n") > 0) {- string[] parts = t.split ("\n");- int i;- for (i = 0; i < parts.length -1; i++) {- pgs.add (parts[i]);- pgs.add ("\n");- }-- pgs.add (parts[parts.length - 1]);- - if (t.has_suffix ("\n")) {- pgs.add ("\n");- }- } else {- s = t;- pgs.add (s);- }- }- - if (has_selection () && show_selection) {- ui = delete_selected_text ();- u = true;- - if (paragraphs.size == 0) {- paragraphs.add (new Paragraph ("", font_size, 0, text_color));- }- } else {- ui = new TextUndoItem (carret);- }- - return_if_fail (0 <= carret.paragraph < paragraphs.size);- paragraph = paragraphs.get (carret.paragraph);- - if (pgs.size > 0) {- if (!u) {- ui.edited.add (paragraph.copy ());- }- - string first = pgs.get (0);- - string end; - string nt = paragraph.text.substring (0, carret.character_index);- - nt += first;- end = paragraph.text.substring (carret.character_index);- - paragraph.set_text (nt);- - int paragraph_index = carret.paragraph;- Paragraph next_paragraph = paragraph;- for (int i = 1; i < pgs.size; i++) {- paragraph_index++;- string next = pgs.get (i);- next_paragraph = new Paragraph (next, font_size, paragraph_index, text_color);- paragraphs.insert (paragraph_index, next_paragraph);- ui.added.add (next_paragraph);- u = true;- }-- carret.paragraph = paragraph_index;- carret.character_index = next_paragraph.text.length;-- next_paragraph.set_text (next_paragraph.text + end);- }- - if (u) {- undo_items.add (ui);- redo_items.clear ();- }-- update_paragraph_index ();- layout ();- - text_changed (get_text ());- show_selection = false;- }- - public string get_text () {- StringBuilder sb = new StringBuilder ();- - generate_all_paragraphs ();- - foreach (Paragraph p in paragraphs) {- sb.append (p.text);- }-- return sb.str;- }- - Carret get_carret_at (double click_x, double click_y, - bool check_boundaries = true) {- - int i = 0;- double tx, ty;- double p;- string w;- int ch_index;- double min_d = double.MAX;- Carret c = new Carret ();- double dt;-- c.paragraph = -1;- c.desired_x = click_x;- c.desired_y = click_y;- - foreach (Paragraph paragraph in paragraphs) {- if (!check_boundaries || paragraph.text_is_on_screen (allocation, widget_y)) {- ch_index = 0;-- if (paragraph.start_y + widget_y - font_size <= click_y <= paragraph.end_y + widget_y + font_size) { - foreach (Text next_word in paragraph.words) {- double tt_click = click_y - widget_y - padding + font_size;-- w = next_word.text;- - if (next_word.widget_y <= tt_click <= next_word.widget_y + font_size) {- - p = next_word.get_sidebearing_extent ();- - if ((next_word.widget_y <= tt_click <= next_word.widget_y + font_size)- && (next_word.widget_x + widget_x <= click_x <= next_word.widget_x + widget_x + padding + next_word.get_sidebearing_extent ())) {- - tx = widget_x + next_word.widget_x + padding;- ty = widget_y + next_word.widget_y + padding;- - next_word.iterate ((glyph, kerning, last) => {- double cw;- int ci;- double d;- string gc = (!) glyph.get_unichar ().to_string ();- - d = Math.fabs (click_x - tx);- - if (d <= min_d) {- min_d = d;- c.character_index = ch_index;- c.paragraph = i;- }- - cw = (glyph.get_width ()) * next_word.get_font_scale () + kerning;- ci = gc.length;- - tx += cw;- ch_index += ci;- });-- dt = Math.fabs (click_x - (tx + widget_x + padding));- if (dt < min_d) {- min_d = dt;- c.character_index = ch_index;- c.paragraph = i;- }- } else {- dt = Math.fabs (click_x - (next_word.widget_x + widget_x + padding + next_word.get_sidebearing_extent ()));- - if (dt < min_d) {- min_d = dt;- c.character_index = ch_index + w.length;- - if (w.has_suffix ("\n")) {- c.character_index -= "\n".length;- }- - c.paragraph = i;- }- - ch_index += w.length;- }- } else {- ch_index += w.length;- }- }- }- }- i++;- }- - if (unlikely (c.paragraph < 0)) {- c.paragraph = paragraphs.size > 0 ? paragraphs.size - 1 : 0;- c.character_index = paragraphs.size > 0 ? paragraphs.get (c.paragraph).text.length : 0;- }- - store_undo_state_at_next_event = true;- - return c;- }- - /** @return offset to click in text. */- public override void layout () {- double p;- double tx, ty;- string w;- double xmax = 0;- int i = 0;- double dd;- - tx = 0;- ty = font_size;- - if (allocation.width <= 0 || allocation.height <= 0) {- warning ("Parent widget allocation is not set.");- }- - for (i = paragraphs.size - 1; i >= 0 && paragraphs.size > 1; i--) {- if (unlikely (paragraphs.get (i).is_empty ())) {- warning ("Empty paragraph.");- paragraphs.remove_at (i);- update_paragraph_index ();- }- }- - i = 0;- foreach (Paragraph paragraph in paragraphs) {- if (paragraph.need_layout - || (paragraph.text_area_width != width- && paragraph.text_is_on_screen (allocation, widget_y))) {- - paragraph.start_y = ty;- paragraph.start_x = tx;- - paragraph.cached_surface = null;- - foreach (Text next_word in paragraph.words) {- next_word.set_font_size (font_size);- - w = next_word.text;- p = next_word.get_sidebearing_extent ();-- if (unlikely (p == 0)) {- warning (@"Zero width word: $(w)");- }-- if (w == "") {- break;- }-- if (w == "\n") {- next_word.widget_x = tx;- next_word.widget_y = ty;- - tx = 0;- ty += next_word.font_size;- } else {- if (!single_line) {- if (tx + p + 2 * padding > width || w == "\n") {- tx = 0;- ty += next_word.font_size;- }- }-- if (tx + p > xmax) {- xmax = tx + p;- }- - next_word.widget_x = tx;- next_word.widget_y = ty;- - if (w != "\n") {- tx += p;- }- }- }-- if (tx > xmax) {- xmax = tx;- }- - paragraph.text_area_width = width;- paragraph.width = xmax;- paragraph.end_x = tx;- paragraph.end_y = ty;- paragraph.need_layout = false;- }- - if (xmax > width) {- break;- }- - tx = paragraph.end_x;- ty = paragraph.end_y;- i++;- }- - if (xmax > width) {- this.width = xmax + 2 * padding;- layout ();- return;- }- - this.height = fmax (min_height, ty + 2 * padding);-- if (last_paragraph != DONE) {- this.height = (text_length / (double) last_paragraph) * ty + 2 * padding; // estimate height- }- - if (ty + widget_y < allocation.height && last_paragraph != DONE) {- generate_paragraphs ();- layout ();- return;- }-- ty = font_size;- tx = 0;- - foreach (Paragraph paragraph in paragraphs) {- dd = ty - paragraph.start_y;- - if (dd != 0) {- paragraph.start_y += dd;- paragraph.end_y += dd;- foreach (Text word in paragraph.words) {- word.widget_y += dd;- }- }- - ty = paragraph.end_y;- }- }- - public override void button_press (uint button, double x, double y) {- if (is_over (x, y)) {- carret = get_carret_at (x, y);- selection_end = carret.copy ();- update_selection = true;- }- }-- public override void button_release (uint button, double x, double y) {- update_selection = false;- show_selection = selection_is_visible ();- }- - public override bool motion (double x, double y) {- if (update_selection) {- selection_end = get_carret_at (x, y);- show_selection = selection_is_visible ();- }- - return update_selection;- }- - public override void draw (Context cr) {- Text word;- double tx, ty;- string w;- double scale;- double width;- double x = widget_x;- double y = widget_y;- Carret selection_start, selection_stop;- double carret_x;- double carret_y;- - layout ();- - if (draw_border) {- // background- cr.save ();- cr.set_line_width (1);- Theme.color (cr, "Text Area Background");- draw_rounded_rectangle (cr, x, y, this.width, this.height - padding, padding);- cr.fill ();- cr.restore ();- - // border- cr.save ();- cr.set_line_width (1);- Theme.color (cr, "Foreground 1");- draw_rounded_rectangle (cr, x, y, this.width, this.height - padding, padding);- cr.stroke ();- cr.restore ();- }- - cr.save ();-- word = new Text ();- - width = this.width - padding;- x += padding;- scale = word.get_font_scale ();- y += font_size;- - // draw selection background- if (has_selection ()) {- tx = 0;- ty = 0;- - selection_start = get_selection_start ();- selection_stop = get_selection_stop ();- - cr.save ();- Theme.color (cr, "Highlighted 1");-- for (int i = selection_start.paragraph; i <= selection_stop.paragraph; i++) {- return_if_fail (0 <= i < paragraphs.size);- Paragraph pg = paragraphs.get (i);-- if (pg.text_is_on_screen (allocation, widget_y)) {- int char_index = 0;- - foreach (Text next_word in pg.words) {- double cw = next_word.get_sidebearing_extent ();- bool paint_background = false;- bool partial_start = false;- bool partial_stop = false;- int wl;- - w = next_word.text;- wl = w.length;- scale = next_word.get_font_scale ();- - if (selection_start.paragraph == selection_stop.paragraph) {- partial_start = true;- partial_stop = true;- } else if (selection_start.paragraph < i < selection_stop.paragraph) {- paint_background = true;- } else if (selection_start.paragraph == i) {- paint_background = true;- partial_start = true;- } else if (selection_stop.paragraph == i) {- paint_background = char_index + wl < selection_stop.character_index;- partial_stop = !paint_background;- }- - if (paint_background && !(partial_start || partial_stop)) { - double selection_y = widget_y + next_word.widget_y + scale * -next_word.cached_font.bottom_limit - font_size;- cr.rectangle (widget_x + padding + next_word.widget_x - 1, selection_y, cw + 1, font_size);- cr.fill ();- }- - if (partial_start || partial_stop) {- int index = char_index;- double bx = widget_x + padding + next_word.widget_x + (partial_start ? 0 : 1);- - next_word.iterate ((glyph, kerning, last) => {- double cwi;- int ci;- bool draw = (index >= selection_start.character_index && partial_start && !partial_stop)- || (index < selection_stop.character_index && !partial_start && partial_stop)- || (selection_start.character_index <= index < selection_stop.character_index && partial_start && partial_stop);- - cwi = (glyph.get_width ()) * next_word.get_font_scale () + kerning;-- if (draw) {- double selection_y = widget_y + next_word.widget_y + scale * -next_word.cached_font.bottom_limit - font_size;- cr.rectangle (bx - 1, selection_y, cwi + 1, font_size);- cr.fill ();- }- - bx += cwi;- ci = ((!) glyph.get_unichar ().to_string ()).length;- index += ci;- });- } - - char_index += w.length; - }- }- }- - cr.restore ();- }-- tx = 0;- ty = 0; - - int first_visible = 0;- int last_visible;- int paragraphs_size = paragraphs.size;- while (first_visible < paragraphs_size) {- if (paragraphs.get (first_visible).text_is_on_screen (allocation, widget_y)) {- break;- }- first_visible++;- }- - last_visible = first_visible;- while (last_visible < paragraphs_size) {- if (!paragraphs.get (last_visible).text_is_on_screen (allocation, widget_y)) {- last_visible++;- break;- }- last_visible++;- } - - if (paragraphs_size == 0) {- if (carret_is_visible) {- draw_carret_at (cr, widget_x + padding, widget_y + font_size + padding);- }- - return;- }- - Context cc; // cached context- Paragraph paragraph;- paragraph = paragraphs.get (0);- - tx = paragraph.start_x;- ty = paragraph.start_y;-- for (int i = first_visible; i < last_visible; i++) {- paragraph = paragraphs.get (i);- - tx = paragraph.start_x;- ty = paragraph.start_y;-- if (paragraph.cached_surface == null) { - paragraph.cached_surface = Screen.create_background_surface ((int) width + 2, paragraph.get_height () + (int) font_size + 2);- cc = new Context ((!) paragraph.cached_surface);- cc.scale (Screen.get_scale(), Screen.get_scale());- - foreach (Text next_word in paragraph.words) {- if (next_word.text != "\n") {- next_word.draw_at_top (cc, next_word.widget_x, next_word.widget_y - ty);- }- }- }- - if (likely (paragraph.cached_surface != null)) {- // FIXME: subpixel offset in text area- Screen.paint_background_surface(cr, - (!) paragraph.cached_surface,- (int) (x + tx),- (int) (widget_y + paragraph.start_y - font_size + padding));- } else {- warning ("No paragraph image.");- }- }-- if (carret_is_visible) {- get_carret_position (carret, out carret_x, out carret_y);- - if (carret_y < 0) {- draw_carret_at (cr, widget_x + padding, widget_y + font_size + padding);- } else {- draw_carret_at (cr, carret_x, carret_y);- }- }- - if (has_selection ()) {- get_carret_position (selection_end, out carret_x, out carret_y);- - if (carret_y < 0) {- draw_carret_at (cr, widget_x + padding, widget_y + font_size + padding);- } else {- draw_carret_at (cr, carret_x, carret_y);- }- }- }- - void get_carret_position (Carret carret, out double carret_x, out double carret_y) {- Paragraph paragraph;- double tx;- double ty;- int ch_index;- int wl;- double pos_x, pos_y;-- ch_index = 0;-- carret_x = -1;- carret_y = -1;-- return_if_fail (0 <= carret.paragraph < paragraphs.size);- paragraph = paragraphs.get (carret.paragraph);-- pos_x = -1;- pos_y = -1;- - foreach (Text next_word in paragraph.words) {- string w = next_word.text;- wl = w.length;-- if (carret.character_index == ch_index) {- pos_x = next_word.widget_x + widget_x + padding;- pos_y = widget_y + next_word.widget_y + next_word.get_baseline_to_bottom_for_font (); - } else if (carret.character_index >= ch_index + wl) {- pos_x = next_word.widget_x + next_word.get_sidebearing_extent () + widget_x + padding;- pos_y = widget_y + next_word.widget_y + next_word.get_baseline_to_bottom_for_font ();- - if (next_word.text.has_suffix ("\n")) {- pos_x = widget_x + padding;- pos_y += next_word.font_size;- }- } else if (ch_index < carret.character_index <= ch_index + wl) {- tx = widget_x + next_word.widget_x;- ty = widget_y + next_word.widget_y + next_word.get_baseline_to_bottom_for_font ();- - if (carret.character_index <= ch_index) {- pos_x = widget_x + padding;- pos_y = ty;- }-- next_word.iterate ((glyph, kerning, last) => {- double cw;- int ci;-- cw = (glyph.get_width ()) * next_word.get_font_scale () + kerning;- ci = ((!) glyph.get_unichar ().to_string ()).length;- - if (ch_index < carret.character_index <= ch_index + ci) {- pos_x = tx + cw + padding;- pos_y = ty;- - if (glyph.get_unichar () == '\n') {- pos_x = widget_x + padding;- pos_y += next_word.font_size;- }- }- - tx += cw;- ch_index += ci;- });- }- - ch_index += wl;- }- - carret_x = pos_x;- carret_y = pos_y;- }- - void draw_carret_at (Context cr, double x, double y) {- cr.save ();- cr.set_source_rgba (0, 0, 0, 0.5);- cr.set_line_width (1);- cr.move_to (x, y);- cr.line_to (x, y - font_size);- cr.stroke ();- cr.restore (); - }-- public void store_undo_edit_state () {- TextUndoItem ui = new TextUndoItem (carret);- ui.edited.add (get_current_paragraph ().copy ());- undo_items.add (ui);- redo_items.clear ();- }- - public void redo () {- TextUndoItem i;- TextUndoItem undo_item;- - if (redo_items.size > 0) {- i = redo_items.get (redo_items.size - 1); - - undo_item = new TextUndoItem (i.carret);- - i.deleted.sort ((a, b) => {- Paragraph pa = (Paragraph) a;- Paragraph pb = (Paragraph) b;- return pb.index - pa.index;- });-- i.added.sort ((a, b) => {- Paragraph pa = (Paragraph) a;- Paragraph pb = (Paragraph) b;- return pa.index - pb.index;- });-- foreach (Paragraph p in i.deleted) {- if (unlikely (!(0 <= p.index < paragraphs.size))) {- warning ("Paragraph not found.");- } else {- undo_item.deleted.add (p.copy ());- paragraphs.remove_at (p.index);- }- }- - foreach (Paragraph p in i.added) {- if (p.index == paragraphs.size) {- paragraphs.add (p.copy ());- } else {- if (unlikely (!(0 <= p.index < paragraphs.size))) {- warning (@"Index: $(p.index) out of bounds, size: $(paragraphs.size)");- } else {- undo_item.added.add (paragraphs.get (p.index).copy ());- paragraphs.insert (p.index, p.copy ());- }- }- }-- foreach (Paragraph p in i.edited) {- if (unlikely (!(0 <= p.index < paragraphs.size))) {- warning (@"Index: $(p.index ) out of bounds, size: $(paragraphs.size)");- return;- }- - undo_item.edited.add (paragraphs.get (p.index).copy ());- paragraphs.set (p.index, p.copy ());- } - - redo_items.remove_at (redo_items.size - 1);- undo_items.add (undo_item);- - carret = i.carret.copy ();- layout ();- }- }- - public void undo () {- TextUndoItem i;- TextUndoItem redo_item;- - if (undo_items.size > 0) {- i = undo_items.get (undo_items.size - 1); - redo_item = new TextUndoItem (i.carret);- - i.deleted.sort ((a, b) => {- Paragraph pa = (Paragraph) a;- Paragraph pb = (Paragraph) b;- return pa.index - pb.index;- });-- i.added.sort ((a, b) => {- Paragraph pa = (Paragraph) a;- Paragraph pb = (Paragraph) b;- return pb.index - pa.index;- });- - foreach (Paragraph p in i.added) {- if (unlikely (!(0 <= p.index < paragraphs.size))) {- warning ("Paragraph not found.");- } else {- redo_item.added.add (paragraphs.get (p.index).copy ());- paragraphs.remove_at (p.index);- }- }- - foreach (Paragraph p in i.deleted) {- if (p.index == paragraphs.size) {- paragraphs.add (p.copy ());- } else {- if (unlikely (!(0 <= p.index < paragraphs.size))) {- warning (@"Index: $(p.index) out of bounds, size: $(paragraphs.size)");- } else {- redo_item.deleted.add (p.copy ());- paragraphs.insert (p.index, p.copy ());- }- }- }-- foreach (Paragraph p in i.edited) {- if (unlikely (!(0 <= p.index < paragraphs.size))) {- warning (@"Index: $(p.index ) out of bounds, size: $(paragraphs.size)");- return;- }- - redo_item.edited.add (paragraphs.get (p.index).copy ());- paragraphs.set (p.index, p.copy ());- } - - undo_items.remove_at (undo_items.size - 1);- redo_items.add (redo_item);- - carret = i.carret.copy ();- layout ();- }- }- - public void set_editable (bool editable) {- this.editable = editable;- }- - public class TextUndoItem : GLib.Object {- public Carret carret;- public Gee.ArrayList<Paragraph> added = new Gee.ArrayList<Paragraph> ();- public Gee.ArrayList<Paragraph> edited = new Gee.ArrayList<Paragraph> ();- public Gee.ArrayList<Paragraph> deleted = new Gee.ArrayList<Paragraph> ();- - public TextUndoItem (Carret c) {- carret = c.copy ();- }- }- - public class Paragraph : GLib.Object {- public double end_x = -10000;- public double end_y = -10000;- - public double start_x = -10000;- public double start_y = -10000;- - public double width = -10000;- public double text_area_width = -10000;- - public string text;- - public Gee.ArrayList<Text> words {- get {- if (words_in_paragraph.size == 0) {- generate_words ();- }- - return words_in_paragraph;- }- }- - private Gee.ArrayList<Text> words_in_paragraph = new Gee.ArrayList<Text> ();- public int text_length;- public bool need_layout = true;- public Surface? cached_surface = null;- double font_size;- public int index;- Color text_color;- - public Paragraph (string text, double font_size, int index, Color c) {- this.index = index;- this.font_size = font_size;- text_color = c;- set_text (text);- }-- public Paragraph copy () {- Paragraph p = new Paragraph (text.dup (), font_size, index, text_color);- p.need_layout = true;- return p;- }-- public bool is_empty () {- return text == "";- }-- public void set_text (string t) {- this.text = t;- text_length = t.length;- need_layout = true;- words.clear ();- cached_surface = null;- }-- public int get_height () {- return (int) (end_y - start_y) + 1;- }- - public int get_width () {- return (int) width + 1;- }- - public bool text_is_on_screen (WidgetAllocation alloc, double widget_y) {- bool v = (0 <= start_y + widget_y <= alloc.height)- || (0 <= end_y + widget_y <= alloc.height)- || (start_y + widget_y <= 0 && alloc.height <= end_y + widget_y);- return v;- }-- private void generate_words () {- string w;- int p = 0;- bool carret_at_word_end = false;- Text word;- int carret = 0;- int iter_pos = 0;-- return_if_fail (words_in_paragraph.size == 0);-- while (p < text_length) {- w = get_next_word (out carret_at_word_end, ref iter_pos, carret);- - if (w == "") {- break;- }- - word = new Text (w, font_size);- - word.r = text_color.r;- word.g = text_color.g;- word.b = text_color.b;- word.a = text_color.a;- - words_in_paragraph.add (word);- }- }-- string get_next_word (out bool carret_at_end_of_word, ref int iter_pos, int carret) {- int i;- int ni;- int pi;- string n;- int nl;- - carret_at_end_of_word = false;- - if (iter_pos >= text_length) {- carret_at_end_of_word = true;- return "".dup ();- }- - if (text.get_char (iter_pos) == '\n') {- iter_pos += "\n".length;- carret_at_end_of_word = (iter_pos == carret);- return "\n".dup ();- }- - i = text.index_of (" ", iter_pos);- pi = i + " ".length;- - ni = text.index_of ("\t", iter_pos);- if (ni != -1 && ni < pi || i == -1) {- i = ni;- pi = i + "\t".length;- }- - ni = text.index_of ("\n", iter_pos);- if (ni != -1 && ni < pi || i == -1) {- i = ni;- pi = i;- }- - if (iter_pos + iter_pos - pi > text_length || i == -1) {- n = text.substring (iter_pos);- } else {- n = text.substring (iter_pos, pi - iter_pos);- }- - nl = n.length;- if (iter_pos < carret < iter_pos + nl) {- n = text.substring (iter_pos, carret - iter_pos);- nl = n.length;- carret_at_end_of_word = true;- }- - iter_pos += nl;- - if (iter_pos == carret) {- carret_at_end_of_word = true;- }- - return n;- }- }- - public class Carret : GLib.Object {- - public int paragraph = 0;- - public int character_index {- get { - return ci;- }- - set { - ci = value;- }- }- - private int ci = 0;- - public double desired_x = 0;- public double desired_y = 0;- - public Carret () {- }- - public void print () {- stdout.printf (@"paragraph: $paragraph, character_index: $character_index\n");- }- - public Carret copy () {- Carret c = new Carret ();- - c.paragraph = paragraph;- c.character_index = character_index;- - c.desired_x = desired_x;- c.desired_y = desired_y;- - return c;- }- }-}--}deleted file mode 100644@@ -1,140 +0,0 @@-/*- Copyright (C) 2015 Johan Mattsson-- This library is free software; you can redistribute it and/or modify - it under the terms of the GNU Lesser General Public License as - published by the Free Software Foundation; either version 3 of the - License, or (at your option) any later version.-- This library 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 - Lesser General Public License for more details.-*/--#include <stdio.h>-#include <glib.h>-#include <fontconfig/fontconfig.h>--gchar* find_font_with_property (FcConfig* fontconfig, const gchar* characters, const gchar* property) {- FcPattern* pattern;- FcCharSet* character_set;- FcObjectSet* font_properties;- FcFontSet* fonts;- FcPattern* font;- FcChar8* path;- gchar* result;- gchar* remaining_characters;- gunichar character;- - if (fontconfig == NULL) {- g_warning("Font config not loaded.");- return NULL;- }- - result = NULL;- pattern = FcPatternCreate ();- - character_set = FcCharSetCreate ();- - remaining_characters = (gchar*) characters;- while (TRUE) {- character = g_utf8_get_char (remaining_characters);- - if (character == '\0') {- break;- }- - FcCharSetAddChar(character_set, character);- - remaining_characters = g_utf8_next_char (remaining_characters);- }-- FcPatternAddCharSet (pattern, FC_CHARSET, character_set);- FcCharSetDestroy (character_set);- FcPatternAddInteger (pattern, FC_SLANT, FC_SLANT_ROMAN);- - FcPatternAddBool(pattern, FC_SCALABLE, FcTrue);- font_properties = FcObjectSetBuild (property, NULL); - fonts = FcFontList (fontconfig, pattern, font_properties);- - if (fonts && fonts->nfont > 0) {- font = fonts->fonts[0];- if (FcPatternGetString(font, property, 0, &path) == FcResultMatch) {- result = g_strdup ((gchar*) path);- }- }-- if (fonts) {- FcFontSetDestroy(fonts);- }-- if (pattern) {- FcPatternDestroy(pattern);- }-- return result;-}--/** Find a fallback font for a set of characters.- * @return A path to the font file.- */-gchar* find_font (FcConfig* fontconfig, const gchar* characters) {- return find_font_with_property (fontconfig, characters, FC_FILE);-}--/** Find a fallback font for a set of characters.- * @return Family name of the font.- */-gchar* find_font_family (FcConfig* fontconfig, const gchar* characters) {- return find_font_with_property (fontconfig, characters, FC_FAMILY);-}--/** Find a font file from its family name.- * @param font_config fontconfig instance- * @param font_name name of the font- * @return full path to the font file- */-gchar* find_font_file (FcConfig* font_config, const gchar* font_name) {- const FcChar8* name;- FcPattern* search_pattern;- FcPattern* font;- FcChar8* file;- gchar* path;- FcObjectSet* font_properties;- FcFontSet* fonts;- int i;-- if (font_config == NULL) {- g_warning("Font config not loaded.");- return NULL;- }- - path = NULL;- name = font_name;-- search_pattern = FcPatternCreate ();- FcPatternAddString (search_pattern, FC_FAMILY, name);- FcPatternAddBool (search_pattern, FC_SCALABLE, FcTrue);- FcPatternAddInteger (search_pattern, FC_WEIGHT, FC_WEIGHT_MEDIUM);- FcPatternAddInteger (search_pattern, FC_SLANT, FC_SLANT_ROMAN);- - font_properties = FcObjectSetBuild (FC_FILE, NULL);- fonts = FcFontList (font_config, search_pattern, font_properties);- - if (fonts->nfont > 0) {- for (i = 0; i < fonts->nfont; i++) {- font = fonts->fonts[i];- - if (FcPatternGetString(font, FC_FILE, 0, &file) == FcResultMatch) {- path = g_strdup ((gchar*) file);- break;- }- }- FcPatternDestroy (font);- }-- FcPatternDestroy (search_pattern);- - return path;-}new file mode 100644@@ -0,0 +1,99 @@+/*+ Copyright (C) 2015 Johan Mattsson++ This library is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation; either version 3 of the + License, or (at your option) any later version.++ This library 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 + Lesser General Public License for more details.+*/++using Gee;++namespace BirdFont {+ +public class CachedFont : GLib.Object {+ public Font? font;++ public double top_limit {+ get { return _top_limit; }+ set { _top_limit = value; }+ }++ public double bottom_limit {+ get { return _bottom_limit; }+ set { _bottom_limit = value; }+ }+ + public double base_line = 0;+ double _top_limit = 92.77; // FIXME: load before first glyph+ double _bottom_limit = -24.4;+ + static FallbackFont fallback_font {+ get {+ if (_fallback_font == null) {+ _fallback_font = new FallbackFont ();+ }+ + return (!) _fallback_font;+ }+ }+ static FallbackFont? _fallback_font = null;++ public CachedFont (Font? font) {+ Glyph? g;+ Glyph glyph;+ + this.font = font;++ g = get_glyph_by_name ("a");+ if (g != null) {+ glyph = (!) g;+ base_line = glyph.baseline;+ top_limit = glyph.top_limit;+ bottom_limit = glyph.bottom_limit;+ } else {+ warning("No default chararacter found in font.");+ }+ }+ + public Glyph? get_glyph_by_name (string name) {+ Glyph? g = null;+ Font f;+ Glyph glyph;+ + if (font != null) {+ f = (!) font;+ g = f.get_glyph_by_name (name);+ + if (g != null) {+ glyph = (!) g;+ glyph.top_limit = f.top_limit;+ glyph.baseline = f.base_line;+ glyph.bottom_limit = f.bottom_limit;+ }+ }+ + if (g == null && name.char_count () == 1) {+ f = fallback_font.get_single_glyph_font (name.get_char (0));+ g = f.get_glyph_by_name (name);+ + if (g == null) {+ return null;+ }+ + glyph = (!) g;+ glyph.top_limit = f.top_limit;+ glyph.baseline = f.base_line;+ glyph.bottom_limit = f.bottom_limit; + }+ + return g;+ }+}++}new file mode 100644@@ -0,0 +1,50 @@+/*+ Copyright (C) 2014 Johan Mattsson++ This library is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation; either version 3 of the + License, or (at your option) any later version.++ This library 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 + Lesser General Public License for more details.+*/++namespace BirdFont {++/** Interface for creating drawing callbacks. */+[Compact]+[CCode (ref_function = "bird_font_drawing_ref", unref_function = "bird_font_drawing_unref")]+public class Drawing {+ + public int iterator_refcount = 1;+ + public Drawing () {+ }++ public void new_path (double x, double y) {+ }++ public void curve_to (double xb, double yb, double xc, double yc, double xd, double yd) {+ }++ public void close_path (double x, double y) {+ }++ public unowned Drawing @ref () {+ iterator_refcount++;+ return this;+ }+ + public void unref () {+ if (--iterator_refcount == 0) {+ this.free ();+ }+ }+ + private extern void free ();+}++}new file mode 100644@@ -0,0 +1,337 @@+/*+ Copyright (C) 2015 Johan Mattsson++ This library is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation; either version 3 of the + License, or (at your option) any later version.++ This library 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 + Lesser General Public License for more details.+*/++using Gee;++[SimpleType]+[CCode (has_type_id = false)]+public extern struct FcConfig {+}++[CCode (cname = "FcInitLoadConfigAndFonts")]+public extern FcConfig* FcInitLoadConfigAndFonts ();++[CCode (cname = "FcConfigAppFontAddDir")]+public extern string* FcConfigAppFontAddDir (FcConfig* config, string path);++[CCode (cname = "FcConfigSetSysRoot")]+public extern void FcConfigSetSysRoot (FcConfig* config, string path);++[CCode (cname = "FcConfigParseAndLoad")]+public extern bool FcConfigParseAndLoad (FcConfig* config, string path, bool complain);++[CCode (cname = "FcConfigSetCurrent")]+public extern void FcConfigSetCurrent (FcConfig* config);++[CCode (cname = "FcConfigCreate")]+public extern FcConfig* FcConfigCreate ();++[CCode (cname = "FcConfigFilename")]+public extern string FcConfigFilename (string path);++[CCode (cname = "find_font")]+public extern string? find_font (FcConfig* font_config, string characters);++[CCode (cname = "find_font_family")]+public extern string? find_font_family (FcConfig* font_config, string characters);++[CCode (cname = "find_font_file")]+public extern string? find_font_file (FcConfig* font_config, string font_name);++namespace BirdFont {++// TODO: use font config+public class FallbackFont : GLib.Object {+ Gee.ArrayList<File> font_directories;+ + FontFace* default_font = null;+ public static FcConfig* font_config = null;+ static bool font_config_started = false;+ + string default_font_file_name = "Roboto-Regular.ttf";+ string default_font_family_name = "Roboto";++ Gee.HashMap<unichar, CachePair> glyphs;+ Gee.ArrayList<CachePair> cached;++ public int max_cached_fonts = 300;+ + string? default_font_file = null;+ + public FallbackFont () {+ string home = Environment.get_home_dir ();+ font_directories = new Gee.ArrayList<File> ();++ if (!font_config_started) {+ font_config_started = true;+ + IdleSource idle = new IdleSource ();+ idle.set_callback (() => {+ Task t = new Task (init_font_config);+ MainWindow.native_window.run_non_blocking_background_thread (t);+ return false;+ });+ idle.attach (null);+ }+ + add_font_folder ("/usr/share/fonts/");+ add_font_folder ("/usr/local/share/fonts/");+ add_font_folder (home + "/.local/share/fonts");+ add_font_folder (home + "/.fonts");+ add_font_folder ("C:\\Windows\\Fonts");+ add_font_folder (home + "/Library/Fonts");+ add_font_folder ("/Library/Fonts");+ add_font_folder ("/Network/Library/Fonts");+ add_font_folder ("/System/Library/Fonts");+ add_font_folder ("/System Folder/Fonts");+ + glyphs = new Gee.HashMap<unichar, CachePair> ();+ cached = new Gee.ArrayList<CachePair> ();+ + open_default_font ();+ }+ + ~FallbackFont () {+ if (default_font != null) {+ close_font (default_font);+ }+ }++ public void init_font_config () {+ FcConfig* config;+ +#if MAC+ config = FcConfigCreate();+ + string bundle = (!) BirdFont.get_settings_directory ().get_path ();+ FcConfigSetSysRoot(config, bundle);+ + string path = FcConfigFilename((!) SearchPaths.search_file(null, "fontconfig.settings").get_path ());+ bool loaded = FcConfigParseAndLoad(config, path, true);+ + if (!loaded) {+ warning ("Fontconfig initialization failed.");+ }+ + FcConfigSetCurrent (config);+#else+ config = FcInitLoadConfigAndFonts ();+#endif++ IdleSource idle = new IdleSource ();+ + idle.set_callback (() => {+ font_config = config;+ printd("Fontconfig loaded\n");+ return false;+ });+ idle.attach (null);+ }++ public Font get_single_glyph_font (unichar c) {+ Font f;+ unichar last; + CachePair p;+ + if (likely (glyphs.has_key (c))) {+ p = glyphs.get (c);+ + if (p.referenced < int.MAX) {+ p.referenced++;+ }+ + return p.font;+ }++ // remove glyphs from cache if it is full+ if (cached.size > max_cached_fonts - 100) {+ + cached.sort ((a, b) => {+ CachePair pa = (CachePair) a;+ CachePair pb = (CachePair) b;+ return pb.referenced - pa.referenced;+ });+ + int j = 0;+ for (int i = cached.size - 1; i > 0; i--) {+ if (j > 100) {+ break;+ }+ + j++;+ + last = cached.get (i).character;+ glyphs.unset (last);+ cached.remove_at (i);+ }+ }+ + f = get_single_fallback_glyph_font (c);+ p = new CachePair (f, c);+ + glyphs.set (c, p);+ cached.add (p);+ + return (Font) f;+ }++ Font get_single_fallback_glyph_font (unichar c) {+ string? font_file;+ BirdFontFile bf_parser;+ Font bf_font;+ StringBuilder? glyph_data;+ FontFace* font;++ bf_font = new Font ();+ font_file = null;+ glyph_data = null;++ // don't use fallback font in private use area+ if (0xe000 <= c <= 0xf8ff) {+ return bf_font;+ }+ + // control characters+ if (c <= 0x001f || (0x007f <= c <= 0x008d)) {+ return bf_font;+ }+ + // check if glyph is available in roboto+ if (default_font != null) {+ glyph_data = get_glyph_in_font ((!) default_font, c);+ }+ + // use fontconfig to find a fallback font+ if (glyph_data == null) {+ font_file = find_font (font_config, (!) c.to_string ());+ if (font_file != null) {+ font = open_font ((!) font_file);+ glyph_data = get_glyph_in_font (font, c);+ close_font (font);+ }+ }+ + if (glyph_data != null) {+ bf_parser = new BirdFontFile (bf_font);+ bf_parser.load_data (((!) glyph_data).str);+ }++ return bf_font; + }++ public StringBuilder? get_glyph_in_font (FontFace* font, unichar c) {+ StringBuilder? glyph_data = null;+ GlyphCollection gc;++ gc = new GlyphCollection (c, (!)c.to_string ()); + glyph_data = load_glyph (font, (uint) c);++ return glyph_data;+ }+ + void add_font_folder (string f) {+ File folder = File.new_for_path (f);+ FileInfo? file_info;+ string fn;+ string file_attributes;+ try {+ if (folder.query_exists ()) {+ font_directories.add (folder);+ + file_attributes = FileAttribute.STANDARD_NAME;+ file_attributes += ",";+ file_attributes += FileAttribute.STANDARD_TYPE;+ var enumerator = folder.enumerate_children (file_attributes, 0);+ + while ((file_info = enumerator.next_file ()) != null) {+ fn = ((!) file_info).get_name ();++ if (((!)file_info).get_file_type () == FileType.DIRECTORY) {+ add_font_folder ((!) get_child (folder, fn).get_path ());+ }+ }+ }+ } catch (GLib.Error e) {+ warning (e.message);+ }+ }++ File search_font_file (string font_file) {+ File d, f;+ + for (int i = font_directories.size - 1; i >= 0; i--) {+ d = font_directories.get (i);+ f = get_child (d, font_file);+ + if (f.query_exists ()) {+ return f;+ }+ }+ + warning (@"The font $font_file not found");+ return File.new_for_path (font_file);+ }++ public string? get_default_font_file () {+ File font_file;+ string? fn = null;+ + if (likely (default_font_file != null)) {+ return default_font_file;+ }+ + font_file = SearchPaths.search_file (null, default_font_file_name);+ + if (font_file.query_exists ()) {+ fn = (!) font_file.get_path ();+ } else {+ font_file = search_font_file (default_font_file_name);+ + if (font_file.query_exists ()) {+ fn = (!) font_file.get_path ();+ } else {+ fn = find_font_file (font_config, default_font_family_name);+ }+ }+ + if (likely (fn != null)) {+ default_font_file = fn;+ return fn;+ }+ + warning(default_font_family_name + " not found");+ return null;+ }+ + void open_default_font () {+ string? fn = get_default_font_file ();+ + if (fn != null) {+ default_font = open_font ((!) fn);+ }+ }+ + class CachePair : GLib.Object {+ public Font font;+ public unichar character;+ public int referenced = 1;+ + public CachePair (Font f, unichar c) {+ font = f;+ character = c;+ }+ }+}++}new file mode 100644@@ -0,0 +1,83 @@+/*+ Copyright (C) 2014 Johan Mattsson++ This library is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation; either version 3 of the + License, or (at your option) any later version.++ This library 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 + Lesser General Public License for more details.+*/++using Gee;++namespace BirdFont {++/** Thread specific font cache. */ +public class FontCache { + public static FallbackFont fallback_font;+ + static FontCache? default_cache = null;+ Gee.HashMap<string, CachedFont> fonts;+ CachedFont fallback;+ + public FontCache () {+ if (is_null (fallback_font)) {+ fallback_font = new FallbackFont ();+ }+ + fallback = new CachedFont (null);+ fonts = new Gee.HashMap<string, CachedFont> ();+ }+ + public CachedFont get_font (string file_name) {+ CachedFont c;+ Font f;+ bool ok;++ if (file_name == "") {+ return fallback;+ }+ + if (fonts.has_key (file_name)) {+ c = fonts.get (file_name);+ return c;+ }+ + f = new Font ();+ f.set_file (file_name);+ ok = f.load ();+ if (!ok) {+ stderr.printf ("Can't load %s\n", file_name);+ return new CachedFont (null);+ }+ + c = new CachedFont (f);+ + if (file_name == "") {+ warning ("No file.");+ return c;+ }+ + fonts.set (file_name, c);+ return c;+ }++ public static FontCache get_default_cache () {+ if (default_cache == null) {+ default_cache = new FontCache ();+ }+ + return (!) default_cache;+ }++ public CachedFont get_fallback () {+ return fallback;+ }++}++}new file mode 100644@@ -0,0 +1,33 @@+/*+ Copyright (C) 2014 Johan Mattsson++ This library is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation; either version 3 of the + License, or (at your option) any later version.++ This library 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 + Lesser General Public License for more details.+*/++using Cairo;+using Math;++namespace BirdFont {++public class LineTextArea : TextArea {++ public LineTextArea (double size) {+ base (size);+ + single_line = true;+ min_height = size;+ height = min_height;+ + layout ();+ }+}++}new file mode 100644@@ -0,0 +1,584 @@+/*+ Copyright (C) 2014 2015 Johan Mattsson++ This library is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation; either version 3 of the + License, or (at your option) any later version.++ This library 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 + Lesser General Public License for more details.+*/++using Cairo;+using Math;++namespace BirdFont {+ +/** Test implementation of a birdfont rendering engine. */+public class Text : Widget {+ FontCache font_cache;+ public CachedFont cached_font;+ + Surface? cache = null;+ + public string text;+ + private bool use_cache = true;+ + GlyphSequence glyph_sequence {+ get {+ if (gs == null) {+ gs = generate_glyphs ();+ }+ + return (!) gs;+ }+ }+ + Gee.ArrayList<string> glyph_names;+ GlyphSequence? gs = null;+ + public delegate void Iterator (Glyph glyph, double kerning, bool last);+ public double font_size;+ double sidebearing_extent = 0;++ public double r = 0;+ public double g = 0;+ public double b = 0;+ public double a = 1;+ double truncated_width = -1;+ + double margin_left = 0;+ + public Text (string text = "", double size = 17, double margin_bottom = 0) {+ this.margin_bottom = margin_bottom;+ font_cache = FontCache.get_default_cache ();+ cached_font = font_cache.get_fallback ();+ + set_text (text);+ set_font_size (size);+ }+ + public void set_use_cache (bool cache) {+ use_cache = cache;+ }+ + public string get_text () {+ return text;+ }+ + /** Set font for this text area.+ * @param font_absolute path to the font file or a file name for one of the font files in search paths.+ * @return true if the font was found+ */+ public bool load_font (string font_file) {+ File path;+ File f;+ FontCache fc;+ + f = File.new_for_path (font_file);+ path = (f.query_exists ()) ? f : SearchPaths.find_file (null, font_file);+ + fc = FontCache.get_default_cache ();+ cached_font = fc.get_font ((!) path.get_path ());+ gs = generate_glyphs ();+ + return cached_font.font != null;+ }+ + public void set_font_size (double height_in_pixels) {+ font_size = height_in_pixels;+ sidebearing_extent = 0;+ + if (gs == null) { // ensure height is loaded for the font+ gs = generate_glyphs ();+ }+ }++ public void set_font_cache (FontCache font_cache) {+ this.font_cache = font_cache;+ }+ + public void set_text (string text) { + this.text = text;+ gs = null;+ sidebearing_extent = 0;+ cache = null;+ }++ private GlyphSequence generate_glyphs () {+ int index;+ unichar c;+ string name;+ Glyph? g;+ GlyphSequence gs;+ + gs = new GlyphSequence ();+ + glyph_names = new Gee.ArrayList<string> ();+ index = 0;+ while (text.get_next_char (ref index, out c)) {+ name = (!) c.to_string ();+ g = cached_font.get_glyph_by_name (name);+ + gs.glyph.add (g);+ glyph_names.add (name);+ }+ + return gs;+ }++ public void iterate (Iterator iter) {+ Glyph glyph;+ double w, kern;+ int wi;+ Glyph? prev;+ Glyph? g;+ GlyphSequence word_with_ligatures;+ GlyphRange? gr_left, gr_right;+ GlyphSequence word;+ KerningClasses kc;+ Font empty = Font.empty;+ + glyph = new Glyph.no_lines ("", '\0');++ w = 0;+ prev = null;+ kern = 0;+ + word = glyph_sequence;+ wi = 0;+ + if (cached_font.font != null) {+ word_with_ligatures = word.process_ligatures ((!) cached_font.font);+ } else {+ word_with_ligatures = word.process_ligatures (new Font ());+ }+ + gr_left = null;+ gr_right = null;+ + if (cached_font.font != null) {+ kc = ((!) cached_font.font).get_kerning_classes ();+ } else {+ kc = new KerningClasses (empty);+ } ++ if (word_with_ligatures.glyph.size > 0) {+ g = word_with_ligatures.glyph.get (0);+ if (g != null) {+ margin_left = ((!) g).get_left_side_bearing ();+ + if (margin_left < 0) {+ margin_left = -margin_left;+ } else {+ margin_left = 0;+ }+ }+ }+ + for (int i = 0; i < word_with_ligatures.glyph.size; i++) {+ g = word_with_ligatures.glyph.get (i);+ + if (g == null || prev == null || wi == 0) {+ kern = 0;+ } else {+ return_if_fail (wi < word_with_ligatures.ranges.size);+ return_if_fail (wi - 1 >= 0);+ + gr_left = word_with_ligatures.ranges.get (wi - 1);+ gr_right = word_with_ligatures.ranges.get (wi);++ kern = kc.get_kerning_for_pair (((!) prev).get_name (), ((!) g).get_name (), gr_left, gr_right);+ }+ + // process glyph+ if (g == null && (0 <= i < glyph_names.size)) {+ g = cached_font.get_glyph_by_name (glyph_names.get (i));+ }+ + glyph = (g == null) ? new Glyph ("") : (!) g;+ iter (glyph, kern, i + 1 == word_with_ligatures.glyph.size); + prev = g;+ wi++;+ }+ }++ // FIXME: some fonts doesn't have on curve extrema+ public double get_extent () {+ double x = 0;+ + iterate ((glyph, kerning, last) => {+ double x1, y1, x2, y2;+ double lsb;+ + lsb = glyph.left_limit;+ + if (!last) {+ x += (glyph.get_width () + kerning) * get_scale (glyph);+ } else {+ glyph.boundaries (out x1, out y1, out x2, out y2);+ x += (x2 - lsb) * get_scale (glyph);+ }+ });+ + return x;+ }++ public double get_sidebearing_extent () {+ double x ;+ + if (likely (sidebearing_extent > 0)) {+ return sidebearing_extent;+ }+ + x = 0;+ + iterate ((glyph, kerning, last) => {+ double lsb;+ lsb = glyph.left_limit;+ x += (glyph.get_width () + kerning) * get_scale (glyph);+ });+ + sidebearing_extent = x;+ return x;+ }++ public override double get_height () {+ return font_size;+ }+ + public double get_acender () {+ double max_height = 0;+ + iterate ((glyph, kerning, last) => {+ double x1, y1, x2, y2;+ double h;+ glyph.boundaries (out x1, out y1, out x2, out y2);+ h = Math.fmax (y1, y2) - Math.fmin (y1, y2);+ h *= get_scale (glyph) - glyph.baseline * get_scale (glyph);+ if (h > max_height) {+ max_height = h;+ }+ });+ + return max_height;+ } ++ public override double get_width () {+ double x = 0;+ bool first = true;+ + iterate ((glyph, kerning, last) => {+ double x1, y1, x2, y2;+ double lsb;+ + lsb = glyph.left_limit;+ + if (first) {+ glyph.boundaries (out x1, out y1, out x2, out y2);+ x += (glyph.get_width () + kerning - Math.fmin (x1, x2)) * get_scale (glyph);+ first = false;+ } else if (!last) {+ x += (glyph.get_width () + kerning) * get_scale (glyph);+ } else {+ glyph.boundaries (out x1, out y1, out x2, out y2);+ x += (x2 - lsb) * get_scale (glyph);+ }+ });+ + return x;+ }++ public double get_decender () {+ double decender_max = get_max_decender ();+ return decender_max > 0 ? decender_max : 0; + } ++ private double get_max_decender () {+ double decender = 0;+ double decender_max = 0;+ + iterate ((glyph, kerning, last) => {+ double x1, y1, x2, y2;+ double y;+ glyph.boundaries (out x1, out y1, out x2, out y2);+ y = Math.fmin (y1, y2);+ decender = (glyph.baseline - y) * get_scale (glyph);+ if (decender > decender_max) {+ decender_max = decender;+ }+ });+ + return decender_max; + }+ + public override void draw (Context cr) {+ double descender = cached_font.bottom_limit + cached_font.base_line;+ double y = widget_y + get_height () + get_font_scale () * descender; // FIXME:+ draw_at_baseline (cr, widget_x, y);+ }+ + public void draw_at_top (Context cr, double px, double py, string cacheid = "") {+ double s = get_font_scale ();+ double y = py + s * (cached_font.top_limit - cached_font.base_line);+ draw_at_baseline (cr, px, y, cacheid);+ }+ + public void set_source_rgba (double r, double g, double b, double a) {+ if (this.r != r || + this.g != g || + this.b != b || + this.a != a) {+ + this.r = r;+ this.g = g;+ this.b = b;+ this.a = a;+ cache = null;+ }+ }+ + public string get_cache_id (int offset_x, int offset_y) {+ string key;+ int64 c;+ + c = (((int64) (r * 255)) << 24)+ | (((int64) (g * 255)) << 16)+ | (((int64) (b * 255)) << 8)+ | (((int64) (a * 255)) << 0);+ + // FIXME: use binary key+ key = @"$font_size $c $offset_x $offset_y";+ + return key;+ }+ + public void draw_at_baseline (Context cr, double px, double py, string cacheid = "") {+ if (cache == null) {+ cache = draw_on_cache_surface (cacheid);+ }+ + double screen_scale = Screen.get_scale ();+ double font_scale = get_font_scale ();+ double cache_y = py - font_scale * (cached_font.top_limit - cached_font.base_line);++ cr.save();+ cr.scale (1 / screen_scale, 1 / screen_scale);+ double scaled_x = (px - margin_left) * screen_scale;+ double scaled_y = cache_y * screen_scale;+ cr.set_source_surface ((!) cache, (int) rint (scaled_x), (int) rint (scaled_y));+ cr.paint ();+ cr.restore();+ }+ + Surface draw_on_cache_surface (string cacheid) {+ double y;+ double ratio;+ double cc_y;+ Context cr;+ Surface cache_surface;+ double screen_scale = Screen.get_scale();+ double h = font_size * screen_scale + 1;++ ratio = get_font_scale ();+ cc_y = (cached_font.top_limit - cached_font.base_line) * ratio;++ // double x = margin_left * ratio;+ double x = 0;+ double w = get_sidebearing_extent () * screen_scale + x + margin_left + 1;+ + cache_surface = Screen.create_background_surface ((int) w, (int) h);+ cr = new Context (cache_surface);+ cr.scale (screen_scale, screen_scale);++ y = cc_y;++ if (unlikely (cached_font.base_line != 0)) {+ warning ("Base line not zero.");+ }+ + iterate ((glyph, kerning, last) => {+ double end;+ + x += kerning * ratio;+ end = x + glyph.get_width () * ratio;+ + // truncation+ if (truncated_width > 0 && end > truncated_width) {+ return;+ }++ if (use_cache) {+ draw_chached (cr, glyph, kerning, last, x, y, cc_y, + ratio, cacheid);+ } else {+ draw_without_cache (cr, glyph, kerning, last, x, y, cc_y, ratio);+ }+ + x = end;+ });+ + return cache_surface;+ }++ void draw_without_cache (Context cr, Glyph glyph, double kerning, bool last, + double x, double y, double cc_y, double ratio) {+ + double lsb;+ + cr.save ();+ cr.set_source_rgba (r, g, b, a);+ cr.new_path ();++ lsb = glyph.left_limit;++ foreach (Path path in glyph.get_visible_paths ()) {+ draw_path (cr, glyph, path, lsb, x, y, ratio);+ }++ cr.fill ();+ cr.restore ();+ + }+ + void draw_chached (Context cr, Glyph glyph, double kerning, bool last, + double x, double y, double cc_y, double ratio,+ string cacheid = "") {+ + double lsb;+ Surface cache;+ Surface cached_glyph;+ Context cc;+ string cache_id;+ double glyph_margin_left = glyph.get_left_side_bearing ();+ + if (glyph_margin_left < 0) {+ glyph_margin_left = -glyph_margin_left;+ } else {+ glyph_margin_left = 0;+ }+ + double xp = (x - glyph_margin_left * ratio) * Screen.get_scale ();+ double yp = (y - cc_y) * Screen.get_scale ();+ int offset_x, offset_y;++ offset_x = (int) (10 * (xp - (int) xp));+ offset_y = (int) (10 * (yp - (int) yp));+ + cache_id = (cacheid == "") ? get_cache_id (offset_x, offset_y) : cacheid;+ + if (!glyph.has_cache (cache_id)) {+ int w = (int) ((2 * glyph_margin_left * ratio + glyph.get_width ()) * ratio) + 2;+ int h = (int) font_size + 2;+ cache = Screen.create_background_surface (w, h);+ cc = new Context (cache);+ + cc.scale(Screen.get_scale (), Screen.get_scale ());+ + lsb = glyph.left_limit - glyph_margin_left;++ cc.save ();+ cc.set_source_rgba (r, g, b, a);+ cc.new_path ();++ foreach (Path path in glyph.get_visible_paths ()) {+ draw_path (cc, glyph, path, lsb, glyph_margin_left * ratio + offset_x / 10.0, cc_y + offset_y / 10.0, ratio);+ }+ + cc.fill ();+ cc.restore ();++ if (use_cache) {+ glyph.set_cache (cache_id, cache);+ }+ + cached_glyph = cache;+ } else {+ cached_glyph = glyph.get_cache (cache_id);+ }++ cr.save ();+ cr.set_antialias (Cairo.Antialias.NONE);+ cr.scale(1 / Screen.get_scale (), 1 / Screen.get_scale ());+ cr.set_source_surface (cached_glyph,+ (int) xp,+ (int) yp);+ cr.paint ();+ cr.restore ();+ }+ + void draw_path (Context cr, Glyph glyph, Path path,+ double lsb, double x, double y, double scale) {+ + EditPoint e, prev;+ double xa, ya, xb, yb, xc, yc, xd, yd;+ double by;+ double s = get_scale (glyph);+ + if (path.points.size > 0) {+ if (unlikely (path.is_open ())) {+ warning (@"Path is open in $(glyph.get_name ()).");+ }+ + //path.add_hidden_double_points (); // FIXME: this distorts shapes+ + prev = path.points.get (path.points.size - 1);+ xa = (prev.x - lsb) * s + x;+ ya = y - prev.y * s;+ cr.move_to (xa, ya);+ + by = (y - cached_font.base_line * s);+ for (int i = 0; i < path.points.size; i++) {+ e = path.points.get (i).copy ();+ + PenTool.convert_point_segment_type (prev, e, PointType.CUBIC);+ + xb = (prev.get_right_handle ().x - lsb) * s + x;+ yb = by - prev.get_right_handle ().y * s;++ xc = (e.get_left_handle ().x - lsb) * s + x;+ yc = by - e.get_left_handle ().y * s;+ + xd = (e.x - lsb) * s + x;+ yd = by - e.y * s;++ cr.curve_to (xb, yb, xc, yc, xd, yd);+ cr.line_to (xd, yd);+ + prev = e;+ }+ }+ }++ public double get_baseline_to_bottom (Glyph g) {+ return get_scale (g) * (-g.baseline - g.bottom_limit);+ }++ public double get_scale (Glyph g) {+ double s = g.top_limit - g.bottom_limit;+ + if (s == 0) {+ s = cached_font.top_limit - cached_font.bottom_limit;+ }+ + return font_size / s;+ }++ public double get_font_scale () {+ return font_size / (cached_font.top_limit - cached_font.bottom_limit);+ }++ public double get_baseline_to_bottom_for_font () {+ return get_font_scale () * (-cached_font.base_line - cached_font.bottom_limit);+ }+ + public void truncate (double max_width) {+ truncated_width = max_width;+ }+}++}new file mode 100644@@ -0,0 +1,1665 @@+/*+ Copyright (C) 2014 Johan Mattsson++ This library is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation; either version 3 of the + License, or (at your option) any later version.++ This library 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 + Lesser General Public License for more details.+*/++using Cairo;+using Math;++namespace BirdFont {++public class TextArea : Widget {+ + public double min_width = 500;+ public double min_height = 100;+ public double font_size;+ public double padding = 3.3;+ public bool single_line = false;+ public Color text_color = Color.black ();+ + public bool draw_carret {+ get { return carret_is_visible; }+ set { + carret_is_visible = value; + if (!value) {+ update_selection = false;+ selection_end = carret.copy ();+ }+ }+ }+ public bool carret_is_visible = false;+ public bool draw_border = true;++ public double width;+ public double height;+ + Carret carret = new Carret ();+ Carret selection_end = new Carret ();+ bool update_selection = false;+ public bool show_selection = false;+ + public signal void scroll (double pixels);+ public signal void text_changed (string text);+ public signal void enter (string text);+ + Gee.ArrayList<Paragraph> paragraphs = new Gee.ArrayList<Paragraph> ();+ private static const int DONE = -2; + + int last_paragraph = 0;+ string text;+ int text_length;+ + Gee.ArrayList<TextUndoItem> undo_items = new Gee.ArrayList<TextUndoItem> ();+ Gee.ArrayList<TextUndoItem> redo_items = new Gee.ArrayList<TextUndoItem> ();+ + bool store_undo_state_at_next_event = false;+ + public bool editable;+ + public TextArea (double font_size = 20, Color? c = null) {+ this.font_size = font_size;+ width = min_width;+ height = min_height;+ editable = true;+ + if (c != null) {+ text_color = (!) c;+ }+ }++ public override void focus (bool focus) {+ draw_carret = focus;+ }++ public override double get_height () {+ return height + 2 * padding;+ }++ public override double get_width () {+ return width + 2 * padding;+ }++ public void set_font_size (double z) {+ font_size = z;+ }+ + bool generate_paragraphs () {+ Paragraph paragraph;++ int next_paragraph = -1;+ + if (is_null (text)) {+ warning ("No text");+ return false;+ }+ + if (last_paragraph == DONE) {+ return false;+ }+ + next_paragraph = text.index_of ("\n", last_paragraph);+ + if (next_paragraph == -1) {+ paragraph = new Paragraph (text.substring (last_paragraph), font_size, paragraphs.size, text_color);+ paragraphs.add (paragraph);+ last_paragraph = DONE;+ } else {+ next_paragraph += "\n".length;+ paragraph = new Paragraph (text.substring (last_paragraph, next_paragraph - last_paragraph), font_size, paragraphs.size, text_color);+ paragraphs.add (paragraph);+ last_paragraph = next_paragraph;+ }+ + return last_paragraph != DONE;+ }+ + void generate_all_paragraphs () {+ while (generate_paragraphs ()) {+ }+ }+ + public override void key_press (uint keyval) {+ unichar c;+ TextUndoItem ui;+ + if (!editable) {+ return;+ }+ + c = (unichar) keyval;+ + switch (c) {+ case ' ':+ store_undo_edit_state ();+ add_character (keyval);+ break;+ case 'a':+ if (KeyBindings.has_ctrl () || KeyBindings.has_logo ()) {+ select_all ();+ } else {+ add_character (keyval);+ }+ break;+ case 'c':+ if (KeyBindings.has_ctrl () || KeyBindings.has_logo ()) {+ ClipTool.copy_text (this);+ } else {+ add_character (keyval);+ }+ break;+ case 'v':+ if (KeyBindings.has_ctrl () || KeyBindings.has_logo ()) {+ ClipTool.paste_text (this);+ store_undo_state_at_next_event = true;+ } else {+ add_character (keyval);+ }+ break;+ case 'y':+ if (KeyBindings.has_ctrl () || KeyBindings.has_logo ()) {+ redo ();+ } else {+ add_character (keyval);+ }+ break;+ case 'z':+ if (KeyBindings.has_ctrl () || KeyBindings.has_logo ()) {+ undo ();+ } else {+ add_character (keyval);+ }+ break;+ case Key.RIGHT:+ check_selection ();+ move_carret_next ();+ break;+ case Key.LEFT:+ check_selection ();+ move_carret_previous ();+ break;+ case Key.DOWN:+ check_selection ();+ move_carret_next_row ();+ break;+ case Key.UP:+ check_selection ();+ move_carret_previous_row ();+ break;+ case Key.END:+ check_selection ();+ move_carret_to_end_of_line ();+ break;+ case Key.HOME:+ check_selection ();+ move_carret_to_beginning_of_line ();+ break;+ case Key.BACK_SPACE:+ if (has_selection ()) {+ ui = delete_selected_text ();+ undo_items.add (ui);+ redo_items.clear ();+ store_undo_state_at_next_event = true;+ } else {+ ui = remove_last_character ();+ undo_items.add (ui);+ redo_items.clear ();+ store_undo_state_at_next_event = true;+ }+ text_changed (get_text ());+ break;+ case Key.ENTER:+ store_undo_edit_state ();+ insert_text ("\n");+ + if (single_line) {+ enter (get_text ());+ }+ break;+ case Key.DEL:+ if (has_selection ()) {+ ui = delete_selected_text ();+ undo_items.add (ui);+ redo_items.clear ();+ store_undo_state_at_next_event = true;+ } else {+ ui = remove_next_character ();+ undo_items.add (ui);+ redo_items.clear ();+ store_undo_state_at_next_event = true;+ }+ text_changed (get_text ());+ break; + default:+ if (!KeyBindings.has_ctrl () && !KeyBindings.has_logo ()) {+ add_character (keyval);+ } + break;+ }+ + GlyphCanvas.redraw ();+ }+ + void check_selection () {+ if (!has_selection () && KeyBindings.has_shift ()) {+ show_selection = true;+ selection_end = carret.copy ();+ }+ + if (!KeyBindings.has_shift ()) {+ show_selection = false;+ }+ }+ + private void add_character (uint keyval) {+ unichar c = (unichar) keyval;+ string s;++ if (!is_modifier_key (keyval) + && !KeyBindings.has_ctrl ()+ && !KeyBindings.has_alt ()) {+ + s = (!) c.to_string (); + + if (s.validate ()) { + if (store_undo_state_at_next_event) {+ store_undo_edit_state ();+ store_undo_state_at_next_event = false;+ }+ + insert_text (s);+ }+ }+ }++ Paragraph get_current_paragraph () {+ Paragraph p;+ + if (unlikely (!(0 <= carret.paragraph < paragraphs.size))) {+ warning (@"No paragraph, index: $(carret.paragraph), size: $(paragraphs.size)");+ p = new Paragraph ("", 0, 0, text_color);+ paragraphs.add (p);+ return p;+ }+ + p = paragraphs.get (carret.paragraph);+ return p;+ }++ public void set_text (string t) {+ int tl;+ + if (single_line) {+ text = t.replace ("\n", "").replace ("\r", "");+ } else {+ text = t;+ }+ + tl = t.length; + text_length += tl;+ + paragraphs.clear ();+ generate_paragraphs ();+ + return_if_fail (paragraphs.size != 0);+ + carret.paragraph = paragraphs.size - 1;+ carret.character_index = paragraphs.get (paragraphs.size - 1).text.length;+ selection_end = carret.copy ();+ show_selection = false;+ + text_changed (get_text ());+ }+ + Carret get_selection_start () {+ if (carret.paragraph == selection_end.paragraph) {+ return carret.character_index < selection_end.character_index ? carret : selection_end;+ }+ + return carret.paragraph < selection_end.paragraph ? carret : selection_end; + }++ Carret get_selection_stop () {+ if (carret.paragraph == selection_end.paragraph) {+ return carret.character_index > selection_end.character_index ? carret : selection_end;+ }+ + return carret.paragraph > selection_end.paragraph ? carret : selection_end; + }+ + public string get_selected_text () {+ Carret selection_start, selection_stop;+ int i;+ Paragraph pg;+ StringBuilder sb;+ + sb = new StringBuilder ();+ + if (!has_selection ()) {+ return "".dup ();+ }+ + selection_start = get_selection_start ();+ selection_stop = get_selection_stop ();+ + if (selection_start.paragraph == selection_stop.paragraph) {+ pg = paragraphs.get (selection_start.paragraph);+ return pg.text.substring (selection_start.character_index, selection_stop.character_index - selection_start.character_index);+ }+ + pg = paragraphs.get (selection_start.paragraph);+ sb.append (pg.text.substring (selection_start.character_index));+ + for (i = selection_start.paragraph + 1; i < selection_stop.paragraph; i++) {+ return_val_if_fail (0 <= i < paragraphs.size, "".dup ());+ pg = paragraphs.get (i);+ sb.append (pg.text);+ }++ pg = paragraphs.get (selection_stop.paragraph);+ sb.append (pg.text.substring (0, selection_stop.character_index));+ + return sb.str;+ }+ + public void select_all () {+ while (last_paragraph != DONE) {+ generate_paragraphs ();+ }+ + if (paragraphs.size > 0) {+ carret.paragraph = 0;+ carret.character_index = 0;+ selection_end.paragraph = paragraphs.size - 1;+ selection_end.character_index = paragraphs.get (paragraphs.size - 1).text_length;+ show_selection = true;+ }+ }+ + public TextUndoItem delete_selected_text () {+ Carret selection_start, selection_stop;+ int i;+ Paragraph pg, pge;+ string e, s, n;+ bool same;+ TextUndoItem ui;+ + ui = new TextUndoItem (carret);+ + e = "";+ s = "";+ n = "";+ + if (!has_selection ()) {+ warning ("No selected text.");+ return ui;+ }+ + selection_start = get_selection_start ();+ selection_stop = get_selection_stop ();+ + same = selection_start.paragraph == selection_stop.paragraph;+ + if (!same) {+ return_val_if_fail (0 <= selection_start.paragraph < paragraphs.size, ui);+ pg = paragraphs.get (selection_start.paragraph);+ s = pg.text.substring (0, selection_start.character_index);++ return_val_if_fail (0 <= selection_stop.paragraph < paragraphs.size, ui);+ pge = paragraphs.get (selection_stop.paragraph);+ e = pge.text.substring (selection_stop.character_index);+ + if (!s.has_suffix ("\n")) {+ ui.deleted.add (pge.copy ());+ ui.edited.add (pg.copy ());+ + pg.set_text (s + e);+ pge.set_text ("");+ } else {+ ui.edited.add (pg.copy ());+ ui.edited.add (pge.copy ());+ + pg.set_text (s);+ pge.set_text (e);+ }+ } else {+ return_val_if_fail (0 <= selection_start.paragraph < paragraphs.size, ui);+ + pg = paragraphs.get (selection_start.paragraph);+ n = pg.text.substring (0, selection_start.character_index);+ n += pg.text.substring (selection_stop.character_index);+ + if (n == "") {+ ui.deleted.add (pg.copy ());+ paragraphs.remove_at (selection_start.paragraph);+ } else {+ ui.edited.add (pg.copy ());+ }+ + pg.set_text (n);+ }+ + if (e == "" && !same) {+ paragraphs.remove_at (selection_stop.paragraph);+ }+ + for (i = selection_stop.paragraph - 1; i > selection_start.paragraph; i--) {+ return_val_if_fail (0 <= i < paragraphs.size, ui);+ ui.deleted.add (paragraphs.get (i));+ paragraphs.remove_at (i);+ }+ + if (s == "" && !same) {+ return_val_if_fail (0 <= selection_start.paragraph < paragraphs.size, ui);+ paragraphs.remove_at (selection_start.paragraph);+ }+ + carret = selection_start.copy ();+ selection_end = carret.copy ();+ + show_selection = false;+ update_paragraph_index ();+ layout ();+ + return ui;+ }+ + void update_paragraph_index () {+ int i = 0;+ foreach (Paragraph p in paragraphs) {+ p.index = i;+ i++;+ }+ }+ + public TextUndoItem remove_last_character () {+ TextUndoItem ui;+ move_carret_previous ();+ ui = remove_next_character ();+ return ui;+ }+ + public TextUndoItem remove_next_character () {+ Paragraph paragraph;+ Paragraph next_paragraph;+ int index;+ unichar c;+ string np;+ TextUndoItem ui;+ + ui = new TextUndoItem (carret);+ + return_val_if_fail (0 <= carret.paragraph < paragraphs.size, ui);+ paragraph = paragraphs.get (carret.paragraph);+ + index = carret.character_index;+ + paragraph.text.get_next_char (ref index, out c);+ + if (index >= paragraph.text_length) {+ np = paragraph.text.substring (0, carret.character_index);+ + if (carret.paragraph + 1 < paragraphs.size) {+ next_paragraph = paragraphs.get (carret.paragraph + 1);+ paragraphs.remove_at (carret.paragraph + 1);+ + np = np + next_paragraph.text;+ + ui.deleted.add (next_paragraph);+ }+ + paragraph.set_text (np);+ ui.edited.add (paragraph);+ } else {+ np = paragraph.text.substring (0, carret.character_index) + paragraph.text.substring (index);+ paragraph.set_text (np);++ if (np == "") {+ return_val_if_fail (carret.paragraph > 0, ui);+ carret.paragraph--;+ paragraph = paragraphs.get (carret.paragraph);+ carret.character_index = paragraph.text_length;+ + ui.deleted.add (paragraphs.get (carret.paragraph + 1));+ + paragraphs.remove_at (carret.paragraph + 1);+ } else {+ ui.edited.add (paragraph);+ }+ }+ + update_paragraph_index ();+ layout ();++ return ui;+ }+ + public void move_carret_next () {+ unichar c;+ + move_carret_one_character ();+ + if (KeyBindings.has_ctrl ()) {+ while (true) {+ c = move_carret_one_character ();+ + if (c == '\0' || c == ' ') {+ break;+ }+ }+ }+ }+ + unichar move_carret_one_character () {+ Paragraph paragraph;+ int index;+ unichar c;+ + return_val_if_fail (0 <= carret.paragraph < paragraphs.size, '\0');+ paragraph = paragraphs.get (carret.paragraph);+ + index = carret.character_index;+ + paragraph.text.get_next_char (ref index, out c);+ + if (index >= paragraph.text_length && carret.paragraph + 1 < paragraphs.size) {+ carret.paragraph++;+ carret.character_index = 0;+ c = ' ';+ } else {+ carret.character_index = index;+ }+ + return c; + }++ public void move_carret_previous () {+ unichar c;+ + move_carret_back_one_character ();+ + if (KeyBindings.has_ctrl ()) {+ while (true) {+ c = move_carret_back_one_character ();+ + if (c == '\0' || c == ' ') {+ break;+ }+ }+ }+ }+ + unichar move_carret_back_one_character () {+ Paragraph paragraph;+ int index, last_index;+ unichar c;+ + return_val_if_fail (0 <= carret.paragraph < paragraphs.size, '\0');+ paragraph = paragraphs.get (carret.paragraph);+ + index = 0;+ last_index = -1;+ + while (paragraph.text.get_next_char (ref index, out c) && index < carret.character_index) {+ last_index = index;+ }+ + if (last_index <= 0 && carret.paragraph > 0) {+ carret.paragraph--;+ + return_val_if_fail (0 <= carret.paragraph < paragraphs.size, '\0');+ paragraph = paragraphs.get (carret.paragraph);+ carret.character_index = paragraph.text_length;+ + if (paragraph.text.has_suffix ("\n")) {+ carret.character_index -= "\n".length;+ }+ + c = ' ';+ } else if (last_index > 0) {+ carret.character_index = last_index;+ } else {+ carret.character_index = 0;+ c = ' ';+ }+ + return_val_if_fail (0 <= carret.paragraph < paragraphs.size, '\0');+ + return c;+ }+ + public void move_carret_next_row () {+ double nr = font_size;+ + if (carret.desired_y + 2 * font_size >= allocation.height) {+ scroll (2 * font_size);+ nr = -font_size;+ }+ + if (carret.desired_y + nr < widget_y + height - padding) {+ carret = get_carret_at (carret.desired_x - widget_x - padding, carret.desired_y + nr);+ }+ }++ public void move_carret_to_end_of_line () {+ carret = get_carret_at (widget_x + padding + width, carret.desired_y, false); + }++ public void move_carret_to_beginning_of_line () {+ carret = get_carret_at (widget_x, carret.desired_y, false); + }+ + public void move_carret_previous_row () {+ double nr = -font_size;+ + if (carret.desired_y - 2 * font_size < 0) {+ scroll (-2 * font_size);+ nr = font_size;+ }+ + if (carret.desired_y + nr > widget_y + padding) {+ carret = get_carret_at (carret.desired_x, carret.desired_y + nr);+ }+ }+ + public bool has_selection () {+ return show_selection && selection_is_visible ();+ }+ + private bool selection_is_visible () {+ return carret.paragraph != selection_end.paragraph || carret.character_index != selection_end.character_index;+ }+ + public void insert_text (string t) {+ string s;+ Paragraph paragraph;+ TextUndoItem ui;+ Gee.ArrayList<string> pgs;+ bool u = false;+ + pgs = new Gee.ArrayList<string> ();+ + if (single_line) {+ s = t.replace ("\n", "").replace ("\r", "");+ pgs.add (s);+ } else {+ if (t.last_index_of ("\n") > 0) {+ string[] parts = t.split ("\n");+ int i;+ for (i = 0; i < parts.length -1; i++) {+ pgs.add (parts[i]);+ pgs.add ("\n");+ }++ pgs.add (parts[parts.length - 1]);+ + if (t.has_suffix ("\n")) {+ pgs.add ("\n");+ }+ } else {+ s = t;+ pgs.add (s);+ }+ }+ + if (has_selection () && show_selection) {+ ui = delete_selected_text ();+ u = true;+ + if (paragraphs.size == 0) {+ paragraphs.add (new Paragraph ("", font_size, 0, text_color));+ }+ } else {+ ui = new TextUndoItem (carret);+ }+ + return_if_fail (0 <= carret.paragraph < paragraphs.size);+ paragraph = paragraphs.get (carret.paragraph);+ + if (pgs.size > 0) {+ if (!u) {+ ui.edited.add (paragraph.copy ());+ }+ + string first = pgs.get (0);+ + string end; + string nt = paragraph.text.substring (0, carret.character_index);+ + nt += first;+ end = paragraph.text.substring (carret.character_index);+ + paragraph.set_text (nt);+ + int paragraph_index = carret.paragraph;+ Paragraph next_paragraph = paragraph;+ for (int i = 1; i < pgs.size; i++) {+ paragraph_index++;+ string next = pgs.get (i);+ next_paragraph = new Paragraph (next, font_size, paragraph_index, text_color);+ paragraphs.insert (paragraph_index, next_paragraph);+ ui.added.add (next_paragraph);+ u = true;+ }++ carret.paragraph = paragraph_index;+ carret.character_index = next_paragraph.text.length;++ next_paragraph.set_text (next_paragraph.text + end);+ }+ + if (u) {+ undo_items.add (ui);+ redo_items.clear ();+ }++ update_paragraph_index ();+ layout ();+ + text_changed (get_text ());+ show_selection = false;+ }+ + public string get_text () {+ StringBuilder sb = new StringBuilder ();+ + generate_all_paragraphs ();+ + foreach (Paragraph p in paragraphs) {+ sb.append (p.text);+ }++ return sb.str;+ }+ + Carret get_carret_at (double click_x, double click_y, + bool check_boundaries = true) {+ + int i = 0;+ double tx, ty;+ double p;+ string w;+ int ch_index;+ double min_d = double.MAX;+ Carret c = new Carret ();+ double dt;++ c.paragraph = -1;+ c.desired_x = click_x;+ c.desired_y = click_y;+ + foreach (Paragraph paragraph in paragraphs) {+ if (!check_boundaries || paragraph.text_is_on_screen (allocation, widget_y)) {+ ch_index = 0;++ if (paragraph.start_y + widget_y - font_size <= click_y <= paragraph.end_y + widget_y + font_size) { + foreach (Text next_word in paragraph.words) {+ double tt_click = click_y - widget_y - padding + font_size;++ w = next_word.text;+ + if (next_word.widget_y <= tt_click <= next_word.widget_y + font_size) {+ + p = next_word.get_sidebearing_extent ();+ + if ((next_word.widget_y <= tt_click <= next_word.widget_y + font_size)+ && (next_word.widget_x + widget_x <= click_x <= next_word.widget_x + widget_x + padding + next_word.get_sidebearing_extent ())) {+ + tx = widget_x + next_word.widget_x + padding;+ ty = widget_y + next_word.widget_y + padding;+ + next_word.iterate ((glyph, kerning, last) => {+ double cw;+ int ci;+ double d;+ string gc = (!) glyph.get_unichar ().to_string ();+ + d = Math.fabs (click_x - tx);+ + if (d <= min_d) {+ min_d = d;+ c.character_index = ch_index;+ c.paragraph = i;+ }+ + cw = (glyph.get_width ()) * next_word.get_font_scale () + kerning;+ ci = gc.length;+ + tx += cw;+ ch_index += ci;+ });++ dt = Math.fabs (click_x - (tx + widget_x + padding));+ if (dt < min_d) {+ min_d = dt;+ c.character_index = ch_index;+ c.paragraph = i;+ }+ } else {+ dt = Math.fabs (click_x - (next_word.widget_x + widget_x + padding + next_word.get_sidebearing_extent ()));+ + if (dt < min_d) {+ min_d = dt;+ c.character_index = ch_index + w.length;+ + if (w.has_suffix ("\n")) {+ c.character_index -= "\n".length;+ }+ + c.paragraph = i;+ }+ + ch_index += w.length;+ }+ } else {+ ch_index += w.length;+ }+ }+ }+ }+ i++;+ }+ + if (unlikely (c.paragraph < 0)) {+ c.paragraph = paragraphs.size > 0 ? paragraphs.size - 1 : 0;+ c.character_index = paragraphs.size > 0 ? paragraphs.get (c.paragraph).text.length : 0;+ }+ + store_undo_state_at_next_event = true;+ + return c;+ }+ + /** @return offset to click in text. */+ public override void layout () {+ double p;+ double tx, ty;+ string w;+ double xmax = 0;+ int i = 0;+ double dd;+ + tx = 0;+ ty = font_size;+ + if (allocation.width <= 0 || allocation.height <= 0) {+ warning ("Parent widget allocation is not set.");+ }+ + for (i = paragraphs.size - 1; i >= 0 && paragraphs.size > 1; i--) {+ if (unlikely (paragraphs.get (i).is_empty ())) {+ warning ("Empty paragraph.");+ paragraphs.remove_at (i);+ update_paragraph_index ();+ }+ }+ + i = 0;+ foreach (Paragraph paragraph in paragraphs) {+ if (paragraph.need_layout + || (paragraph.text_area_width != width+ && paragraph.text_is_on_screen (allocation, widget_y))) {+ + paragraph.start_y = ty;+ paragraph.start_x = tx;+ + paragraph.cached_surface = null;+ + foreach (Text next_word in paragraph.words) {+ next_word.set_font_size (font_size);+ + w = next_word.text;+ p = next_word.get_sidebearing_extent ();++ if (unlikely (p == 0)) {+ warning (@"Zero width word: $(w)");+ }++ if (w == "") {+ break;+ }++ if (w == "\n") {+ next_word.widget_x = tx;+ next_word.widget_y = ty;+ + tx = 0;+ ty += next_word.font_size;+ } else {+ if (!single_line) {+ if (tx + p + 2 * padding > width || w == "\n") {+ tx = 0;+ ty += next_word.font_size;+ }+ }++ if (tx + p > xmax) {+ xmax = tx + p;+ }+ + next_word.widget_x = tx;+ next_word.widget_y = ty;+ + if (w != "\n") {+ tx += p;+ }+ }+ }++ if (tx > xmax) {+ xmax = tx;+ }+ + paragraph.text_area_width = width;+ paragraph.width = xmax;+ paragraph.end_x = tx;+ paragraph.end_y = ty;+ paragraph.need_layout = false;+ }+ + if (xmax > width) {+ break;+ }+ + tx = paragraph.end_x;+ ty = paragraph.end_y;+ i++;+ }+ + if (xmax > width) {+ this.width = xmax + 2 * padding;+ layout ();+ return;+ }+ + this.height = fmax (min_height, ty + 2 * padding);++ if (last_paragraph != DONE) {+ this.height = (text_length / (double) last_paragraph) * ty + 2 * padding; // estimate height+ }+ + if (ty + widget_y < allocation.height && last_paragraph != DONE) {+ generate_paragraphs ();+ layout ();+ return;+ }++ ty = font_size;+ tx = 0;+ + foreach (Paragraph paragraph in paragraphs) {+ dd = ty - paragraph.start_y;+ + if (dd != 0) {+ paragraph.start_y += dd;+ paragraph.end_y += dd;+ foreach (Text word in paragraph.words) {+ word.widget_y += dd;+ }+ }+ + ty = paragraph.end_y;+ }+ }+ + public override void button_press (uint button, double x, double y) {+ if (is_over (x, y)) {+ carret = get_carret_at (x, y);+ selection_end = carret.copy ();+ update_selection = true;+ }+ }++ public override void button_release (uint button, double x, double y) {+ update_selection = false;+ show_selection = selection_is_visible ();+ }+ + public override bool motion (double x, double y) {+ if (update_selection) {+ selection_end = get_carret_at (x, y);+ show_selection = selection_is_visible ();+ }+ + return update_selection;+ }+ + public override void draw (Context cr) {+ Text word;+ double tx, ty;+ string w;+ double scale;+ double width;+ double x = widget_x;+ double y = widget_y;+ Carret selection_start, selection_stop;+ double carret_x;+ double carret_y;+ + layout ();+ + if (draw_border) {+ // background+ cr.save ();+ cr.set_line_width (1);+ Theme.color (cr, "Text Area Background");+ draw_rounded_rectangle (cr, x, y, this.width, this.height - padding, padding);+ cr.fill ();+ cr.restore ();+ + // border+ cr.save ();+ cr.set_line_width (1);+ Theme.color (cr, "Foreground 1");+ draw_rounded_rectangle (cr, x, y, this.width, this.height - padding, padding);+ cr.stroke ();+ cr.restore ();+ }+ + cr.save ();++ word = new Text ();+ + width = this.width - padding;+ x += padding;+ scale = word.get_font_scale ();+ y += font_size;+ + // draw selection background+ if (has_selection ()) {+ tx = 0;+ ty = 0;+ + selection_start = get_selection_start ();+ selection_stop = get_selection_stop ();+ + cr.save ();+ Theme.color (cr, "Highlighted 1");++ for (int i = selection_start.paragraph; i <= selection_stop.paragraph; i++) {+ return_if_fail (0 <= i < paragraphs.size);+ Paragraph pg = paragraphs.get (i);++ if (pg.text_is_on_screen (allocation, widget_y)) {+ int char_index = 0;+ + foreach (Text next_word in pg.words) {+ double cw = next_word.get_sidebearing_extent ();+ bool paint_background = false;+ bool partial_start = false;+ bool partial_stop = false;+ int wl;+ + w = next_word.text;+ wl = w.length;+ scale = next_word.get_font_scale ();+ + if (selection_start.paragraph == selection_stop.paragraph) {+ partial_start = true;+ partial_stop = true;+ } else if (selection_start.paragraph < i < selection_stop.paragraph) {+ paint_background = true;+ } else if (selection_start.paragraph == i) {+ paint_background = true;+ partial_start = true;+ } else if (selection_stop.paragraph == i) {+ paint_background = char_index + wl < selection_stop.character_index;+ partial_stop = !paint_background;+ }+ + if (paint_background && !(partial_start || partial_stop)) { + double selection_y = widget_y + next_word.widget_y + scale * -next_word.cached_font.bottom_limit - font_size;+ cr.rectangle (widget_x + padding + next_word.widget_x - 1, selection_y, cw + 1, font_size);+ cr.fill ();+ }+ + if (partial_start || partial_stop) {+ int index = char_index;+ double bx = widget_x + padding + next_word.widget_x + (partial_start ? 0 : 1);+ + next_word.iterate ((glyph, kerning, last) => {+ double cwi;+ int ci;+ bool draw = (index >= selection_start.character_index && partial_start && !partial_stop)+ || (index < selection_stop.character_index && !partial_start && partial_stop)+ || (selection_start.character_index <= index < selection_stop.character_index && partial_start && partial_stop);+ + cwi = (glyph.get_width ()) * next_word.get_font_scale () + kerning;++ if (draw) {+ double selection_y = widget_y + next_word.widget_y + scale * -next_word.cached_font.bottom_limit - font_size;+ cr.rectangle (bx - 1, selection_y, cwi + 1, font_size);+ cr.fill ();+ }+ + bx += cwi;+ ci = ((!) glyph.get_unichar ().to_string ()).length;+ index += ci;+ });+ } + + char_index += w.length; + }+ }+ }+ + cr.restore ();+ }++ tx = 0;+ ty = 0; + + int first_visible = 0;+ int last_visible;+ int paragraphs_size = paragraphs.size;+ while (first_visible < paragraphs_size) {+ if (paragraphs.get (first_visible).text_is_on_screen (allocation, widget_y)) {+ break;+ }+ first_visible++;+ }+ + last_visible = first_visible;+ while (last_visible < paragraphs_size) {+ if (!paragraphs.get (last_visible).text_is_on_screen (allocation, widget_y)) {+ last_visible++;+ break;+ }+ last_visible++;+ } + + if (paragraphs_size == 0) {+ if (carret_is_visible) {+ draw_carret_at (cr, widget_x + padding, widget_y + font_size + padding);+ }+ + return;+ }+ + Context cc; // cached context+ Paragraph paragraph;+ paragraph = paragraphs.get (0);+ + tx = paragraph.start_x;+ ty = paragraph.start_y;++ for (int i = first_visible; i < last_visible; i++) {+ paragraph = paragraphs.get (i);+ + tx = paragraph.start_x;+ ty = paragraph.start_y;++ if (paragraph.cached_surface == null) { + paragraph.cached_surface = Screen.create_background_surface ((int) width + 2, paragraph.get_height () + (int) font_size + 2);+ cc = new Context ((!) paragraph.cached_surface);+ cc.scale (Screen.get_scale(), Screen.get_scale());+ + foreach (Text next_word in paragraph.words) {+ if (next_word.text != "\n") {+ next_word.draw_at_top (cc, next_word.widget_x, next_word.widget_y - ty);+ }+ }+ }+ + if (likely (paragraph.cached_surface != null)) {+ // FIXME: subpixel offset in text area+ Screen.paint_background_surface(cr, + (!) paragraph.cached_surface,+ (int) (x + tx),+ (int) (widget_y + paragraph.start_y - font_size + padding));+ } else {+ warning ("No paragraph image.");+ }+ }++ if (carret_is_visible) {+ get_carret_position (carret, out carret_x, out carret_y);+ + if (carret_y < 0) {+ draw_carret_at (cr, widget_x + padding, widget_y + font_size + padding);+ } else {+ draw_carret_at (cr, carret_x, carret_y);+ }+ }+ + if (has_selection ()) {+ get_carret_position (selection_end, out carret_x, out carret_y);+ + if (carret_y < 0) {+ draw_carret_at (cr, widget_x + padding, widget_y + font_size + padding);+ } else {+ draw_carret_at (cr, carret_x, carret_y);+ }+ }+ }+ + void get_carret_position (Carret carret, out double carret_x, out double carret_y) {+ Paragraph paragraph;+ double tx;+ double ty;+ int ch_index;+ int wl;+ double pos_x, pos_y;++ ch_index = 0;++ carret_x = -1;+ carret_y = -1;++ return_if_fail (0 <= carret.paragraph < paragraphs.size);+ paragraph = paragraphs.get (carret.paragraph);++ pos_x = -1;+ pos_y = -1;+ + foreach (Text next_word in paragraph.words) {+ string w = next_word.text;+ wl = w.length;++ if (carret.character_index == ch_index) {+ pos_x = next_word.widget_x + widget_x + padding;+ pos_y = widget_y + next_word.widget_y + next_word.get_baseline_to_bottom_for_font (); + } else if (carret.character_index >= ch_index + wl) {+ pos_x = next_word.widget_x + next_word.get_sidebearing_extent () + widget_x + padding;+ pos_y = widget_y + next_word.widget_y + next_word.get_baseline_to_bottom_for_font ();+ + if (next_word.text.has_suffix ("\n")) {+ pos_x = widget_x + padding;+ pos_y += next_word.font_size;+ }+ } else if (ch_index < carret.character_index <= ch_index + wl) {+ tx = widget_x + next_word.widget_x;+ ty = widget_y + next_word.widget_y + next_word.get_baseline_to_bottom_for_font ();+ + if (carret.character_index <= ch_index) {+ pos_x = widget_x + padding;+ pos_y = ty;+ }++ next_word.iterate ((glyph, kerning, last) => {+ double cw;+ int ci;++ cw = (glyph.get_width ()) * next_word.get_font_scale () + kerning;+ ci = ((!) glyph.get_unichar ().to_string ()).length;+ + if (ch_index < carret.character_index <= ch_index + ci) {+ pos_x = tx + cw + padding;+ pos_y = ty;+ + if (glyph.get_unichar () == '\n') {+ pos_x = widget_x + padding;+ pos_y += next_word.font_size;+ }+ }+ + tx += cw;+ ch_index += ci;+ });+ }+ + ch_index += wl;+ }+ + carret_x = pos_x;+ carret_y = pos_y;+ }+ + void draw_carret_at (Context cr, double x, double y) {+ cr.save ();+ cr.set_source_rgba (0, 0, 0, 0.5);+ cr.set_line_width (1);+ cr.move_to (x, y);+ cr.line_to (x, y - font_size);+ cr.stroke ();+ cr.restore (); + }++ public void store_undo_edit_state () {+ TextUndoItem ui = new TextUndoItem (carret);+ ui.edited.add (get_current_paragraph ().copy ());+ undo_items.add (ui);+ redo_items.clear ();+ }+ + public void redo () {+ TextUndoItem i;+ TextUndoItem undo_item;+ + if (redo_items.size > 0) {+ i = redo_items.get (redo_items.size - 1); + + undo_item = new TextUndoItem (i.carret);+ + i.deleted.sort ((a, b) => {+ Paragraph pa = (Paragraph) a;+ Paragraph pb = (Paragraph) b;+ return pb.index - pa.index;+ });++ i.added.sort ((a, b) => {+ Paragraph pa = (Paragraph) a;+ Paragraph pb = (Paragraph) b;+ return pa.index - pb.index;+ });++ foreach (Paragraph p in i.deleted) {+ if (unlikely (!(0 <= p.index < paragraphs.size))) {+ warning ("Paragraph not found.");+ } else {+ undo_item.deleted.add (p.copy ());+ paragraphs.remove_at (p.index);+ }+ }+ + foreach (Paragraph p in i.added) {+ if (p.index == paragraphs.size) {+ paragraphs.add (p.copy ());+ } else {+ if (unlikely (!(0 <= p.index < paragraphs.size))) {+ warning (@"Index: $(p.index) out of bounds, size: $(paragraphs.size)");+ } else {+ undo_item.added.add (paragraphs.get (p.index).copy ());+ paragraphs.insert (p.index, p.copy ());+ }+ }+ }++ foreach (Paragraph p in i.edited) {+ if (unlikely (!(0 <= p.index < paragraphs.size))) {+ warning (@"Index: $(p.index ) out of bounds, size: $(paragraphs.size)");+ return;+ }+ + undo_item.edited.add (paragraphs.get (p.index).copy ());+ paragraphs.set (p.index, p.copy ());+ } + + redo_items.remove_at (redo_items.size - 1);+ undo_items.add (undo_item);+ + carret = i.carret.copy ();+ layout ();+ }+ }+ + public void undo () {+ TextUndoItem i;+ TextUndoItem redo_item;+ + if (undo_items.size > 0) {+ i = undo_items.get (undo_items.size - 1); + redo_item = new TextUndoItem (i.carret);+ + i.deleted.sort ((a, b) => {+ Paragraph pa = (Paragraph) a;+ Paragraph pb = (Paragraph) b;+ return pa.index - pb.index;+ });++ i.added.sort ((a, b) => {+ Paragraph pa = (Paragraph) a;+ Paragraph pb = (Paragraph) b;+ return pb.index - pa.index;+ });+ + foreach (Paragraph p in i.added) {+ if (unlikely (!(0 <= p.index < paragraphs.size))) {+ warning ("Paragraph not found.");+ } else {+ redo_item.added.add (paragraphs.get (p.index).copy ());+ paragraphs.remove_at (p.index);+ }+ }+ + foreach (Paragraph p in i.deleted) {+ if (p.index == paragraphs.size) {+ paragraphs.add (p.copy ());+ } else {+ if (unlikely (!(0 <= p.index < paragraphs.size))) {+ warning (@"Index: $(p.index) out of bounds, size: $(paragraphs.size)");+ } else {+ redo_item.deleted.add (p.copy ());+ paragraphs.insert (p.index, p.copy ());+ }+ }+ }++ foreach (Paragraph p in i.edited) {+ if (unlikely (!(0 <= p.index < paragraphs.size))) {+ warning (@"Index: $(p.index ) out of bounds, size: $(paragraphs.size)");+ return;+ }+ + redo_item.edited.add (paragraphs.get (p.index).copy ());+ paragraphs.set (p.index, p.copy ());+ } + + undo_items.remove_at (undo_items.size - 1);+ redo_items.add (redo_item);+ + carret = i.carret.copy ();+ layout ();+ }+ }+ + public void set_editable (bool editable) {+ this.editable = editable;+ }+ + public class TextUndoItem : GLib.Object {+ public Carret carret;+ public Gee.ArrayList<Paragraph> added = new Gee.ArrayList<Paragraph> ();+ public Gee.ArrayList<Paragraph> edited = new Gee.ArrayList<Paragraph> ();+ public Gee.ArrayList<Paragraph> deleted = new Gee.ArrayList<Paragraph> ();+ + public TextUndoItem (Carret c) {+ carret = c.copy ();+ }+ }+ + public class Paragraph : GLib.Object {+ public double end_x = -10000;+ public double end_y = -10000;+ + public double start_x = -10000;+ public double start_y = -10000;+ + public double width = -10000;+ public double text_area_width = -10000;+ + public string text;+ + public Gee.ArrayList<Text> words {+ get {+ if (words_in_paragraph.size == 0) {+ generate_words ();+ }+ + return words_in_paragraph;+ }+ }+ + private Gee.ArrayList<Text> words_in_paragraph = new Gee.ArrayList<Text> ();+ public int text_length;+ public bool need_layout = true;+ public Surface? cached_surface = null;+ double font_size;+ public int index;+ Color text_color;+ + public Paragraph (string text, double font_size, int index, Color c) {+ this.index = index;+ this.font_size = font_size;+ text_color = c;+ set_text (text);+ }++ public Paragraph copy () {+ Paragraph p = new Paragraph (text.dup (), font_size, index, text_color);+ p.need_layout = true;+ return p;+ }++ public bool is_empty () {+ return text == "";+ }++ public void set_text (string t) {+ this.text = t;+ text_length = t.length;+ need_layout = true;+ words.clear ();+ cached_surface = null;+ }++ public int get_height () {+ return (int) (end_y - start_y) + 1;+ }+ + public int get_width () {+ return (int) width + 1;+ }+ + public bool text_is_on_screen (WidgetAllocation alloc, double widget_y) {+ bool v = (0 <= start_y + widget_y <= alloc.height)+ || (0 <= end_y + widget_y <= alloc.height)+ || (start_y + widget_y <= 0 && alloc.height <= end_y + widget_y);+ return v;+ }++ private void generate_words () {+ string w;+ int p = 0;+ bool carret_at_word_end = false;+ Text word;+ int carret = 0;+ int iter_pos = 0;++ return_if_fail (words_in_paragraph.size == 0);++ while (p < text_length) {+ w = get_next_word (out carret_at_word_end, ref iter_pos, carret);+ + if (w == "") {+ break;+ }+ + word = new Text (w, font_size);+ + word.r = text_color.r;+ word.g = text_color.g;+ word.b = text_color.b;+ word.a = text_color.a;+ + words_in_paragraph.add (word);+ }+ }++ string get_next_word (out bool carret_at_end_of_word, ref int iter_pos, int carret) {+ int i;+ int ni;+ int pi;+ string n;+ int nl;+ + carret_at_end_of_word = false;+ + if (iter_pos >= text_length) {+ carret_at_end_of_word = true;+ return "".dup ();+ }+ + if (text.get_char (iter_pos) == '\n') {+ iter_pos += "\n".length;+ carret_at_end_of_word = (iter_pos == carret);+ return "\n".dup ();+ }+ + i = text.index_of (" ", iter_pos);+ pi = i + " ".length;+ + ni = text.index_of ("\t", iter_pos);+ if (ni != -1 && ni < pi || i == -1) {+ i = ni;+ pi = i + "\t".length;+ }+ + ni = text.index_of ("\n", iter_pos);+ if (ni != -1 && ni < pi || i == -1) {+ i = ni;+ pi = i;+ }+ + if (iter_pos + iter_pos - pi > text_length || i == -1) {+ n = text.substring (iter_pos);+ } else {+ n = text.substring (iter_pos, pi - iter_pos);+ }+ + nl = n.length;+ if (iter_pos < carret < iter_pos + nl) {+ n = text.substring (iter_pos, carret - iter_pos);+ nl = n.length;+ carret_at_end_of_word = true;+ }+ + iter_pos += nl;+ + if (iter_pos == carret) {+ carret_at_end_of_word = true;+ }+ + return n;+ }+ }+ + public class Carret : GLib.Object {+ + public int paragraph = 0;+ + public int character_index {+ get { + return ci;+ }+ + set { + ci = value;+ }+ }+ + private int ci = 0;+ + public double desired_x = 0;+ public double desired_y = 0;+ + public Carret () {+ }+ + public void print () {+ stdout.printf (@"paragraph: $paragraph, character_index: $character_index\n");+ }+ + public Carret copy () {+ Carret c = new Carret ();+ + c.paragraph = paragraph;+ c.character_index = character_index;+ + c.desired_x = desired_x;+ c.desired_y = desired_y;+ + return c;+ }+ }+}++}new file mode 100644@@ -0,0 +1,140 @@+/*+ Copyright (C) 2015 Johan Mattsson++ This library is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation; either version 3 of the + License, or (at your option) any later version.++ This library 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 + Lesser General Public License for more details.+*/++#include <stdio.h>+#include <glib.h>+#include <fontconfig/fontconfig.h>++gchar* find_font_with_property (FcConfig* fontconfig, const gchar* characters, const gchar* property) {+ FcPattern* pattern;+ FcCharSet* character_set;+ FcObjectSet* font_properties;+ FcFontSet* fonts;+ FcPattern* font;+ FcChar8* path;+ gchar* result;+ gchar* remaining_characters;+ gunichar character;+ + if (fontconfig == NULL) {+ g_warning("Font config not loaded.");+ return NULL;+ }+ + result = NULL;+ pattern = FcPatternCreate ();+ + character_set = FcCharSetCreate ();+ + remaining_characters = (gchar*) characters;+ while (TRUE) {+ character = g_utf8_get_char (remaining_characters);+ + if (character == '\0') {+ break;+ }+ + FcCharSetAddChar(character_set, character);+ + remaining_characters = g_utf8_next_char (remaining_characters);+ }++ FcPatternAddCharSet (pattern, FC_CHARSET, character_set);+ FcCharSetDestroy (character_set);+ FcPatternAddInteger (pattern, FC_SLANT, FC_SLANT_ROMAN);+ + FcPatternAddBool(pattern, FC_SCALABLE, FcTrue);+ font_properties = FcObjectSetBuild (property, NULL); + fonts = FcFontList (fontconfig, pattern, font_properties);+ + if (fonts && fonts->nfont > 0) {+ font = fonts->fonts[0];+ if (FcPatternGetString(font, property, 0, &path) == FcResultMatch) {+ result = g_strdup ((gchar*) path);+ }+ }++ if (fonts) {+ FcFontSetDestroy(fonts);+ }++ if (pattern) {+ FcPatternDestroy(pattern);+ }++ return result;+}++/** Find a fallback font for a set of characters.+ * @return A path to the font file.+ */+gchar* find_font (FcConfig* fontconfig, const gchar* characters) {+ return find_font_with_property (fontconfig, characters, FC_FILE);+}++/** Find a fallback font for a set of characters.+ * @return Family name of the font.+ */+gchar* find_font_family (FcConfig* fontconfig, const gchar* characters) {+ return find_font_with_property (fontconfig, characters, FC_FAMILY);+}++/** Find a font file from its family name.+ * @param font_config fontconfig instance+ * @param font_name name of the font+ * @return full path to the font file+ */+gchar* find_font_file (FcConfig* font_config, const gchar* font_name) {+ const FcChar8* name;+ FcPattern* search_pattern;+ FcPattern* font;+ FcChar8* file;+ gchar* path;+ FcObjectSet* font_properties;+ FcFontSet* fonts;+ int i;++ if (font_config == NULL) {+ g_warning("Font config not loaded.");+ return NULL;+ }+ + path = NULL;+ name = font_name;++ search_pattern = FcPatternCreate ();+ FcPatternAddString (search_pattern, FC_FAMILY, name);+ FcPatternAddBool (search_pattern, FC_SCALABLE, FcTrue);+ FcPatternAddInteger (search_pattern, FC_WEIGHT, FC_WEIGHT_MEDIUM);+ FcPatternAddInteger (search_pattern, FC_SLANT, FC_SLANT_ROMAN);+ + font_properties = FcObjectSetBuild (FC_FILE, NULL);+ fonts = FcFontList (font_config, search_pattern, font_properties);+ + if (fonts->nfont > 0) {+ for (i = 0; i < fonts->nfont; i++) {+ font = fonts->fonts[i];+ + if (FcPatternGetString(font, FC_FILE, 0, &file) == FcResultMatch) {+ path = g_strdup ((gchar*) file);+ break;+ }+ }+ FcPatternDestroy (font);+ }++ FcPatternDestroy (search_pattern);+ + return path;+}