yay! new layouter *almost* working
[xreader.git] / xlayouter.d
blobc5c95d45917b5fc4f4ee778013d565ef6bb087f5
1 /* Written by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
2 * Understanding is not required. Only obedience.
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 module xlayouter;
19 import core.time;
20 import std.stdio;
22 import arsd.color;
23 import arsd.png;
24 import arsd.jpeg;
26 import iv.nanovg;
27 import iv.nanovg.oui.blendish;
28 import iv.utfutil;
30 import booktext;
32 version(laytest) import iv.encoding;
33 version(aliced) {} else private alias usize = size_t;
36 // ////////////////////////////////////////////////////////////////////////// //
37 struct LayStyle {
38 enum Flag : uint {
39 Italic = 1<<0,
40 Bold = 1<<1,
41 Strike = 1<<2,
42 Underline = 1<<3,
43 Mono = 1<<4,
45 uint flags;
46 mixin({
47 import std.conv : to;
48 import std.ascii : toLower;
49 string res;
50 foreach (string s; __traits(allMembers, Flag)) {
51 //pragma(msg, s);
52 res ~= "@property bool "~s[0].toLower~s[1..$]~" () const pure nothrow @safe @nogc { pragma(inline, true); return ((flags&Flag."~s~") != 0); }\n";
53 res ~= "@property void "~s[0].toLower~s[1..$]~" (bool v) pure nothrow @safe @nogc { pragma(inline, true); if (v) flags |= Flag."~s~"; else flags &= ~Flag."~s~"; }\n";
55 return res;
56 }());
57 bool opEquals() (in auto ref LayStyle s) const pure nothrow @safe @nogc { pragma(inline, true); return (flags == s.flags); }
61 // ////////////////////////////////////////////////////////////////////////// //
62 struct LayJustify {
63 enum Justify : ubyte {
64 Left,
65 Right,
66 Center,
67 Justify,
69 Justify mode = Justify.Left;
70 int leftpad;
71 mixin({
72 import std.conv : to;
73 import std.ascii : toLower;
74 string res;
75 foreach (string s; __traits(allMembers, Justify)) {
76 //pragma(msg, s);
77 res ~= "@property bool "~s[0].toLower~s[1..$]~" () const pure nothrow @safe @nogc { pragma(inline, true); return (mode == Justify."~s~"); }\n";
78 res ~= "ref LayJustify set"~s~" () pure nothrow @safe @nogc { mode = Justify."~s~"; return this; }\n";
80 return res;
81 }());
82 bool opEquals() (in auto ref LayJustify s) const pure nothrow @safe @nogc { pragma(inline, true); return (mode == s.mode && leftpad == s.leftpad); }
86 // ////////////////////////////////////////////////////////////////////////// //
87 // this needs such fonts in stash:
88 // text -- normal
89 // texti -- italic
90 // textb -- bold
91 // textz -- italic and bold
92 // mono -- normal
93 // monoi -- italic
94 // monob -- bold
95 // monoz -- italic and bold
96 final class LayoutFonts {
97 public:
98 FONScontext* fs;
100 private:
101 bool killFontStash;
102 int lastSize = -1; // to ensure that first call to `setFont()` will do it's work
103 LayStyle lastStyle;
105 public:
106 this () {
107 // create new fontstash
108 FONSparams fontParams;
109 fontParams.width = 1024/*NVG_INIT_FONTIMAGE_SIZE*/;
110 fontParams.height = 1024/*NVG_INIT_FONTIMAGE_SIZE*/;
111 fontParams.flags = FONS_ZERO_TOPLEFT;
112 fs = fonsCreateInternal(&fontParams);
113 if (fs is null) throw new Exception("error creating font stash");
114 killFontStash = true;
115 fs.fonsResetAtlas(1024, 1024);
116 fonsSetSpacing(fs, 0);
117 fonsSetBlur(fs, 0);
118 fonsSetAlign(fs, NVGAlign.Left|NVGAlign.Baseline);
121 this (FONScontext* afs) {
122 if (afs is null) assert(0, "empty font stash");
123 fs = afs;
124 killFontStash = false;
127 ~this () { freeFontStash(); }
129 void freeFontStash () {
130 if (killFontStash && fs !is null) {
131 fs.fonsDeleteInternal();
133 killFontStash = false;
134 fs = null;
137 void setFont (int size) { setFont(size, lastStyle); }
138 void setFont (LayStyle style) { setFont(lastSize, style); }
140 void setFont (int size, LayStyle style) {
141 if (size < 1) size = 1;
142 if (style != lastStyle) {
143 char[6] fname;
144 uint fnlen = 4;
145 fname[0..4] = (style.mono ? "mono" : "text");
146 if (style.italic && style.bold) fname.ptr[fnlen++] = 'z';
147 else if (style.italic) fname.ptr[fnlen++] = 'i';
148 else if (style.bold) fname.ptr[fnlen++] = 'b';
149 auto fidx = fonsGetFontByName(fs, fname[0..fnlen]);
150 if (fidx == FONS_INVALID) throw new Exception("font '"~fname[0..fnlen].idup~"' not found");
151 fonsSetFont(fs, fidx);
153 fonsSetSize(fs, size);
154 lastSize = size;
155 lastStyle = style;
158 int textWidth(T) (const(T)[] str) if (is(T == char) || is(T == dchar)) {
159 import std.algorithm : max;
160 import core.stdc.math : lrintf;
161 float[4] b = void;
162 float adv = fs.fonsTextBounds(0, 0, str, b[]);
163 float w = b[2]-b[0];
164 return lrintf(max(adv, w));
167 void textWidth2(T) (const(T)[] str, int* w=null, int* wsp=null) if (is(T == char) || is(T == dchar)) {
168 import core.stdc.math : lrintf;
169 import std.algorithm : max;
170 float[4] b = void, bsp = void;
171 auto it = FonsTextBoundsIterator(fs, 0, 0);
172 it.put(str);
173 float adv = it.advance;
174 it.getBounds(b[]);
175 it.put(" ");
176 float advsp = it.advance;
177 it.getBounds(bsp[]);
178 // widths
179 if (w !is null) *w = lrintf(max(adv, b[2]-b[0]));
180 if (wsp !is null) *wsp = lrintf(max(advsp, bsp[2]-bsp[0]));
183 int textHeight () {
184 import core.stdc.math : lrintf;
185 // use line bounds for height
186 float y0 = void, y1 = void;
187 fs.fonsLineBounds(0, &y0, &y1);
188 return lrintf(y1-y0);
191 void textMetrics (int* asc, int* desc, int* lineh) {
192 import core.stdc.math : lrintf;
193 float a = void, d = void, h = void;
194 fs.fonsVertMetrics(&a, &d, &h);
195 if (asc !is null) *asc = lrintf(a);
196 if (desc !is null) *desc = lrintf(d);
197 if (lineh !is null) *lineh = lrintf(h);
202 // ////////////////////////////////////////////////////////////////////////// //
203 // layouted text word
204 final class LayWord {
205 //dstring text;
206 private:
207 usize wstart, wend;
208 LayText owner; // we will take word text from it
209 public:
210 bool wbreak; // can we break line on this word?
211 bool wspaced; // should we add spacing after this word?
212 bool wspacedOrig; // should we add spacing after this word? original value
213 LayStyle style;
214 int fsize; // font size
215 // calculated properties
216 int x=0, w=0, wsp=0; // starting x position, width, width with right space
217 int h=0, asc=0, desc=0; // height, ascent (positive), descent (negative)
218 ulong wordNum;
219 // return word spacing (if any)
220 @property int spacing () const pure nothrow @safe @nogc { pragma(inline, true); return (wspaced ? wsp-w : 0); }
221 @property const(dchar)[] text () const pure nothrow @safe @nogc { pragma(inline, true); return (owner !is null ? owner.fulltext[wstart..wend] : ""d); }
225 // ////////////////////////////////////////////////////////////////////////// //
226 // layouted text line
227 final class LayLine {
228 LayWord[] words;
229 LayJustify just;
230 // calculated properties
231 int x=0, y=0, w=0; // starting x and y positions, width
232 // on finish, layouter will calculate minimal ('cause it is negative) descent
233 int h=0, desc=0; // height, descent (negative)
234 ulong firstWordNum = 0;
238 // ////////////////////////////////////////////////////////////////////////// //
239 // layouted text
240 final class LayText {
241 public:
242 // special control characters
243 enum dchar NextLineCh = 0x2028; // 0x0085 is treated like whitespace
244 enum dchar NextParaCh = 0x2029;
245 enum : dchar {
246 MonoFontCh = 0x01,
247 PropFontCh = 0x02, // proportional
248 ItalicOnCh = 0x03,
249 ItalicOffCh = 0x04,
250 BoldOnCh = 0x05,
251 BoldOffCh = 0x06,
252 StrikeOnCh = 0x07,
253 StrikeOffCh = 0x08,
254 // the following chars implies new paragraph if previous mode is different
255 NormalTextCh = 0x0a,
256 CiteCh = 0x0b,
257 EpigraphCh = 0x0c,
258 StanzaCh = 0x0d, // new poem stanze
259 VerseCh = 0x0e, // new stanza line
260 TitleCh = 0x0f,
263 enum paraIndent = " ";
265 private:
266 bool finalized;
267 bool lineCreated; // is current line created? we do lazy line creation
268 ulong curWordNum = 0;
269 bool firstParaLine = true;
270 dchar[] fulltext;
271 usize lastWordStart; // in fulltext
272 dchar curTextMode = NormalTextCh;
273 bool normJustified; // is normal text justified?
274 bool newJustified;
276 public:
277 LayoutFonts laf;
278 LayLine[] lines;
279 int width; // maximum text width
281 private:
282 // current attributes
283 int fsize = 24; // font size
284 LayJustify just;
285 LayStyle style;
287 public:
288 // calculated after finalization
289 int textHeight = 0, textWidth = 0;
291 public:
292 // compare function should return (roughly): key-l
293 alias CmpFn = int delegate (LayLine l) nothrow @nogc;
295 int findLineBinary (scope CmpFn cmpfn) {
296 if (lines.length == 0) return -1;
297 int bot = 0, i = cast(int)lines.length-1;
298 while (bot != i) {
299 int mid = i-(i-bot)/2;
300 int cmp = cmpfn(lines[mid]);
301 if (cmp < 0) i = mid-1;
302 else if (cmp > 0) bot = mid;
303 else return mid;
305 return (cmpfn(lines[i]) == 0 ? i : -1);
308 public:
309 // this is for rendering engine
310 ulong[] sections; // line at which each section starts
312 public:
313 // find line with this word index
314 int findLineWithWord (ulong idx) {
315 return findLineBinary((LayLine l) {
316 if (idx < l.firstWordNum) return -1;
317 if (idx >= l.firstWordNum+l.words.length) return 1;
318 // skip empty lines
319 if (l.words.length == 0) return -1;
320 return 0;
324 // find line which contains this coordinate
325 int findLineWithY (int y) {
326 if (lines.length == 0) return 0;
327 if (y < 0) return 0;
328 if (y >= textHeight) return cast(int)lines.length-1;
329 auto res = findLineBinary((LayLine l) {
330 if (y < l.y) return -1;
331 if (y >= l.y+l.h) return 1;
332 return 0;
334 //if (res == -1) { import std.stdio; writeln("*** y=", y, "; th=", textHeight); }
335 assert(res != -1);
336 return res;
339 private:
340 Utf8Decoder dec;
341 bool lastWasUtf;
343 public:
344 this (LayoutFonts alaf, int awidth) {
345 if (alaf is null) assert(0, "no layout fonts");
346 if (awidth < 1) awidth = 1;
347 laf = alaf;
348 laf.setFont(fsize, style);
349 width = awidth;
352 // should normal text be justified? will take effect at next paragraph
353 @property bool normJustify () const pure nothrow @safe @nogc { pragma(inline, true); return newJustified; }
354 @property void normJustify (bool v) pure nothrow @safe @nogc { pragma(inline, true); newJustified = v; }
356 @property ulong curWordIndex () const pure nothrow @safe @nogc { pragma(inline, true); return curWordNum; }
358 @property int size () const pure nothrow @safe @nogc { pragma(inline, true); return fsize; }
359 @property void size (int newsize) {
360 if (finalized) throw new Exception("can't add chars to finalized layout");
361 if (newsize < 1) newsize = 1;
362 if (newsize != fsize) {
363 flushWord();
364 fsize = newsize;
368 // add text to layouter; it recognizes special chars
369 void put(T) (const(T)[] str...) if (is(T == char) || is(T == dchar)) {
370 if (finalized) throw new Exception("can't add chars to finalized layout");
371 if (str.length == 0) return;
373 dchar curCh; // 0: no more chars
374 usize stpos;
376 static if (is(T == char)) {
377 // utf-8 stream
378 if (!lastWasUtf) { lastWasUtf = true; dec.reset; }
379 void skipCh () @trusted {
380 while (stpos < str.length) {
381 curCh = dec.decode(cast(ubyte)str.ptr[stpos++]);
382 if (curCh <= dchar.max) return;
384 curCh = 0;
386 // load first char
387 skipCh();
388 } else {
389 // dchar stream
390 void skipCh () @trusted {
391 if (stpos < str.length) {
392 curCh = str.ptr[stpos++];
393 if (curCh > dchar.max) curCh = '?';
394 } else {
395 curCh = 0;
398 // load first char
399 if (lastWasUtf) {
400 lastWasUtf = false;
401 if (!dec.complete) curCh = '?'; else skipCh();
402 } else {
403 skipCh();
407 template StyleSwitchMixin(string ch, string prop, bool val) {
408 static if (val) enum sval = "true"; else enum sval = "false";
409 enum StyleSwitchMixin = "case "~ch~": if (style."~prop~" != "~sval~") { flushWord(); style."~prop~" = "~sval~"; } break;";
412 // process stream dchars
413 if (curCh == 0) return;
414 while (curCh) {
415 import std.uni : isWhite;
416 dchar ch = curCh;
417 skipCh();
418 switch (ch) {
419 case NormalTextCh:
420 case CiteCh:
421 case EpigraphCh:
422 case StanzaCh:
423 case VerseCh:
424 case TitleCh:
425 if (curTextMode != ch) {
426 flushWord();
427 addSpace();
428 flushPara(ch);
430 break;
431 case NextLineCh:
432 flushWord();
433 addSpace();
434 if (lineCreated) flushLine(); else newLine();
435 break;
436 case NextParaCh:
437 flushWord();
438 addSpace();
439 newPara();
440 break;
441 mixin(StyleSwitchMixin!("MonoFontCh", "mono", true));
442 mixin(StyleSwitchMixin!("PropFontCh", "mono", false));
443 mixin(StyleSwitchMixin!("ItalicOnCh", "italic", true));
444 mixin(StyleSwitchMixin!("ItalicOffCh", "italic", false));
445 mixin(StyleSwitchMixin!("BoldOnCh", "bold", true));
446 mixin(StyleSwitchMixin!("BoldOffCh", "bold", false));
447 mixin(StyleSwitchMixin!("StrikeOnCh", "strike", true));
448 mixin(StyleSwitchMixin!("StrikeOffCh", "strike", false));
449 case 0x00a0: // non-breaking space
450 fulltext ~= ' ';
451 break;
452 default:
453 if (ch <= ' ' || isWhite(ch)) {
454 flushWord();
455 addSpace(); // this will fix the last word in the current line
456 } else {
457 fulltext ~= ch;
459 break;
464 // finalize layout
465 void finalize () {
466 import std.algorithm : max, min;
467 if (finalized) throw new Exception("can't finalize already finalized layout");
468 flushPara();
469 finalized = true;
470 // calculate line positions and line widthes
471 textHeight = 0;
472 textWidth = 0;
473 int curY = 0;
474 ulong nlws = 0; // next line starting word index
475 //{ import std.stdio; writeln(lines.length, " lines"); }
476 foreach (immutable lidx, LayLine ln; lines) {
477 ln.firstWordNum = nlws;
478 if (ln.words.length) {
479 assert(ln.words.ptr[0].wordNum == nlws);
480 nlws += ln.words.length;
482 ln.y = curY;
483 if (ln.words.length) {
484 // calculate line height
485 ln.w = 0;
486 ln.h = 0;
487 ln.desc = 0;
488 foreach (LayWord w; ln.words) {
489 ln.w += (w.wspaced ? w.wsp : w.w);
490 ln.h = max(ln.h, w.h);
491 ln.desc = min(ln.desc, w.desc);
493 // calculate words coordinates
494 if (ln.just.justify) {
495 int spc = width;
496 int wcount; // we don't have to space non-breaking words
497 // last word in line is marked as "nonbreaking", so we don't need to check for it
498 foreach (immutable idx, LayWord w; ln.words) {
499 if (w.wspaced) ++wcount;
500 spc -= w.w;
502 version(laytest) { import std.stdio; writeln("+++ width=", width, "; spc=", spc, "; wcount=", wcount); }
503 if (spc < 0) spc = 0;
504 int xadvsp, frac;
505 if (wcount > 0) {
506 xadvsp = spc/wcount;
507 frac = spc-xadvsp*wcount;
509 version(laytest) { import std.stdio; writeln("+++ xspc=", spc); }
510 int x = 0;
511 ln.x = x;
512 foreach (LayWord w; ln.words) {
513 w.x = w.x+x;
514 x += w.w;
515 if (wcount && w.wspaced) {
516 x += xadvsp;
517 if (frac-- > 0) ++x;
520 //ln.w = x;
521 version(laytest) { import std.stdio; writeln(" xe=", x, "; w=", ln.w, "; width=", width); }
522 } else {
523 int x = 0;
524 if (ln.just.left) x = 0;
525 else if (ln.just.right) x = width-ln.w;
526 else if (ln.just.center) x = (width-ln.w)/2;
527 else assert(0, "wtf?!");
528 ln.x = x;
529 foreach (LayWord w; ln.words) {
530 w.x += x;
531 x += (w.wspaced ? w.wsp : w.w);
533 //ln.w = x;
535 } else {
536 if (ln.h <= 0) ln.h = 24; // just in case
537 ln.w = 0;
539 version(laytest) {
540 import std.stdio;
541 writeln("LINE #", lidx, "; y=", ln.y, "; w=", ln.w, "; h=", ln.h);
542 foreach (immutable widx, LayWord w; ln.words) {
543 import std.conv : to;
544 writeln(" word #", widx, ": x=", w.x, "; w=", w.w, "; wsp=", w.wsp, "; [", w.text.to!string.recodeToKOI8, "]");
547 curY += ln.h;
548 textWidth = max(textWidth, ln.w);
550 laf = null;
551 textHeight = curY;
554 private:
555 // word should be already flushed
556 void addSpace () {
557 if (!lineCreated) return;
558 auto ln = lines[$-1];
559 if (ln.words.length == 0) return;
560 with (ln.words[$-1]) { wbreak = true; wspaced = true; }
563 void fixParaStyle () {
564 switch (curTextMode) {
565 case NormalTextCh:
566 if (normJustified) just.setJustify; else just.setLeft;
567 style.italic = false;
568 style.bold = false;
569 style.strike = false;
570 style.underline = false;
571 style.mono = false;
572 break;
573 case CiteCh:
574 if (normJustified) just.setJustify; else just.setLeft;
575 style.italic = true;
576 style.bold = false;
577 style.strike = false;
578 style.underline = false;
579 style.mono = false;
580 break;
581 case EpigraphCh:
582 just.setRight;
583 style.italic = true;
584 style.bold = false;
585 style.strike = false;
586 style.underline = false;
587 style.mono = false;
588 break;
589 case StanzaCh:
590 just.setLeft;
591 style.italic = true;
592 style.bold = false;
593 style.strike = false;
594 style.underline = false;
595 style.mono = false;
596 break;
597 case VerseCh:
598 just.setLeft;
599 style.italic = true;
600 style.bold = false;
601 style.strike = false;
602 style.underline = false;
603 style.mono = false;
604 break;
605 case TitleCh:
606 just.setCenter;
607 style.italic = false;
608 style.bold = false;
609 style.strike = false;
610 style.underline = false;
611 style.mono = false;
612 break;
613 default: assert(0, "wtf?!");
617 // end this paragraph; word should be already flushed
618 void flushPara (dchar newMode=0) {
619 // last paragraph line should not be justified
620 flushLine!"last"();
621 firstParaLine = true;
622 normJustified = newJustified;
623 if (newMode) curTextMode = newMode;
624 fixParaStyle();
625 if (curTextMode == StanzaCh) newLine();
628 // word should be flushed
629 void newLine () {
630 flushLine!"last"();
631 auto ln = new LayLine();
632 laf.setFont(fsize, style);
633 ln.h = laf.textHeight();
634 ln.just = just;
635 lines ~= ln;
638 // paragraph should be flushed
639 void newPara () {
640 flushLine!"last"();
641 fixParaStyle();
642 auto ln = new LayLine();
643 laf.setFont(fsize, style);
644 ln.h = laf.textHeight();
645 ln.just = just;
646 firstParaLine = true;
649 void flushLine(string mode="") () if (mode == "" || mode == "last") {
650 if (!lineCreated) return;
651 auto ln = lines[$-1];
652 // last paragraph line should not be justified
653 static if (mode == "last") {
654 if (normJustified) ln.just.setJustify; else ln.just.setLeft;
656 // last word should not have space after it
657 if (ln.words.length) {
658 auto w = ln.words[$-1];
659 w.wspacedOrig = w.wspaced;
660 w.wspaced = false;
662 lineCreated = false;
665 // add current word (if any) to line
666 // the word is non-spaced and non-breaking by default
667 void flushWord () {
668 static bool isDash (dchar ch) {
669 pragma(inline, true);
670 return (ch == '-' || ch == 0x2013);
673 while (lastWordStart < fulltext.length) {
674 // break after dashes
675 bool allowBreak = false;
676 usize wend = lastWordStart;
677 while (wend < fulltext.length) {
678 if (isDash(fulltext.ptr[wend])) {
679 while (wend < fulltext.length && isDash(fulltext.ptr[wend])) ++wend;
680 allowBreak = true;
681 break;
683 ++wend;
685 flushWordImpl(lastWordStart, wend, allowBreak);
686 lastWordStart = wend;
690 // really add word ;-)
691 void flushWordImpl (usize wstart, usize wend, bool allowBreak) {
692 import std.algorithm : max;
693 assert(wend > wstart);
694 version(laytest) {
695 import std.conv : to;
696 import std.stdio;
697 writeln("*** ", str.to!string.recodeToKOI8, "|", allowBreak);
699 // normal word
700 LayLine ln;
701 if (!lineCreated) {
702 // add new line if necessary
703 if (lines.length >= int.max/2-8) throw new Exception("too many lines");
704 ln = new LayLine();
705 laf.setFont(fsize, style);
706 ln.h = laf.textHeight();
707 ln.just = just;
708 lines ~= ln;
709 lineCreated = true;
710 } else {
711 ln = lines[$-1];
713 // create new word
714 auto w = new LayWord();
715 w.owner = this;
716 w.wstart = wstart;
717 w.wend = wend;
718 w.wordNum = curWordNum++;
719 w.wbreak = allowBreak;
720 w.wspaced = false;
721 w.style = style;
722 w.fsize = fsize;
723 // calculate dimensions
724 laf.setFont(fsize, style);
725 laf.textWidth2(w.text, &w.w, &w.wsp); // normal and with space
726 // calculate ascent, descent and height
727 laf.textMetrics(&w.asc, &w.desc, &w.h);
728 version(laytest) { import std.stdio; writeln("width=", width, "; ln.w=", ln.w, "; w.w=", w.w, "; w.wsp=", w.wsp); }
729 if (ln.words.length > 0) {
730 firstParaLine = false; // just in case
731 // does this word fit?
732 int newW = ln.w+ln.words[$-1].spacing+w.w;
733 if (newW <= width) {
734 // yay, it fits!
735 if (ln.words.length >= int.max/2) throw new Exception("too many words in line");
736 ln.words ~= w;
737 ln.w = newW; // fix current line width
738 return;
740 // oops, we have to start new line here
741 flushLine();
742 // add new line
743 if (lines.length >= int.max/2-8) throw new Exception("too many lines");
744 ln = new LayLine();
745 ln.h = w.h;
746 ln.just = just;
747 lines ~= ln;
748 lineCreated = true;
750 if (firstParaLine && (just.left || just.justify)) {
751 // paragraph indent
752 int ind = laf.textWidth(paraIndent);
753 switch (curTextMode) {
754 case EpigraphCh:
755 case TitleCh:
756 ind = 0;
757 break;
758 case CiteCh:
759 case StanzaCh:
760 case VerseCh:
761 ind *= 2;
762 break;
763 default:
765 w.w += ind;
766 w.wsp += ind;
767 w.x = ind; // indent
769 firstParaLine = false;
770 ln.words ~= w;
771 ln.w = w.w;
772 ln.h = max(ln.h, w.h);