Mercurial > projects > dynamin
view dynamin/painting/text_layout.d @ 96:301e077da540
Add the beginnings of a Windows Uniscribe text backend.
author | Jordan Miner <jminer7@gmail.com> |
---|---|
date | Wed, 02 May 2012 03:19:00 -0500 |
parents | 27445f24d5fd |
children | 604d20cac836 |
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-2012 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Jordan Miner <jminer7@gmail.com> * */ module dynamin.painting.text_layout; import dynamin.painting_backend; import dynamin.core.list; import dynamin.core.string; import dynamin.painting.color; import dynamin.painting.coordinates; import tango.text.convert.Utf; import tango.io.Stdout; //version = TextLayoutDebug; //{{{ 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, /// /// The last line of justified text will be natural aligned. Justify, /// Left aligned for left-to-right text and right aligned for right-to-left text. Natural } /// enum TabStopType { /// Left, /// Center, /// Right, /// Decimal } /// struct TabStop { /// uint location; /// TabStopType type = TabStopType.Left; /// char leading = '.'; } struct Run { cairo_scaled_font_t* font; float baseline; // distance from position.y down to the baseline float height; Point position; // the top-left corner of the run uint start; // the first UTF-8 code unit in this run uint length; // the number of UTF-8 code units in this run bool rightToLeft; uint[] clusterMap; // map from UTF-8 code units to the beginning of the glyph cluster they're in uint[] glyphs; // glyphs are in visual order float[] advanceWidths; uint end() { return start + length; } float width() { float sum = 0; foreach(w; advanceWidths) sum += w; return sum; } } struct LogAttr { private ubyte data; // true if the line can be broken at this index bool softBreak() { return cast(bool)(data & 1); } void softBreak(bool b) { b ? (data |= 1) : (data &= ~1); } // true if the caret can be placed at this index bool clusterStart() { return cast(bool)(data & 2); } void clusterStart(bool b) { b ? (data |= 2) : (data &= ~2); } // true if the caret can be placed at this index when moving to the beginning of a word bool wordStart() { return cast(bool)(data & 4); } void wordStart(bool b) { b ? (data |= 4) : (data &= ~4); } } /** * Normally, at least for situations when paragraphs need to be formatted differently (e.g. one * centered and another justified), a separate TextLayout object is used for each paragraph. */ class TextLayout { private: mixin TextLayoutBackend; public: string text; Rect[] layoutBoxes; /// void delegate(Rect line, List!(Rect) boxes) getLineBoxes; // 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.Natural; List!(Run) runs; // runs are in logical order LogAttr[] logAttrs; void clearRuns() { while(runs.count > 0) releaseScaledFont(runs.pop().font); } void defaultGetLineBoxes(Rect line, List!(Rect) boxes) { boxes.add(line); } this(string fontFamily, double fontSize) { getLineBoxes = &defaultGetLineBoxes; runs = new List!(Run); if(!runLists) runLists = new List!(List!(Run)); runLists.add(runs); // to keep the runs list around until the destructor is called formatting = new List!(FormatChange); initialFormat.fontFamily = fontFamily; initialFormat.fontSize = fontSize; } ~this() { clearRuns(); // have to call releaseScaledFont() so unused ones don't stay in font cache runLists.remove(runs); } /*invariant { // TODO: uncomment this when D no longer calls it right before the destructor // ensure that formatting is sorted correctly uint index = 0; foreach(change; formatting) { assert(change.index >= index); index = change.index; } }*/ // The first number is the index of the run to put at the left of the first line. // The second number is the index of the run to put just right of that, and so on. uint[] visToLogMap; // length == runs.count // Narrow down the visual to logical map to just the logical runs between startRun and endRun. // startRun is inclusive; endRun is exclusive // The first number in the return map is index of the run (between startRun and // endRun) that goes first visually. The second index goes just to right of it, and so on. void getVisualToLogicalMap(uint startRun, uint endRun, uint[] map) { assert(map.length == endRun - startRun); uint mapIndex = 0; for(int i = 0; i < visToLogMap.length; ++i) { // we are basically removing all numbers from visToLogMap except ones >= start and < end if(visToLogMap[i] >= startRun && visToLogMap[i] < endRun) { map[mapIndex] = visToLogMap[i]; mapIndex++; } } assert(mapIndex == map.length - 1); // To use a logical to visual map to implement this function, you'd have to // loop through the entire map and find the smallest index. Then put that in the // return array. Then loop through and find the second smallest index, and put that in // the return array. Repeat until it is filled. } // Splits this run into two runs. This Run is the second one, and the first run is returned. void splitRun(uint runIndex, uint splitIndex) { // TODO: need to update visToLogMap // TODO: rename position to location? version(TextLayoutDebug) { Stdout.format("splitRun(): runIndex: {0}, text: {1}", runIndex, text[runs[runIndex].start..runs[runIndex].end]).newline; } assert(splitIndex != runs[runIndex].start && splitIndex != runs[runIndex].end); assert(logAttrs[splitIndex].clusterStart); runs.insert(runs[runIndex], runIndex); Run* run1 = &runs.data[runIndex]; auto glyphIndex = run1.clusterMap[splitIndex - run1.start]; run1.length = splitIndex - run1.start; run1.glyphs = run1.glyphs[0..glyphIndex]; run1.advanceWidths = run1.advanceWidths[0..glyphIndex]; run1.clusterMap = run1.clusterMap[0..run1.length]; cairo_scaled_font_reference(run1.font); Run* run2 = &runs.data[runIndex+1]; run2.length = (run2.start + run2.length) - splitIndex; run2.start = splitIndex; run2.glyphs = run2.glyphs[glyphIndex..$]; run2.advanceWidths = run2.advanceWidths[glyphIndex..$]; run2.clusterMap = run2.clusterMap[run1.length..$]; // need to change the cluster map to account for the glyphs removed for(int i = 0; i < run2.clusterMap.length; ++i) run2.clusterMap[i] -= glyphIndex; backend_splitRun(runIndex, splitIndex); } /// 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; } /** * */ TextAlignment naturalAlignment() { return TextAlignment.Left; // TODO: } /** * */ TextAlignment resolvedAlignment() { if(alignment == TextAlignment.Left || alignment == TextAlignment.Right) return alignment; else return naturalAlignment; } /* Point indexToPoint(uint index) { } uint pointToIndex(Point pt) { } uint nextLeft(uint index) { } uint nextRight(uint index) { } uint nextWordLeft(uint index) { } uint nextWordRight(uint index) { } */ //{{{ 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; // NOTE: Do not use cropRight(). It is broken. It will cut off an ending code point even // when it is a perfectly valid string. Thankfully, cropLeft() works. if(cropLeft!(char)(text[index..$]).length != text.length-index) throw new Exception("index must be at a valid code point, not inside one"); } protected void layout(Graphics g) { backend_preprocess(g); version(TextLayoutDebug) Stdout("-----layout start-----").newline; int lastRunIndex = 0; // used in case of a bug LayoutProgress progress; lineLoop: // loop once for each line until done laying out text while(progress.runIndex < runs.count) { // Try laying out the line at the height of the first run. // If a taller run is found, try the line at that height, and so on // If no taller run is found, or if laying out the line at the taller height didn't // fit more characters on, then we've found the height that works best. double baseline; double height = 0; // height of the line uint prevLength = 0; // how many chars fit on when the line is that tall double heightToTry = runs[progress.runIndex].height; while(true) { baseline = 0; double newHeightToTry = heightToTry; uint length = layoutLine(newHeightToTry, baseline, progress, true); if(length == 0) { // we ran out of layout boxes--no place to put text while(runs.count > progress.runIndex) runs.removeAt(runs.count-1); break lineLoop; } version(TextLayoutDebug) Stdout.format("^ length: {0}, runIndex: {1}, y: {2}", length, progress.runIndex, progress.y).newline; if(length > prevLength) { // if more fit on at heightToTry than height height = heightToTry; prevLength = length; if(newHeightToTry <= heightToTry) // if no need to try again break; heightToTry = newHeightToTry; } else { break; } } // now that we have found the right height and baseline for the line, // actually do the layout layoutLine(height, baseline, progress, false); version(TextLayoutDebug) { Stdout.format("^^ rI: {0}, y: {1}", progress.runIndex, progress.y).newline; if(runIndex == lastRunIndex) Stdout("assert failed").newline; } assert(progress.runIndex > lastRunIndex); // should never happen, but if there is a bug, it is better than an infinite loop if(progress.runIndex == lastRunIndex) break; lastRunIndex = progress.runIndex; } // if wrapping around an object, a tab should go on the other side of the object } struct LayoutProgress { int runIndex = 0; int boxIndex = 0; double y = 0; // y is relative to the top of the layout box } // {{{ layoutLine() // Returns how many chars fit on the line when it is the specified height tall. // When this method returns, height will have been set to a new height that layoutLine() can // be called with. // runIndex and totalY are only updated if dryRun is false // totalY is the total height of text layed out before this // note that this includes empty space at the bottom of a layout box where a line couldn't fit List!(Rect) lineBoxes; // try to reuse the same memory for each call uint layoutLine(ref double height, ref double baseline, ref LayoutProgress progress, bool dryRun) { // make local copies in case of dryRun int boxIndex = progress.boxIndex; double y = progress.y; // for now, y is relative to the top of the layout box // if the line won't fit on, go to the top of the next box while(y + height > layoutBoxes[boxIndex].height) { boxIndex++; if(boxIndex == layoutBoxes.length) return 0; y = 0; } Rect layoutBox = layoutBoxes[boxIndex]; if(!dryRun) { progress.boxIndex = boxIndex; progress.y = y + height; // top of line after this one } // change y to absolute y += layoutBox.y; /* double top = totalY; // top will be the space from layoutBox.y to the top of the line foreach(i, box; layoutBoxes) { layoutBox = box; // use the last box if we never break if(top >= box.height) { top -= box.height; if(i == layoutBoxes.length - 1) return 0; // if we are out of layout boxes, there is no place to put text } else if(top + height > box.height) { // add on empty space at bottom of box totalY += dryRun ? 0 : top + height - box.height; top = 0; // loop to next box, then break if(i == layoutBoxes.length - 1) return 0; // if we are out of layout boxes, there is no place to put text } else { break; } } totalY += dryRun ? 0 : height;*/ if(!lineBoxes) lineBoxes = new List!(Rect); lineBoxes.clear(); getLineBoxes(Rect(layoutBox.x, y, layoutBox.width, height), lineBoxes); version(TextLayoutDebug) { Stdout.format("layoutLine(): height: {0}, runIndex: {1}, dryRun: {2}, runs[rI]: {3}", height, runIndex, dryRun, text[runs[runIndex].start..runs[runIndex].end]).newline; } int totalWidth = 0; foreach(box; lineBoxes) totalWidth += box.width; wordsLoop: for(int words = getMaxWords(progress.runIndex, totalWidth); words >= 1; --words) { // then for right-aligned, start with the last line box and last run, and work left // for left-aligned, start with the first line box and first run, and work right // loop over each glyph/char from left to right // int endRun, runSplit; getRuns(words, progress.runIndex, endRun, runSplit); version(TextLayoutDebug) { Stdout.format(" words: {0}, endRun: {1}, runSplit: {2}", words, endRun, runSplit).newline; } assert(runSplit > 0); GlyphIter lastSoftBreak; GlyphIter iter; iter.runs = runs; iter.startRun = progress.runIndex; iter.endRun = endRun; iter.endSplit = runSplit; lastSoftBreak = iter; int boxI = 0; int lastRunIndex = iter.runIndex; cairo_font_extents_t lastRunExtents; lastRunExtents.ascent = lastRunExtents.descent = lastRunExtents.height = 0; float x = lineBoxes[0].x; while(iter.next()) { if(iter.runIndex != lastRunIndex) { // If this isn't a dry run, blindly trust the height and baseline. // nothing we could do if they were wrong if(!dryRun) { iter.run.position = Point(x, y); iter.run.baseline = baseline; } else { // if this new run is taller, return the taller height and baseline cairo_font_extents_t extents; cairo_scaled_font_extents(iter.run.font, &extents); auto below = max(extents.height-extents.ascent, lastRunExtents.height-lastRunExtents.ascent); baseline = max(baseline, extents.ascent, lastRunExtents.ascent); // floats aren't exact, so require a tenth of a pixel higher if(baseline + below > height + 0.1) { height = baseline + below; return iter.charCount; } lastRunExtents = extents; } } lastRunIndex = iter.runIndex; if(logAttrs[iter.charIndex+iter.run.start].softBreak) lastSoftBreak = iter; x += iter.advanceWidth; // we always have to put at least one word per line if(x > lineBoxes[boxI].right && words > 1) { version(TextLayoutDebug) Stdout.format(" hit end of line box, boxI: {0}", boxI).newline; boxI++; if(boxI == lineBoxes.count) // we failed at getting all the text on continue wordsLoop; // try again with one fewer word x = lineBoxes[boxI].x; if(!dryRun) { splitRun(lastSoftBreak.runIndex, lastSoftBreak.charIndex+lastSoftBreak.run.start); lastSoftBreak.endRun += 1; } iter = lastSoftBreak; } // if LTR, loop over clusterMap and logAttrs forward; if RTL, loop reverse } // getting to here means that we were successful in getting the text on if(!dryRun) { if(runSplit != 0 && runSplit != runs[iter.endRun-1].length) { splitRun(iter.endRun-1, runSplit+runs[iter.endRun-1].start); } // now that we know for sure what runs are on the line, set their height for(int i = progress.runIndex; i < iter.endRun; ++i) runs.data[i].height = height; progress.runIndex = iter.endRun; } return iter.charCount; /*if(resolvedAlignment == TextAlignment.Left) { } else if(resolvedAlignment == TextAlignment.Right) { } else { assert(false); }*/ } assert(false, "reached end of layoutLine()"); } // }}} // {{{ getMaxWords() // returns the maximum number of words, starting at runIndex, that could fit in // the specified width int getMaxWords(int runIndex, float width) { int start = runs[runIndex].start; // find out how many glyphs will fit in the width int glyphIndex; both: for(; runIndex < runs.count; ++runIndex) { // have to go over runs and glyphs in logical order because all the characters on // the first line are logically before all the runs on the second line, and so on. glyphIndex = runs[runIndex].rightToLeft ? runs[runIndex].glyphs.length-1 : 0; while(glyphIndex >= 0 && glyphIndex < runs[runIndex].glyphs.length) { width -= runs[runIndex].advanceWidths[glyphIndex]; if(width < 0) break both; glyphIndex += runs[runIndex].rightToLeft ? -1 : 1; } } if(runIndex == runs.count) runIndex--; // find which character goes with the last glyph int charIndex = 0; while(charIndex < runs[runIndex].length) { if(runs[runIndex].rightToLeft && runs[runIndex].clusterMap[charIndex] < glyphIndex) break; if(!runs[runIndex].rightToLeft && runs[runIndex].clusterMap[charIndex] > glyphIndex) break; charIndex++; } int end = charIndex + runs[runIndex].start; // find out how many words are in the character range (and thus in the glyphs) int words = 0; for(int i = start; i < end; ++i) { if(logAttrs[i].softBreak) words++; } if(end == text.length || words == 0) // consider the end as the start of another word words++; return words; } // }}} // {{{ struct GlyphIter // TODO: need to loop using visualToLogicalOrder struct GlyphIter { List!(Run) runs; void startRun(int index) { runIndex = index - 1; } int endRun; int endGlyphSplit; // the number of glyphs in the last run // sets the number of characters in the last run void endSplit(int split) { assert(split <= runs[endRun-1].length); if(split == runs[endRun-1].length) endGlyphSplit = runs[endRun-1].glyphs.length; else endGlyphSplit = runs[endRun-1].clusterMap[split]; } int runIndex = -1; // usually the character that produced the current glyph (except when reordered) int charIndex = 0; // counting from the start of the run int glyphIndex = -1; int charCount = 1; // charIndex starts at 0; it has already advanced to the first char Run* run() { return &runs.data[runIndex]; } // need to call once before getting the first glyph // returns true if there is another valid glyph to use // if false is returned, do not access any more glyphs or call next() again bool next() { assert(runIndex < endRun); //Stdout("glyphIndex: ")(glyphIndex)(" runIndex: ")(runIndex).newline; if(glyphIndex == -1 || glyphIndex == runs[runIndex].glyphs.length-1) { runIndex++; if(runIndex == endRun) return false; glyphIndex = 0; charCount += run.rightToLeft ? charIndex : run.length-charIndex; charIndex = run.rightToLeft ? run.length-1 : 0; if(runIndex == endRun-1 && runs[runIndex].rightToLeft) glyphIndex = endGlyphSplit-1; } else { glyphIndex++; } if(runIndex == endRun-1) { if(!runs[runIndex].rightToLeft && glyphIndex == endGlyphSplit) return false; if(runs[runIndex].rightToLeft && glyphIndex == runs[runIndex].glyphs.length) return false; } // advance charIndex, if needed auto newChar = charIndex; while(newChar >= 0 && newChar < run.length) { // if we found the next cluster if(run.clusterMap[newChar] != run.clusterMap[charIndex]) { // if the next char produced a glyph after where we are, then stay put if(run.clusterMap[newChar] > glyphIndex) break; charCount += newChar-charIndex > 0 ? newChar-charIndex : charIndex-newChar; charIndex = newChar; } newChar += run.rightToLeft ? -1 : 1; } //Stdout(" *glyphIndex: ")(glyphIndex)(" runIndex: ")(runIndex).newline; return true; } float advanceWidth() { return runs[runIndex].advanceWidths[glyphIndex]; } } // }}} // words is the number of words the runs should contain // startRun is the index of the first run to count words for // endRun is the index of the last run plus 1 (the last run exclusive) // endRunSplit is how many characters in the last run it takes to get the specified word count void getRuns(int words, int startRun, out int lastRun, out int lastRunSplit) { assert(words >= 1); // TODO: change endRun to lastRun and make it inclusive lastRun = startRun; // add 1 to start with so that if a run begins with a word, it doesn't count for(int i = runs[startRun].start + 1; i < text.length; ++i) { if(runs[lastRun].end < i) lastRun++; if(logAttrs[i].softBreak) { words--; if(words == 0) { lastRunSplit = i - runs[lastRun].start; lastRun++; // TODO: hack return; } } } lastRun = runs.count - 1; lastRunSplit = runs[lastRun].length; lastRun++; //hack } // {{{ draw() // TODO: make layout() protected // functions should call it automatically when needed List!(cairo_glyph_t) glyphs; void draw(Graphics g) { // TODO: take a layoutBoxIndex parameter to only draw one of them? layout(g); // TODO: fix to only call if needed if(!glyphs) glyphs = new List!(cairo_glyph_t)(80); /* * If runs are removed because not all the text will fit in the layout boxes, * then we need to split at the end of the last one in splitter(), and break out * of the loop when we reach the last run, since otherwise it goes to the end of * the text. */ int splitter(uint i) { return i < runs.count ? runs[i].start : (i == runs.count ? runs[runs.count-1].end : -1); } int runIndex = 0; double x = runs[runIndex].position.x; foreach(start, length, format; formatRuns(&splitter)) { uint end = start + length; if(runs[runIndex].end == start) { runIndex++; if(runIndex == runs.count) // happens if runs were removed because they didn't fit break; x = runs[runIndex].position.x; if(runs[runIndex].rightToLeft) x += runs[runIndex].width; } Run* r = &runs[runIndex]; assert(r.end >= end); cairo_matrix_t ctm; cairo_get_matrix(g.handle, &ctm); double x0 = ctm.x0, y0 = ctm.y0; cairo_scaled_font_get_ctm(r.font, &ctm); ctm.x0 = x0, ctm.y0 = y0; cairo_set_matrix(g.handle, &ctm); cairo_set_scaled_font(g.handle, r.font); //Stdout(r.position)(" ")(text[start..end]).newline; // note: using 'length' in a slice doesn't mean the 'length' in scope--it means the // length of the array you are slicing...arg, wasted 15 minutes // find glyphs to draw int glyphStart; int glyphEnd; // TODO: I believe this setting glyphStart/End works right, but can it be shortened? // besides the places the index equals the length, could just swap gStart and gEnd? if(r.rightToLeft) { if(start-r.start == 0) glyphEnd = r.glyphs.length; else glyphEnd = r.clusterMap[start-r.start]; if(end-r.start == r.clusterMap.length) glyphStart = 0; else glyphStart = r.clusterMap[end-r.start]; } else { glyphStart = r.clusterMap[start-r.start]; if(end-r.start == r.clusterMap.length) glyphEnd = r.glyphs.length; else glyphEnd = r.clusterMap[end-r.start]; } //if(r.rightToLeft) // Stdout(glyphStart)(" -- ")(glyphEnd).newline; // draw backColor float width = 0; for(int i = glyphStart; i < glyphEnd; ++i) width += r.advanceWidths[i]; Rect rect = Rect(r.rightToLeft ? x-width : x, r.position.y, width, r.height); g.source = format.backColor; g.rectangle(rect); g.fill(); // draw glyphs int j = r.rightToLeft ? glyphEnd-1 : glyphStart; while(j >= glyphStart && j < glyphEnd) { if(r.rightToLeft) x -= r.advanceWidths[j]; cairo_glyph_t cairoGlyph; cairoGlyph.index = r.glyphs[j]; cairoGlyph.x = x; cairoGlyph.y = r.position.y + r.baseline; //Stdout.format("x {0} y {1}", cairoGlyph.x, cairoGlyph.y).newline; glyphs.add(cairoGlyph); if(!r.rightToLeft) x += r.advanceWidths[j]; r.rightToLeft ? j-- : j++; } //Stdout.format("start {0}, color: {1}", start, format.foreColor.B).newline; g.source = format.foreColor; cairo_show_glyphs(g.handle, glyphs.data.ptr, glyphs.count); glyphs.clear(); } } // }}} private: // {{{ font cache // Returns a scaled font matching the specified look. If the font is in the font cache // it will be returned from there. If it isn't in the cache, it will be created using a // backend specific function. // Note that the reference count on the scaled font is increased each time this function // is called. Therefore, releaseScaledFont() needs to be called once for each time // getScaledFont() is called. static cairo_scaled_font_t* getScaledFont(string family, double size, bool bold, bool italic, cairo_matrix_t ctm) { if(!fontCache) fontCache = new List!(FontCacheEntry); // set the translation to zero so that // 1. entryCtm == ctm can be used when searching through the cache, and it will still // ignore the translation // 2. the CTM of every scaled font in the font cache has a translation of zero ctm.x0 = 0; ctm.y0 = 0; foreach(entry; fontCache) { if(entry.family == family && entry.size == size && entry.bold == bold && entry.italic == italic) { cairo_matrix_t entryCtm; cairo_scaled_font_get_ctm(entry.font, &entryCtm); // a font can only match another if they have the same CTM (except the // transformation can be different), so check that the CTMs are the same if(entryCtm == ctm) { cairo_scaled_font_reference(entry.font); return entry.font; } } } // since a matching font wasn't found in the cache, create it and add it to the cache FontCacheEntry entry; entry.font = backend_createScaledFont(family, size, bold, italic, &ctm); entry.family = family; entry.size = size; entry.bold = bold; entry.italic = italic; fontCache.add(entry); return entry.font; } // Decreases the reference count of the scaled font and removes it from the cache when its // reference count reaches zero. static void releaseScaledFont(cairo_scaled_font_t* font) { if(cairo_scaled_font_get_reference_count(font) == 1) { bool found = false; foreach(i, entry; fontCache) { if(entry.font == font) { fontCache.removeAt(i); found = true; break; } } assert(found); } cairo_scaled_font_destroy(font); } //}}} } struct FontCacheEntry { cairo_scaled_font_t* font; string family; double size; bool bold; bool italic; } private List!(FontCacheEntry) fontCache; // global in D1, thread local in D2 // this is to keep the TextLayout.runs List around until the destructor is called List!(List!(Run)) runLists; 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 } unittest { auto surf = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 1, 1); auto cr = cairo_create(surf); cairo_surface_destroy(surf); auto g = new Graphics(cr); cairo_destroy(cr); // with 0 being the first line void assertLineRange(TextLayout t, int line, int start, int length) { float lastLineY; int startAct = -1, lengthAct = -1; // actual start and length of line foreach(run; t.runs) { if(run.position.y != lastLineY) { line--; if(line == -1) { startAct = run.start; } else if(line == -2) { lengthAct = run.start - startAct; break; } } lastLineY = run.position.y; } assert(startAct >= 0); if(lengthAct == -1) lengthAct = t.text.length - startAct; Stdout.format("s {0} l {1} s2 {2} l2 {3}", start, length, startAct, lengthAct).newline; assert(start == startAct && length == lengthAct); } void assertLinePosition(TextLayout t, int line, float x, float y) { float lastLineY; foreach(run; t.runs) { if(run.position.y != lastLineY) { line--; if(line == -1) // test the x of the first run on the line assert(run.position.x < x+0.01 && run.position.x > x-0.01); } if(line == -1) // and test the y of every run on the line assert(run.position.y < y+0.01 && run.position.y > y-0.01); else if(line < -1) break; lastLineY = run.position.y; } assert(line < 0); // assert that there were enough lines } auto t = new TextLayout("Ahem", 10); t.text = "The quick brown fox jumps over the lazy dog."; // Test that lines are moved down when text on them is larger than the beginning. // Test that the second line is not too tall, since no text on it is larger. // Test that having a one character run at the end is handled correctly. (doesn't crash) t.setFontSize(12, 15, 1); // " " t.setFontSize(13, 16, 4); // "fox " t.setFontSize( 8, 43, 1); // "." t.layoutBoxes = [Rect(40, 30, 225, 100)]; //The quick brown fox /jumps over the lazy /dog./ t.draw(g); assertLineRange(t, 0, 0, 20); // line 0 has first 20 chars assertLineRange(t, 1, 20, 20); // line 1 has next 20 chars assertLineRange(t, 2, 40, 4); // line 2 has next 4 chars assertLinePosition(t, 0, 40, 30); assertLinePosition(t, 1, 40, 43); assertLinePosition(t, 2, 40, 53); // Test that when runs are cut off due to not fitting in layout boxes, // there is no assert failure in draw() t.layoutBoxes = [Rect(40, 30, 225, 24)]; t.draw(g); // Test that layout boxes work: // that lines are wrapped to the width of the layout box and // that they are positioned correctly vertically. t.setFontSize(10, 0, t.text.length); t.layoutBoxes = [Rect(20, 20, 170, 24), Rect(200, 1000, 60, 33)]; //The quick brown /fox jumps over /the /lazy /dog./ t.draw(g); assertLineRange(t, 0, 0, 16); assertLineRange(t, 1, 16, 15); assertLineRange(t, 2, 31, 4); assertLineRange(t, 3, 35, 5); assertLineRange(t, 4, 40, 4); assertLinePosition(t, 0, 20, 20); assertLinePosition(t, 1, 20, 30); assertLinePosition(t, 2, 200, 1000); assertLinePosition(t, 3, 200, 1010); assertLinePosition(t, 4, 200, 1020); /* test that the height of a run on a line is not set to the height of the line (stays the height of the text test that having taller text on a line, then shorter text, then that the next line is the right height test that text is broken for line boxes test having bigger text near end of line, and when line is moved down, first line box is smaller, so text is broken sooner test that if text needs to be moved down and fewer chars fit on, that it is not moved down */ // Test that a whole word is put on a line, even when the layout box is not wide enough. // Test that there is no crash when the first word is wider than the layout box. t.text = "Somebodyforgottousespaces, oh, thisisbad, I say."; t.layoutBoxes = [Rect(40, 30, 85, 50)]; t.draw(g); assertLineRange(t, 0, 0, 27); assertLineRange(t, 1, 27, 4); assertLineRange(t, 2, 31, 11); assertLineRange(t, 3, 42, 6); assertLinePosition(t, 0, 40, 30); assertLinePosition(t, 1, 40, 40); assertLinePosition(t, 2, 40, 50); assertLinePosition(t, 3, 40, 60); // write manual tests into showcase }