comparison 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
comparison
equal deleted inserted replaced
95:592f7aa40bf1 96:301e077da540
13 * License. 13 * License.
14 * 14 *
15 * The Original Code is the Dynamin library. 15 * The Original Code is the Dynamin library.
16 * 16 *
17 * The Initial Developer of the Original Code is Jordan Miner. 17 * The Initial Developer of the Original Code is Jordan Miner.
18 * Portions created by the Initial Developer are Copyright (C) 2007-2010 18 * Portions created by the Initial Developer are Copyright (C) 2007-2012
19 * the Initial Developer. All Rights Reserved. 19 * the Initial Developer. All Rights Reserved.
20 * 20 *
21 * Contributor(s): 21 * Contributor(s):
22 * Jordan Miner <jminer7@gmail.com> 22 * Jordan Miner <jminer7@gmail.com>
23 * 23 *
24 */ 24 */
25 25
26 module dynamin.painting.text_layout; 26 module dynamin.painting.text_layout;
27 27
28 import dynamin.painting_backend;
28 import dynamin.core.list; 29 import dynamin.core.list;
29 import dynamin.core.string; 30 import dynamin.core.string;
30 import dynamin.painting.color; 31 import dynamin.painting.color;
32 import dynamin.painting.coordinates;
31 import tango.text.convert.Utf; 33 import tango.text.convert.Utf;
32 import tango.io.Stdout; 34 import tango.io.Stdout;
35
36 //version = TextLayoutDebug;
33 37
34 //{{{ character formatting types 38 //{{{ character formatting types
35 /// The line style of an underline, strikethrough, or overline. 39 /// The line style of an underline, strikethrough, or overline.
36 // TODO: add images of what these line styles look like 40 // TODO: add images of what these line styles look like
37 enum LineStyle { 41 enum LineStyle {
42 /// 46 ///
43 Double, 47 Double,
44 /// 48 ///
45 Dotted, 49 Dotted,
46 /// 50 ///
47 Dashed, 51 //Dashed,
48 /// 52 ///
49 Wavy 53 //Wavy
50 } 54 }
51 55
52 /// 56 ///
53 enum SmallType { 57 enum SmallType {
54 // Specifies normal text. 58 // Specifies normal text.
217 /// 221 ///
218 Center, 222 Center,
219 /// 223 ///
220 Right, 224 Right,
221 /// 225 ///
222 Justify 226 /// The last line of justified text will be natural aligned.
227 Justify,
228 /// Left aligned for left-to-right text and right aligned for right-to-left text.
229 Natural
223 } 230 }
224 231
225 /// 232 ///
226 enum TabStopType { 233 enum TabStopType {
227 /// 234 ///
242 TabStopType type = TabStopType.Left; 249 TabStopType type = TabStopType.Left;
243 /// 250 ///
244 char leading = '.'; 251 char leading = '.';
245 } 252 }
246 253
254 struct Run {
255 cairo_scaled_font_t* font;
256 float baseline; // distance from position.y down to the baseline
257 float height;
258 Point position; // the top-left corner of the run
259 uint start; // the first UTF-8 code unit in this run
260 uint length; // the number of UTF-8 code units in this run
261 bool rightToLeft;
262 uint[] clusterMap; // map from UTF-8 code units to the beginning of the glyph cluster they're in
263 uint[] glyphs; // glyphs are in visual order
264 float[] advanceWidths;
265
266 uint end() { return start + length; }
267 float width() {
268 float sum = 0;
269 foreach(w; advanceWidths)
270 sum += w;
271 return sum;
272 }
273 }
274 struct LogAttr {
275 private ubyte data;
276 // true if the line can be broken at this index
277 bool softBreak() {
278 return cast(bool)(data & 1);
279 }
280 void softBreak(bool b) {
281 b ? (data |= 1) : (data &= ~1);
282 }
283 // true if the caret can be placed at this index
284 bool clusterStart() {
285 return cast(bool)(data & 2);
286 }
287 void clusterStart(bool b) {
288 b ? (data |= 2) : (data &= ~2);
289 }
290 // true if the caret can be placed at this index when moving to the beginning of a word
291 bool wordStart() {
292 return cast(bool)(data & 4);
293 }
294 void wordStart(bool b) {
295 b ? (data |= 4) : (data &= ~4);
296 }
297 }
247 /** 298 /**
248 * 299 * Normally, at least for situations when paragraphs need to be formatted differently (e.g. one
300 * centered and another justified), a separate TextLayout object is used for each paragraph.
249 */ 301 */
250 class TextLayout { 302 class TextLayout {
303 private:
304 mixin TextLayoutBackend;
305 public:
251 string text; 306 string text;
252 Rect[] layoutBoxes; 307 Rect[] layoutBoxes;
253 /// 308 ///
254 void delegate(Rect line, List!(Rect) boxes) getLineBoxes; 309 void delegate(Rect line, List!(Rect) boxes) getLineBoxes;
255 310
259 314
260 // paragraph formatting 315 // paragraph formatting
261 double lineSpacing = 1.0; 316 double lineSpacing = 1.0;
262 double defaultTabStopLocations = 0; // 0 means default tabs every (4 * width of character '0') 317 double defaultTabStopLocations = 0; // 0 means default tabs every (4 * width of character '0')
263 TabStop[] tabStops; 318 TabStop[] tabStops;
264 TextAlignment alignment = TextAlignment.Left; 319 TextAlignment alignment = TextAlignment.Natural;
265 320
321 List!(Run) runs; // runs are in logical order
322 LogAttr[] logAttrs;
323 void clearRuns() {
324 while(runs.count > 0)
325 releaseScaledFont(runs.pop().font);
326 }
266 void defaultGetLineBoxes(Rect line, List!(Rect) boxes) { 327 void defaultGetLineBoxes(Rect line, List!(Rect) boxes) {
267 boxes.add(line); 328 boxes.add(line);
268 } 329 }
269 this(string fontFamily, double fontSize) { 330 this(string fontFamily, double fontSize) {
270 getLineBoxes = &defaultGetLineBoxes; 331 getLineBoxes = &defaultGetLineBoxes;
332 runs = new List!(Run);
333 if(!runLists)
334 runLists = new List!(List!(Run));
335 runLists.add(runs); // to keep the runs list around until the destructor is called
336
271 formatting = new List!(FormatChange); 337 formatting = new List!(FormatChange);
272 initialFormat.fontFamily = fontFamily; 338 initialFormat.fontFamily = fontFamily;
273 initialFormat.fontSize = fontSize; 339 initialFormat.fontSize = fontSize;
274 } 340 }
275 invariant { 341 ~this() {
342 clearRuns(); // have to call releaseScaledFont() so unused ones don't stay in font cache
343 runLists.remove(runs);
344 }
345 /*invariant { // TODO: uncomment this when D no longer calls it right before the destructor
276 // ensure that formatting is sorted correctly 346 // ensure that formatting is sorted correctly
277 uint index = 0; 347 uint index = 0;
278 foreach(change; formatting) { 348 foreach(change; formatting) {
279 assert(change.index >= index); 349 assert(change.index >= index);
280 index = change.index; 350 index = change.index;
281 } 351 }
352 }*/
353 // The first number is the index of the run to put at the left of the first line.
354 // The second number is the index of the run to put just right of that, and so on.
355 uint[] visToLogMap; // length == runs.count
356 // Narrow down the visual to logical map to just the logical runs between startRun and endRun.
357 // startRun is inclusive; endRun is exclusive
358 // The first number in the return map is index of the run (between startRun and
359 // endRun) that goes first visually. The second index goes just to right of it, and so on.
360 void getVisualToLogicalMap(uint startRun, uint endRun, uint[] map) {
361 assert(map.length == endRun - startRun);
362 uint mapIndex = 0;
363 for(int i = 0; i < visToLogMap.length; ++i) {
364 // we are basically removing all numbers from visToLogMap except ones >= start and < end
365 if(visToLogMap[i] >= startRun && visToLogMap[i] < endRun) {
366 map[mapIndex] = visToLogMap[i];
367 mapIndex++;
368 }
369 }
370 assert(mapIndex == map.length - 1);
371
372 // To use a logical to visual map to implement this function, you'd have to
373 // loop through the entire map and find the smallest index. Then put that in the
374 // return array. Then loop through and find the second smallest index, and put that in
375 // the return array. Repeat until it is filled.
376 }
377 // Splits this run into two runs. This Run is the second one, and the first run is returned.
378 void splitRun(uint runIndex, uint splitIndex) {
379 // TODO: need to update visToLogMap
380 // TODO: rename position to location?
381 version(TextLayoutDebug) {
382 Stdout.format("splitRun(): runIndex: {0}, text: {1}",
383 runIndex, text[runs[runIndex].start..runs[runIndex].end]).newline;
384 }
385 assert(splitIndex != runs[runIndex].start && splitIndex != runs[runIndex].end);
386 assert(logAttrs[splitIndex].clusterStart);
387
388 runs.insert(runs[runIndex], runIndex);
389
390 Run* run1 = &runs.data[runIndex];
391 auto glyphIndex = run1.clusterMap[splitIndex - run1.start];
392 run1.length = splitIndex - run1.start;
393 run1.glyphs = run1.glyphs[0..glyphIndex];
394 run1.advanceWidths = run1.advanceWidths[0..glyphIndex];
395 run1.clusterMap = run1.clusterMap[0..run1.length];
396 cairo_scaled_font_reference(run1.font);
397
398 Run* run2 = &runs.data[runIndex+1];
399 run2.length = (run2.start + run2.length) - splitIndex;
400 run2.start = splitIndex;
401 run2.glyphs = run2.glyphs[glyphIndex..$];
402 run2.advanceWidths = run2.advanceWidths[glyphIndex..$];
403 run2.clusterMap = run2.clusterMap[run1.length..$];
404 // need to change the cluster map to account for the glyphs removed
405 for(int i = 0; i < run2.clusterMap.length; ++i)
406 run2.clusterMap[i] -= glyphIndex;
407
408 backend_splitRun(runIndex, splitIndex);
282 } 409 }
283 410
284 /// 411 ///
285 struct FormatRunsIter { 412 struct FormatRunsIter {
286 private: 413 private:
375 FormatType.Small]; 502 FormatType.Small];
376 iter.splitter = splitter; 503 iter.splitter = splitter;
377 return iter; 504 return iter;
378 } 505 }
379 506
507 /**
508 *
509 */
510 TextAlignment naturalAlignment() {
511 return TextAlignment.Left; // TODO:
512 }
513 /**
514 *
515 */
516 TextAlignment resolvedAlignment() {
517 if(alignment == TextAlignment.Left || alignment == TextAlignment.Right)
518 return alignment;
519 else
520 return naturalAlignment;
521 }
522
523 /*
524 Point indexToPoint(uint index) {
525 }
526 uint pointToIndex(Point pt) {
527 }
528 uint nextLeft(uint index) {
529 }
530 uint nextRight(uint index) {
531 }
532 uint nextWordLeft(uint index) {
533 }
534 uint nextWordRight(uint index) {
535 }
536 */
537
380 //{{{ character formatting 538 //{{{ character formatting
381 void setFontFamily(string family, uint start, uint length) { 539 void setFontFamily(string family, uint start, uint length) {
382 FormatData data; 540 FormatData data;
383 data.family = family; 541 data.family = family;
384 setFormat(FormatType.FontFamily, data, start, length); 542 setFormat(FormatType.FontFamily, data, start, length);
487 while(i < formatting.count && formatting[i].index <= change.index) 645 while(i < formatting.count && formatting[i].index <= change.index)
488 ++i; 646 ++i;
489 formatting.insert(change, i); 647 formatting.insert(change, i);
490 } 648 }
491 //}}} 649 //}}}
650
492 private void checkIndex(uint index) { 651 private void checkIndex(uint index) {
493 if(index == 0) 652 if(index == 0)
494 return; 653 return;
495 if(cropRight!(char)(text[0..index]).length != index) 654 // NOTE: Do not use cropRight(). It is broken. It will cut off an ending code point even
655 // when it is a perfectly valid string. Thankfully, cropLeft() works.
656 if(cropLeft!(char)(text[index..$]).length != text.length-index)
496 throw new Exception("index must be at a valid code point, not inside one"); 657 throw new Exception("index must be at a valid code point, not inside one");
497 } 658 }
498 } 659
660 protected void layout(Graphics g) {
661 backend_preprocess(g);
662
663 version(TextLayoutDebug)
664 Stdout("-----layout start-----").newline;
665 int lastRunIndex = 0; // used in case of a bug
666 LayoutProgress progress;
667 lineLoop:
668 // loop once for each line until done laying out text
669 while(progress.runIndex < runs.count) {
670 // Try laying out the line at the height of the first run.
671 // If a taller run is found, try the line at that height, and so on
672 // If no taller run is found, or if laying out the line at the taller height didn't
673 // fit more characters on, then we've found the height that works best.
674 double baseline;
675 double height = 0; // height of the line
676 uint prevLength = 0; // how many chars fit on when the line is that tall
677 double heightToTry = runs[progress.runIndex].height;
678 while(true) {
679 baseline = 0;
680 double newHeightToTry = heightToTry;
681 uint length = layoutLine(newHeightToTry, baseline, progress, true);
682 if(length == 0) { // we ran out of layout boxes--no place to put text
683 while(runs.count > progress.runIndex)
684 runs.removeAt(runs.count-1);
685 break lineLoop;
686 }
687 version(TextLayoutDebug)
688 Stdout.format("^ length: {0}, runIndex: {1}, y: {2}", length, progress.runIndex, progress.y).newline;
689 if(length > prevLength) { // if more fit on at heightToTry than height
690 height = heightToTry;
691 prevLength = length;
692 if(newHeightToTry <= heightToTry) // if no need to try again
693 break;
694 heightToTry = newHeightToTry;
695 } else {
696 break;
697 }
698 }
699 // now that we have found the right height and baseline for the line,
700 // actually do the layout
701 layoutLine(height, baseline, progress, false);
702
703 version(TextLayoutDebug) {
704 Stdout.format("^^ rI: {0}, y: {1}", progress.runIndex, progress.y).newline;
705 if(runIndex == lastRunIndex)
706 Stdout("assert failed").newline;
707 }
708 assert(progress.runIndex > lastRunIndex);
709 // should never happen, but if there is a bug, it is better than an infinite loop
710 if(progress.runIndex == lastRunIndex)
711 break;
712 lastRunIndex = progress.runIndex;
713 }
714 // if wrapping around an object, a tab should go on the other side of the object
715 }
716
717 struct LayoutProgress {
718 int runIndex = 0;
719 int boxIndex = 0;
720 double y = 0; // y is relative to the top of the layout box
721 }
722
723 // {{{ layoutLine()
724 // Returns how many chars fit on the line when it is the specified height tall.
725 // When this method returns, height will have been set to a new height that layoutLine() can
726 // be called with.
727 // runIndex and totalY are only updated if dryRun is false
728 // totalY is the total height of text layed out before this
729 // note that this includes empty space at the bottom of a layout box where a line couldn't fit
730 List!(Rect) lineBoxes; // try to reuse the same memory for each call
731 uint layoutLine(ref double height, ref double baseline,
732 ref LayoutProgress progress, bool dryRun) {
733 // make local copies in case of dryRun
734 int boxIndex = progress.boxIndex;
735 double y = progress.y; // for now, y is relative to the top of the layout box
736 // if the line won't fit on, go to the top of the next box
737 while(y + height > layoutBoxes[boxIndex].height) {
738 boxIndex++;
739 if(boxIndex == layoutBoxes.length)
740 return 0;
741 y = 0;
742 }
743 Rect layoutBox = layoutBoxes[boxIndex];
744 if(!dryRun) {
745 progress.boxIndex = boxIndex;
746 progress.y = y + height; // top of line after this one
747 }
748 // change y to absolute
749 y += layoutBox.y;
750
751 /*
752 double top = totalY; // top will be the space from layoutBox.y to the top of the line
753 foreach(i, box; layoutBoxes) {
754 layoutBox = box; // use the last box if we never break
755 if(top >= box.height) {
756 top -= box.height;
757 if(i == layoutBoxes.length - 1)
758 return 0; // if we are out of layout boxes, there is no place to put text
759 } else if(top + height > box.height) {
760 // add on empty space at bottom of box
761 totalY += dryRun ? 0 : top + height - box.height;
762 top = 0; // loop to next box, then break
763 if(i == layoutBoxes.length - 1)
764 return 0; // if we are out of layout boxes, there is no place to put text
765 } else {
766 break;
767 }
768 }
769 totalY += dryRun ? 0 : height;*/
770
771 if(!lineBoxes)
772 lineBoxes = new List!(Rect);
773 lineBoxes.clear();
774 getLineBoxes(Rect(layoutBox.x, y, layoutBox.width, height), lineBoxes);
775
776
777 version(TextLayoutDebug) {
778 Stdout.format("layoutLine(): height: {0}, runIndex: {1}, dryRun: {2}, runs[rI]: {3}",
779 height, runIndex, dryRun,
780 text[runs[runIndex].start..runs[runIndex].end]).newline;
781 }
782 int totalWidth = 0;
783 foreach(box; lineBoxes)
784 totalWidth += box.width;
785 wordsLoop:
786 for(int words = getMaxWords(progress.runIndex, totalWidth); words >= 1; --words) {
787 // then for right-aligned, start with the last line box and last run, and work left
788 // for left-aligned, start with the first line box and first run, and work right
789
790 // loop over each glyph/char from left to right
791 //
792 int endRun, runSplit;
793 getRuns(words, progress.runIndex, endRun, runSplit);
794 version(TextLayoutDebug) {
795 Stdout.format(" words: {0}, endRun: {1}, runSplit: {2}",
796 words, endRun, runSplit).newline;
797 }
798 assert(runSplit > 0);
799
800 GlyphIter lastSoftBreak;
801 GlyphIter iter;
802 iter.runs = runs;
803 iter.startRun = progress.runIndex;
804 iter.endRun = endRun;
805 iter.endSplit = runSplit;
806 lastSoftBreak = iter;
807
808 int boxI = 0;
809
810 int lastRunIndex = iter.runIndex;
811 cairo_font_extents_t lastRunExtents;
812 lastRunExtents.ascent = lastRunExtents.descent = lastRunExtents.height = 0;
813
814 float x = lineBoxes[0].x;
815 while(iter.next()) {
816 if(iter.runIndex != lastRunIndex) {
817 // If this isn't a dry run, blindly trust the height and baseline.
818 // nothing we could do if they were wrong
819 if(!dryRun) {
820 iter.run.position = Point(x, y);
821 iter.run.baseline = baseline;
822 } else {
823 // if this new run is taller, return the taller height and baseline
824 cairo_font_extents_t extents;
825 cairo_scaled_font_extents(iter.run.font, &extents);
826 auto below = max(extents.height-extents.ascent,
827 lastRunExtents.height-lastRunExtents.ascent);
828 baseline = max(baseline, extents.ascent, lastRunExtents.ascent);
829 // floats aren't exact, so require a tenth of a pixel higher
830 if(baseline + below > height + 0.1) {
831 height = baseline + below;
832 return iter.charCount;
833 }
834 lastRunExtents = extents;
835 }
836 }
837 lastRunIndex = iter.runIndex;
838
839 if(logAttrs[iter.charIndex+iter.run.start].softBreak)
840 lastSoftBreak = iter;
841
842 x += iter.advanceWidth;
843 // we always have to put at least one word per line
844 if(x > lineBoxes[boxI].right && words > 1) {
845 version(TextLayoutDebug)
846 Stdout.format(" hit end of line box, boxI: {0}", boxI).newline;
847 boxI++;
848 if(boxI == lineBoxes.count) // we failed at getting all the text on
849 continue wordsLoop; // try again with one fewer word
850 x = lineBoxes[boxI].x;
851
852 if(!dryRun) {
853 splitRun(lastSoftBreak.runIndex, lastSoftBreak.charIndex+lastSoftBreak.run.start);
854 lastSoftBreak.endRun += 1;
855 }
856 iter = lastSoftBreak;
857 }
858 // if LTR, loop over clusterMap and logAttrs forward; if RTL, loop reverse
859 }
860 // getting to here means that we were successful in getting the text on
861
862
863 if(!dryRun) {
864 if(runSplit != 0 && runSplit != runs[iter.endRun-1].length) {
865 splitRun(iter.endRun-1, runSplit+runs[iter.endRun-1].start);
866 }
867 // now that we know for sure what runs are on the line, set their height
868 for(int i = progress.runIndex; i < iter.endRun; ++i)
869 runs.data[i].height = height;
870 progress.runIndex = iter.endRun;
871 }
872 return iter.charCount;
873
874 /*if(resolvedAlignment == TextAlignment.Left) {
875 } else if(resolvedAlignment == TextAlignment.Right) {
876 } else {
877 assert(false);
878 }*/
879 }
880 assert(false, "reached end of layoutLine()");
881 }
882 // }}}
883
884 // {{{ getMaxWords()
885 // returns the maximum number of words, starting at runIndex, that could fit in
886 // the specified width
887 int getMaxWords(int runIndex, float width) {
888 int start = runs[runIndex].start;
889
890 // find out how many glyphs will fit in the width
891 int glyphIndex;
892 both:
893 for(; runIndex < runs.count; ++runIndex) {
894 // have to go over runs and glyphs in logical order because all the characters on
895 // the first line are logically before all the runs on the second line, and so on.
896 glyphIndex = runs[runIndex].rightToLeft ? runs[runIndex].glyphs.length-1 : 0;
897 while(glyphIndex >= 0 && glyphIndex < runs[runIndex].glyphs.length) {
898 width -= runs[runIndex].advanceWidths[glyphIndex];
899 if(width < 0)
900 break both;
901 glyphIndex += runs[runIndex].rightToLeft ? -1 : 1;
902 }
903 }
904 if(runIndex == runs.count)
905 runIndex--;
906
907 // find which character goes with the last glyph
908 int charIndex = 0;
909 while(charIndex < runs[runIndex].length) {
910 if(runs[runIndex].rightToLeft && runs[runIndex].clusterMap[charIndex] < glyphIndex)
911 break;
912 if(!runs[runIndex].rightToLeft && runs[runIndex].clusterMap[charIndex] > glyphIndex)
913 break;
914 charIndex++;
915 }
916 int end = charIndex + runs[runIndex].start;
917
918 // find out how many words are in the character range (and thus in the glyphs)
919 int words = 0;
920 for(int i = start; i < end; ++i) {
921 if(logAttrs[i].softBreak)
922 words++;
923 }
924 if(end == text.length || words == 0) // consider the end as the start of another word
925 words++;
926 return words;
927 }
928 // }}}
929
930 // {{{ struct GlyphIter
931 // TODO: need to loop using visualToLogicalOrder
932 struct GlyphIter {
933 List!(Run) runs;
934 void startRun(int index) { runIndex = index - 1; }
935 int endRun;
936 int endGlyphSplit; // the number of glyphs in the last run
937 // sets the number of characters in the last run
938 void endSplit(int split) {
939 assert(split <= runs[endRun-1].length);
940 if(split == runs[endRun-1].length)
941 endGlyphSplit = runs[endRun-1].glyphs.length;
942 else
943 endGlyphSplit = runs[endRun-1].clusterMap[split];
944 }
945
946 int runIndex = -1;
947 // usually the character that produced the current glyph (except when reordered)
948 int charIndex = 0; // counting from the start of the run
949 int glyphIndex = -1;
950 int charCount = 1; // charIndex starts at 0; it has already advanced to the first char
951
952 Run* run() { return &runs.data[runIndex]; }
953
954 // need to call once before getting the first glyph
955 // returns true if there is another valid glyph to use
956 // if false is returned, do not access any more glyphs or call next() again
957 bool next() {
958 assert(runIndex < endRun);
959 //Stdout("glyphIndex: ")(glyphIndex)(" runIndex: ")(runIndex).newline;
960
961 if(glyphIndex == -1 || glyphIndex == runs[runIndex].glyphs.length-1) {
962 runIndex++;
963 if(runIndex == endRun)
964 return false;
965 glyphIndex = 0;
966 charCount += run.rightToLeft ? charIndex : run.length-charIndex;
967 charIndex = run.rightToLeft ? run.length-1 : 0;
968 if(runIndex == endRun-1 && runs[runIndex].rightToLeft)
969 glyphIndex = endGlyphSplit-1;
970 } else {
971 glyphIndex++;
972 }
973 if(runIndex == endRun-1) {
974 if(!runs[runIndex].rightToLeft && glyphIndex == endGlyphSplit)
975 return false;
976 if(runs[runIndex].rightToLeft && glyphIndex == runs[runIndex].glyphs.length)
977 return false;
978 }
979
980 // advance charIndex, if needed
981 auto newChar = charIndex;
982 while(newChar >= 0 && newChar < run.length) {
983 // if we found the next cluster
984 if(run.clusterMap[newChar] != run.clusterMap[charIndex]) {
985 // if the next char produced a glyph after where we are, then stay put
986 if(run.clusterMap[newChar] > glyphIndex)
987 break;
988 charCount += newChar-charIndex > 0 ? newChar-charIndex : charIndex-newChar;
989 charIndex = newChar;
990 }
991 newChar += run.rightToLeft ? -1 : 1;
992 }
993
994 //Stdout(" *glyphIndex: ")(glyphIndex)(" runIndex: ")(runIndex).newline;
995 return true;
996 }
997 float advanceWidth() { return runs[runIndex].advanceWidths[glyphIndex]; }
998 }
999 // }}}
1000
1001 // words is the number of words the runs should contain
1002 // startRun is the index of the first run to count words for
1003 // endRun is the index of the last run plus 1 (the last run exclusive)
1004 // endRunSplit is how many characters in the last run it takes to get the specified word count
1005 void getRuns(int words, int startRun, out int lastRun, out int lastRunSplit) {
1006 assert(words >= 1); // TODO: change endRun to lastRun and make it inclusive
1007 lastRun = startRun;
1008 // add 1 to start with so that if a run begins with a word, it doesn't count
1009 for(int i = runs[startRun].start + 1; i < text.length; ++i) {
1010 if(runs[lastRun].end < i)
1011 lastRun++;
1012 if(logAttrs[i].softBreak) {
1013 words--;
1014 if(words == 0) {
1015 lastRunSplit = i - runs[lastRun].start;
1016 lastRun++; // TODO: hack
1017 return;
1018 }
1019 }
1020 }
1021 lastRun = runs.count - 1;
1022 lastRunSplit = runs[lastRun].length;
1023 lastRun++; //hack
1024 }
1025
1026 // {{{ draw()
1027 // TODO: make layout() protected
1028 // functions should call it automatically when needed
1029 List!(cairo_glyph_t) glyphs;
1030 void draw(Graphics g) { // TODO: take a layoutBoxIndex parameter to only draw one of them?
1031 layout(g); // TODO: fix to only call if needed
1032
1033 if(!glyphs)
1034 glyphs = new List!(cairo_glyph_t)(80);
1035
1036 /*
1037 * If runs are removed because not all the text will fit in the layout boxes,
1038 * then we need to split at the end of the last one in splitter(), and break out
1039 * of the loop when we reach the last run, since otherwise it goes to the end of
1040 * the text.
1041 */
1042 int splitter(uint i) {
1043 return i < runs.count ? runs[i].start : (i == runs.count ? runs[runs.count-1].end : -1);
1044 }
1045 int runIndex = 0;
1046 double x = runs[runIndex].position.x;
1047 foreach(start, length, format; formatRuns(&splitter)) {
1048 uint end = start + length;
1049
1050 if(runs[runIndex].end == start) {
1051 runIndex++;
1052 if(runIndex == runs.count) // happens if runs were removed because they didn't fit
1053 break;
1054 x = runs[runIndex].position.x;
1055 if(runs[runIndex].rightToLeft)
1056 x += runs[runIndex].width;
1057 }
1058 Run* r = &runs[runIndex];
1059 assert(r.end >= end);
1060
1061 cairo_matrix_t ctm;
1062 cairo_get_matrix(g.handle, &ctm);
1063 double x0 = ctm.x0, y0 = ctm.y0;
1064 cairo_scaled_font_get_ctm(r.font, &ctm);
1065 ctm.x0 = x0, ctm.y0 = y0;
1066 cairo_set_matrix(g.handle, &ctm);
1067 cairo_set_scaled_font(g.handle, r.font);
1068
1069 //Stdout(r.position)(" ")(text[start..end]).newline;
1070 // note: using 'length' in a slice doesn't mean the 'length' in scope--it means the
1071 // length of the array you are slicing...arg, wasted 15 minutes
1072
1073 // find glyphs to draw
1074 int glyphStart;
1075 int glyphEnd;
1076 // TODO: I believe this setting glyphStart/End works right, but can it be shortened?
1077 // besides the places the index equals the length, could just swap gStart and gEnd?
1078 if(r.rightToLeft) {
1079 if(start-r.start == 0)
1080 glyphEnd = r.glyphs.length;
1081 else
1082 glyphEnd = r.clusterMap[start-r.start];
1083 if(end-r.start == r.clusterMap.length)
1084 glyphStart = 0;
1085 else
1086 glyphStart = r.clusterMap[end-r.start];
1087 } else {
1088 glyphStart = r.clusterMap[start-r.start];
1089 if(end-r.start == r.clusterMap.length)
1090 glyphEnd = r.glyphs.length;
1091 else
1092 glyphEnd = r.clusterMap[end-r.start];
1093 }
1094 //if(r.rightToLeft)
1095 // Stdout(glyphStart)(" -- ")(glyphEnd).newline;
1096
1097 // draw backColor
1098 float width = 0;
1099 for(int i = glyphStart; i < glyphEnd; ++i)
1100 width += r.advanceWidths[i];
1101 Rect rect = Rect(r.rightToLeft ? x-width : x, r.position.y, width, r.height);
1102 g.source = format.backColor;
1103 g.rectangle(rect);
1104 g.fill();
1105
1106 // draw glyphs
1107 int j = r.rightToLeft ? glyphEnd-1 : glyphStart;
1108 while(j >= glyphStart && j < glyphEnd) {
1109 if(r.rightToLeft)
1110 x -= r.advanceWidths[j];
1111 cairo_glyph_t cairoGlyph;
1112 cairoGlyph.index = r.glyphs[j];
1113 cairoGlyph.x = x;
1114 cairoGlyph.y = r.position.y + r.baseline;
1115 //Stdout.format("x {0} y {1}", cairoGlyph.x, cairoGlyph.y).newline;
1116 glyphs.add(cairoGlyph);
1117 if(!r.rightToLeft)
1118 x += r.advanceWidths[j];
1119 r.rightToLeft ? j-- : j++;
1120 }
1121 //Stdout.format("start {0}, color: {1}", start, format.foreColor.B).newline;
1122 g.source = format.foreColor;
1123 cairo_show_glyphs(g.handle, glyphs.data.ptr, glyphs.count);
1124 glyphs.clear();
1125 }
1126 }
1127 // }}}
1128
1129 private:
1130 // {{{ font cache
1131 // Returns a scaled font matching the specified look. If the font is in the font cache
1132 // it will be returned from there. If it isn't in the cache, it will be created using a
1133 // backend specific function.
1134 // Note that the reference count on the scaled font is increased each time this function
1135 // is called. Therefore, releaseScaledFont() needs to be called once for each time
1136 // getScaledFont() is called.
1137 static cairo_scaled_font_t* getScaledFont(string family,
1138 double size,
1139 bool bold,
1140 bool italic,
1141 cairo_matrix_t ctm) {
1142 if(!fontCache)
1143 fontCache = new List!(FontCacheEntry);
1144 // set the translation to zero so that
1145 // 1. entryCtm == ctm can be used when searching through the cache, and it will still
1146 // ignore the translation
1147 // 2. the CTM of every scaled font in the font cache has a translation of zero
1148 ctm.x0 = 0;
1149 ctm.y0 = 0;
1150 foreach(entry; fontCache) {
1151 if(entry.family == family && entry.size == size &&
1152 entry.bold == bold && entry.italic == italic) {
1153 cairo_matrix_t entryCtm;
1154 cairo_scaled_font_get_ctm(entry.font, &entryCtm);
1155 // a font can only match another if they have the same CTM (except the
1156 // transformation can be different), so check that the CTMs are the same
1157 if(entryCtm == ctm) {
1158 cairo_scaled_font_reference(entry.font);
1159 return entry.font;
1160 }
1161 }
1162 }
1163
1164 // since a matching font wasn't found in the cache, create it and add it to the cache
1165 FontCacheEntry entry;
1166 entry.font = backend_createScaledFont(family, size, bold, italic, &ctm);
1167 entry.family = family;
1168 entry.size = size;
1169 entry.bold = bold;
1170 entry.italic = italic;
1171 fontCache.add(entry);
1172 return entry.font;
1173 }
1174 // Decreases the reference count of the scaled font and removes it from the cache when its
1175 // reference count reaches zero.
1176 static void releaseScaledFont(cairo_scaled_font_t* font) {
1177 if(cairo_scaled_font_get_reference_count(font) == 1) {
1178 bool found = false;
1179 foreach(i, entry; fontCache) {
1180 if(entry.font == font) {
1181 fontCache.removeAt(i);
1182 found = true;
1183 break;
1184 }
1185 }
1186 assert(found);
1187 }
1188 cairo_scaled_font_destroy(font);
1189 }
1190 //}}}
1191 }
1192
1193 struct FontCacheEntry {
1194 cairo_scaled_font_t* font;
1195 string family;
1196 double size;
1197 bool bold;
1198 bool italic;
1199 }
1200
1201 private List!(FontCacheEntry) fontCache; // global in D1, thread local in D2
1202
1203 // this is to keep the TextLayout.runs List around until the destructor is called
1204 List!(List!(Run)) runLists;
499 1205
500 unittest { 1206 unittest {
501 auto t = new TextLayout("Tahoma", 15); 1207 auto t = new TextLayout("Tahoma", 15);
502 t.text = "How are you doing today?"; 1208 t.text = "How are you doing today?";
503 t.setBold(true, 4, 3); // "are" 1209 t.setBold(true, 4, 3); // "are"
551 assert(t.formatting[0].data.style == LineStyle.Single); 1257 assert(t.formatting[0].data.style == LineStyle.Single);
552 assert(t.formatting[0].index == 18); 1258 assert(t.formatting[0].index == 18);
553 t.setUnderline(LineStyle.None, 4, 20); // "are you doing today?" 1259 t.setUnderline(LineStyle.None, 4, 20); // "are you doing today?"
554 assert(t.formatting.count == 0); 1260 assert(t.formatting.count == 0);
555 } 1261 }
1262
556 unittest { 1263 unittest {
557 auto t = new TextLayout("Arial", 14); 1264 auto t = new TextLayout("Arial", 14);
558 t.text = "The computer is black."; 1265 t.text = "The computer is black.";
559 int[][] runs = [[0, 22]]; 1266 int[][] runs = [[0, 22]];
560 int delegate(uint index) splitter; 1267 int delegate(uint index) splitter;
596 checkRuns(); // test a split in middle of a format run and at the same index as a format run 1303 checkRuns(); // test a split in middle of a format run and at the same index as a format run
597 1304
598 // TODO: test fontFormatRuns 1305 // TODO: test fontFormatRuns
599 } 1306 }
600 1307
1308 unittest {
1309 auto surf = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 1, 1);
1310 auto cr = cairo_create(surf);
1311 cairo_surface_destroy(surf);
1312 auto g = new Graphics(cr);
1313 cairo_destroy(cr);
1314
1315 // with 0 being the first line
1316 void assertLineRange(TextLayout t, int line, int start, int length) {
1317 float lastLineY;
1318 int startAct = -1, lengthAct = -1; // actual start and length of line
1319 foreach(run; t.runs) {
1320 if(run.position.y != lastLineY) {
1321 line--;
1322 if(line == -1) {
1323 startAct = run.start;
1324 } else if(line == -2) {
1325 lengthAct = run.start - startAct;
1326 break;
1327 }
1328 }
1329 lastLineY = run.position.y;
1330 }
1331 assert(startAct >= 0);
1332 if(lengthAct == -1)
1333 lengthAct = t.text.length - startAct;
1334 Stdout.format("s {0} l {1} s2 {2} l2 {3}", start, length, startAct, lengthAct).newline;
1335 assert(start == startAct && length == lengthAct);
1336 }
1337 void assertLinePosition(TextLayout t, int line, float x, float y) {
1338 float lastLineY;
1339 foreach(run; t.runs) {
1340 if(run.position.y != lastLineY) {
1341 line--;
1342 if(line == -1) // test the x of the first run on the line
1343 assert(run.position.x < x+0.01 && run.position.x > x-0.01);
1344 }
1345 if(line == -1) // and test the y of every run on the line
1346 assert(run.position.y < y+0.01 && run.position.y > y-0.01);
1347 else if(line < -1)
1348 break;
1349 lastLineY = run.position.y;
1350 }
1351 assert(line < 0); // assert that there were enough lines
1352 }
1353
1354 auto t = new TextLayout("Ahem", 10);
1355 t.text = "The quick brown fox jumps over the lazy dog.";
1356
1357 // Test that lines are moved down when text on them is larger than the beginning.
1358 // Test that the second line is not too tall, since no text on it is larger.
1359 // Test that having a one character run at the end is handled correctly. (doesn't crash)
1360 t.setFontSize(12, 15, 1); // " "
1361 t.setFontSize(13, 16, 4); // "fox "
1362 t.setFontSize( 8, 43, 1); // "."
1363 t.layoutBoxes = [Rect(40, 30, 225, 100)];
1364 //The quick brown fox /jumps over the lazy /dog./
1365 t.draw(g);
1366 assertLineRange(t, 0, 0, 20); // line 0 has first 20 chars
1367 assertLineRange(t, 1, 20, 20); // line 1 has next 20 chars
1368 assertLineRange(t, 2, 40, 4); // line 2 has next 4 chars
1369 assertLinePosition(t, 0, 40, 30);
1370 assertLinePosition(t, 1, 40, 43);
1371 assertLinePosition(t, 2, 40, 53);
1372
1373 // Test that when runs are cut off due to not fitting in layout boxes,
1374 // there is no assert failure in draw()
1375 t.layoutBoxes = [Rect(40, 30, 225, 24)];
1376 t.draw(g);
1377
1378 // Test that layout boxes work:
1379 // that lines are wrapped to the width of the layout box and
1380 // that they are positioned correctly vertically.
1381 t.setFontSize(10, 0, t.text.length);
1382 t.layoutBoxes = [Rect(20, 20, 170, 24), Rect(200, 1000, 60, 33)];
1383 //The quick brown /fox jumps over /the /lazy /dog./
1384 t.draw(g);
1385 assertLineRange(t, 0, 0, 16);
1386 assertLineRange(t, 1, 16, 15);
1387 assertLineRange(t, 2, 31, 4);
1388 assertLineRange(t, 3, 35, 5);
1389 assertLineRange(t, 4, 40, 4);
1390 assertLinePosition(t, 0, 20, 20);
1391 assertLinePosition(t, 1, 20, 30);
1392 assertLinePosition(t, 2, 200, 1000);
1393 assertLinePosition(t, 3, 200, 1010);
1394 assertLinePosition(t, 4, 200, 1020);
1395
1396 /*
1397 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
1398 test that having taller text on a line, then shorter text, then that the next line is the right height
1399 test that text is broken for line boxes
1400 test having bigger text near end of line, and when line is moved down, first line box is smaller, so text is broken sooner
1401 test that if text needs to be moved down and fewer chars fit on, that it is not moved down
1402 */
1403
1404 // Test that a whole word is put on a line, even when the layout box is not wide enough.
1405 // Test that there is no crash when the first word is wider than the layout box.
1406 t.text = "Somebodyforgottousespaces, oh, thisisbad, I say.";
1407 t.layoutBoxes = [Rect(40, 30, 85, 50)];
1408 t.draw(g);
1409 assertLineRange(t, 0, 0, 27);
1410 assertLineRange(t, 1, 27, 4);
1411 assertLineRange(t, 2, 31, 11);
1412 assertLineRange(t, 3, 42, 6);
1413 assertLinePosition(t, 0, 40, 30);
1414 assertLinePosition(t, 1, 40, 40);
1415 assertLinePosition(t, 2, 40, 50);
1416 assertLinePosition(t, 3, 40, 60);
1417
1418 // write manual tests into showcase
1419 }
1420