Mercurial > projects > dynamin
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 |