Mercurial > projects > dynamin
view dynamin/painting/text_layout.d @ 91:85cd0b04777c
Add TextLayout.formatRuns and fontFormatRuns, and tests for formatRuns
author | Jordan Miner <jminer7@gmail.com> |
---|---|
date | Thu, 05 Aug 2010 03:31:21 -0500 |
parents | 41b430aa319f |
children | 27445f24d5fd |
line wrap: on
line source
// Written in the D programming language // www.digitalmars.com/d/ /* * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is the Dynamin library. * * The Initial Developer of the Original Code is Jordan Miner. * Portions created by the Initial Developer are Copyright (C) 2007-2010 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Jordan Miner <jminer7@gmail.com> * */ module dynamin.painting.text_layout; import dynamin.core.list; import dynamin.core.string; import dynamin.painting.color; import tango.text.convert.Utf; import tango.io.Stdout; //{{{ character formatting types /// The line style of an underline, strikethrough, or overline. // TODO: add images of what these line styles look like enum LineStyle { /// None, /// Single, /// Double, /// Dotted, /// Dashed, /// Wavy } /// enum SmallType { // Specifies normal text. Normal, // Specifies text smaller than normal and raised above the normal baseline. Superscript, // Specifies text smaller than normal and lowered below the normal baseline. Subscript } /// A change in character formatting. struct FormatChange { uint index; FormatType type; FormatData data; static FormatChange opCall(uint i, FormatType t, FormatData d) { FormatChange c; c.index = i; c.type = t; c.data = d; return c; } } /** * Returns true if data1 is equal to data2. */ bool formatDataEqual(FormatType type, FormatData data1, FormatData data2) { if(type == FormatType.FontFamily) return data1.family == data2.family; else if(type == FormatType.FontSize) return data1.size == data2.size; else if(type == FormatType.Bold || type == FormatType.Italic) return data1.on == data2.on; else if(type == FormatType.Underline || type == FormatType.Strikethrough || type == FormatType.Overline) return data1.style == data2.style; else if(type == FormatType.Small) return data1.type == data2.type; else if(type == FormatType.ForeColor || type == FormatType.BackColor) return data1.color == data2.color; else if(type == FormatType.Spacing) return data1.multiple == data2.multiple; else throw new Exception("unknown type"); } /// enum FormatType : ubyte { /// FontFamily = 1, /// FontSize, /// Bold, /// Italic, /// Underline, /// Strikethrough, /// Overline, /// Small, /// ForeColor, /// BackColor, /// Spacing } /// union FormatData { /// Valid for FontFamily string family; /// Valid for FontSize double size; /// Valid for Bold and Italic bool on; /// Valid for Underline, Strikethrough, and Overline LineStyle style; /// Valid for Small SmallType type; /// Valid for ForeColor and BackColor Color color; /// Valid for Spacing double multiple; } struct Format { string fontFamily; // no default double fontSize; // no default bool bold = false; bool italic = false; LineStyle underline = LineStyle.None; LineStyle strikethrough = LineStyle.None; LineStyle overline = LineStyle.None; SmallType small = SmallType.Normal; Color foreColor = Color(255, 0, 0, 0); // black Color backColor = Color( 0, 0, 0, 0); // transparent double spacing = 1.0; FormatData getDataForType(FormatType type) { FormatData data; if(type == FormatType.FontFamily) data.family = fontFamily; else if(type == FormatType.FontSize) data.size = fontSize; else if(type == FormatType.Bold) data.on = bold; else if(type == FormatType.Italic) data.on = italic; else if(type == FormatType.Underline) data.style = underline; else if(type == FormatType.Strikethrough) data.style = strikethrough; else if(type == FormatType.Overline) data.style = overline; else if(type == FormatType.Small) data.type = small; else if(type == FormatType.ForeColor) data.color = foreColor; else if(type == FormatType.BackColor) data.color = backColor; else if(type == FormatType.Spacing) data.multiple = spacing; else throw new Exception("unknown type"); return data; } void setDataForType(FormatType type, FormatData data) { if(type == FormatType.FontFamily) fontFamily = data.family; else if(type == FormatType.FontSize) fontSize = data.size; else if(type == FormatType.Bold) bold = data.on; else if(type == FormatType.Italic) italic = data.on; else if(type == FormatType.Underline) underline = data.style; else if(type == FormatType.Strikethrough) strikethrough = data.style; else if(type == FormatType.Overline) overline = data.style; else if(type == FormatType.Small) small = data.type; else if(type == FormatType.ForeColor) foreColor = data.color; else if(type == FormatType.BackColor) backColor = data.color; else if(type == FormatType.Spacing) spacing = data.multiple; else throw new Exception("unknown type"); } } //}}} /// enum TextAlignment { /// Left, /// Center, /// Right, /// Justify } /// enum TabStopType { /// Left, /// Center, /// Right, /// Decimal } /// struct TabStop { /// uint location; /// TabStopType type = TabStopType.Left; /// char leading = '.'; } /** * */ class TextLayout { string text; // character formatting List!(FormatChange) formatting; // Always sorted by FormatChange.index Format initialFormat; // paragraph formatting double lineSpacing = 1.0; double defaultTabStopLocations = 0; // 0 means default tabs every (4 * width of character '0') TabStop[] tabStops; TextAlignment alignment = TextAlignment.Left; this(string fontFamily, double fontSize) { formatting = new List!(FormatChange); initialFormat.fontFamily = fontFamily; initialFormat.fontSize = fontSize; } invariant { // ensure that formatting is sorted correctly uint index = 0; foreach(change; formatting) { assert(change.index >= index); index = change.index; } } /// struct FormatRunsIter { private: TextLayout owner; FormatType[] filter; int delegate(uint index) splitter; public: /// int opApply(int delegate(ref uint start, ref uint length, ref Format format) dg) { bool inFilter(FormatType type) { return filter.length == 0 || filter.containsT(type); } with(owner) { int result; uint fIndex = 0; // index of formatting array uint sIndex = 0; // index passed to splitter Format format = initialFormat; uint start = 0; uint end; while(start != text.length) { end = text.length; // stop looping when one is found that is greater than start and in the filter while(fIndex < formatting.count && (formatting[fIndex].index <= start || !inFilter(formatting[fIndex].type))) { // the only ones that are greater are ones skipped due to filter assert(formatting[fIndex].index >= start); format.setDataForType(formatting[fIndex].type, formatting[fIndex].data); fIndex++; } if(fIndex < formatting.count) end = formatting[fIndex].index; if(splitter) { while(splitter(sIndex) != -1 && splitter(sIndex) <= start) sIndex++; if(splitter(sIndex) != -1 && splitter(sIndex) < end) end = splitter(sIndex); } if(end == start) end = text.length; uint _start = start; uint _length = end - start; result = dg(_start, _length, format); if(result) break; start = end; } return result; } } } /** * Returns an iterator struct that can be used with foreach. The struct will iterate over * each range of the text that has the same formatting. * * The difference between formatRuns and fontFormatRuns is that fontFormatRuns ignores * all formatting except FontFamily, FontSize, Bold, Italic, and Small, whereas formatRuns * does not ignore any formatting. * * If the optional delegate is specified, it will be called with index = 0, then index = 1, * and so on. The delegate should return an index to split the ranges at. When there are no * more indexes to split the ranges at, the delegate should return -1. It is a way of * giving an array of indexes to split at without allocating an array. * * Example: * ----- * foreach(start, length, format; textLayout.formatRuns) { * // code goes here * } * ----- */ FormatRunsIter formatRuns(int delegate(uint index) splitter = null) { FormatRunsIter iter; iter.owner = this; iter.splitter = splitter; return iter; } /// ditto FormatRunsIter fontFormatRuns(int delegate(uint index) splitter = null) { FormatRunsIter iter; iter.owner = this; iter.filter = [FormatType.FontFamily, FormatType.FontSize, FormatType.Bold, FormatType.Italic, FormatType.Small]; iter.splitter = splitter; return iter; } //{{{ character formatting void setFontFamily(string family, uint start, uint length) { FormatData data; data.family = family; setFormat(FormatType.FontFamily, data, start, length); } void setFontSize(double size, uint start, uint length) { FormatData data; data.size = size; setFormat(FormatType.FontSize, data, start, length); } void setBold(bool on, uint start, uint length) { FormatData data; data.on = on; setFormat(FormatType.Bold, data, start, length); } void setItalic(bool on, uint start, uint length) { FormatData data; data.on = on; setFormat(FormatType.Italic, data, start, length); } void setUnderline(LineStyle style, uint start, uint length) { FormatData data; data.style = style; setFormat(FormatType.Underline, data, start, length); } void setStrikethrough(LineStyle style, uint start, uint length) { FormatData data; data.style = style; setFormat(FormatType.Strikethrough, data, start, length); } void setOverline(LineStyle style, uint start, uint length) { FormatData data; data.style = style; setFormat(FormatType.Overline, data, start, length); } /// Sets the text either superscript or subscript. void setSmall(SmallType type, uint start, uint length) { FormatData data; data.type = type; setFormat(FormatType.Small, data, start, length); } // see http://en.wikipedia.org/wiki/Subscript_and_superscript#Desktop_publishing for // info on positioning superscript and subscript void setForeColor(Color color, uint start, uint length) { FormatData data; data.color = color; setFormat(FormatType.ForeColor, data, start, length); } void setBackColor(Color color, uint start, uint length) { FormatData data; data.color = color; setFormat(FormatType.BackColor, data, start, length); } /** * Sets the spacing between characters, given in multiples of a character's width. * For example, a multiple of 2.0 would make drawn text take twice as much space * horizontally, due to twice as much space being given to each character. */ void setSpacing(double multiple, uint start, uint length) { FormatData data; data.multiple = multiple; setFormat(FormatType.Spacing, data, start, length); } void setFormat(FormatType type, FormatData data, uint start, uint length) { uint end = start + length; checkIndex(start); checkIndex(end); FormatData endData = getFormat(type, end); for(int i = formatting.count-1; i >= 0; --i) { if(formatting[i].type == type && formatting[i].index <= end) { if(formatting[i].index >= start) formatting.removeAt(i); else break; } } if(!formatDataEqual(type, getFormat(type, start), data)) { insertFormatChange(FormatChange(start, type, data)); } // make sure that the formatting >= end stays the same as it was if(!formatDataEqual(type, endData, getFormat(type, end))) { insertFormatChange(FormatChange(end, type, endData)); } } FormatData getFormat(FormatType type, uint index) { checkIndex(index); FormatData data = initialFormat.getDataForType(type); for(int i = 0; i < formatting.count; ++i) { if(formatting[i].index > index) break; if(formatting[i].type == type) data = formatting[i].data; } return data; } private void insertFormatChange(FormatChange change) { int i = 0; while(i < formatting.count && formatting[i].index <= change.index) ++i; formatting.insert(change, i); } //}}} private void checkIndex(uint index) { if(index == 0) return; if(cropRight!(char)(text[0..index]).length != index) throw new Exception("index must be at a valid code point, not inside one"); } } unittest { auto t = new TextLayout("Tahoma", 15); t.text = "How are you doing today?"; t.setBold(true, 4, 3); // "are" assert(t.formatting.count == 2); t.setBold(true, 7, 4); // " you" assert(t.formatting.count == 2); t.setBold(true, 1, 3); // "ow " assert(t.formatting.count == 2); t.setBold(true, 8, 9); // "you doing" assert(t.formatting.count == 2); t.setBold(true, 0, 18); // "How are you doing " assert(t.formatting.count == 2); assert(t.formatting[0].type == FormatType.Bold); assert(t.formatting[0].data.on == true); assert(t.formatting[1].type == FormatType.Bold); assert(t.formatting[1].data.on == false); t.setBold(false, 0, 24); assert(t.formatting.count == 0); t.setBold(true, 4, 3); // "are" assert(t.formatting.count == 2); t.setBold(true, 8, 3); // "you" assert(t.formatting.count == 4); t.setBold(true, 1, 16); // "ow are you doing" assert(t.formatting.count == 2); t.setBold(false, 4, 8); // "are you " assert(t.formatting.count == 4); assert(t.formatting[0].index == 1); assert(t.formatting[0].data.on == true); assert(t.formatting[1].index == 4); assert(t.formatting[1].data.on == false); assert(t.formatting[2].index == 12); assert(t.formatting[2].data.on == true); assert(t.formatting[3].index == 17); assert(t.formatting[3].data.on == false); t.setBold(false, 1, 3); // "ow " assert(t.formatting.count == 2); t.setUnderline(LineStyle.Double, 8, 9); // "you doing" assert(t.formatting.count == 4); t.setBold(false, 0, 20); // "How are you doing t" assert(t.formatting.count == 2); assert(t.formatting[0].type == FormatType.Underline); assert(t.formatting[1].type == FormatType.Underline); t.setUnderline(LineStyle.Single, 12, 11); // "doing today" assert(t.formatting.count == 3); assert(t.formatting[0].data.style == LineStyle.Double); assert(t.formatting[1].data.style == LineStyle.Single); assert(t.formatting[2].data.style == LineStyle.None); t.setUnderline(LineStyle.None, 4, 14); // "are you doing " assert(t.formatting.count == 2); assert(t.formatting[0].data.style == LineStyle.Single); assert(t.formatting[0].index == 18); t.setUnderline(LineStyle.None, 4, 20); // "are you doing today?" assert(t.formatting.count == 0); } unittest { auto t = new TextLayout("Arial", 14); t.text = "The computer is black."; int[][] runs = [[0, 22]]; int delegate(uint index) splitter; void checkRuns() { int i = 0; foreach(s, l, f; t.formatRuns(splitter)) { assert(runs[i][0] == s && runs[i][1] == l); i++; } } // no formatting or splitter checkRuns(); // with a splitter but no formatting splitter = delegate int(uint i) { return [0, -1][i]; }; checkRuns(); splitter = delegate int(uint i) { return [22, -1][i]; }; checkRuns(); splitter = delegate int(uint i) { return [10, 15, -1][i]; }; runs = [[0, 10], [10, 5], [15, 7]]; checkRuns(); splitter = null; // with formatting but no splitter t.setFontFamily("Tahoma", 4, 8); // "computer" runs = [[0, 4], [4, 8], [12, 10]]; checkRuns(); t.setUnderline(LineStyle.Single, 12, 3); // " is" runs = [[0, 4], [4, 8], [12, 3], [15,7]]; checkRuns(); // test two FormatChanges at the same index t.setUnderline(LineStyle.Double, 0, 22); runs = [[0, 4], [4, 8], [12, 10]]; checkRuns(); // test a FormatChange at beginning and at end splitter = delegate int(uint i) { return [2, 4, 21, -1][i]; }; runs = [[0, 2], [2, 2], [4, 8], [12, 9], [21, 1]]; checkRuns(); // test a split in middle of a format run and at the same index as a format run // TODO: test fontFormatRuns }