it seems to work again
[xreader.git] / xlayouter.d
blob7889a37181583f9fc69181ea5c63128ab836c313
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 arsd.color;
20 import arsd.png;
21 import arsd.jpeg;
23 import iv.nanovg;
24 import iv.nanovg.oui.blendish;
25 import iv.utfutil;
26 import iv.vfs;
27 import iv.vfs.io;
29 import booktext;
31 version(laytest) import iv.encoding;
32 version(aliced) {} else private alias usize = size_t;
35 // ////////////////////////////////////////////////////////////////////////// //
36 // this needs such fonts in stash:
37 // text -- normal
38 // texti -- italic
39 // textb -- bold
40 // textz -- italic and bold
41 // mono -- normal
42 // monoi -- italic
43 // monob -- bold
44 // monoz -- italic and bold
45 final class LayFontStash {
46 public:
47 FONScontext* fs;
48 // list of known font faces, should be filled by caller when this object created
49 // DO NOT MODIFY!
50 int[string] fontfaces;
52 private:
53 bool killFontStash;
54 bool fontWasSet; // to ensure that first call to `setFont()` will do it's work
55 LayFontStyle lastStyle;
57 public:
58 this () {
59 // create new fontstash
60 FONSparams fontParams;
61 fontParams.width = 1024/*NVG_INIT_FONTIMAGE_SIZE*/;
62 fontParams.height = 1024/*NVG_INIT_FONTIMAGE_SIZE*/;
63 fontParams.flags = FONS_ZERO_TOPLEFT;
64 fs = fonsCreateInternal(&fontParams);
65 if (fs is null) throw new Exception("error creating font stash");
66 killFontStash = true;
67 fs.fonsResetAtlas(1024, 1024);
68 fonsSetSpacing(fs, 0);
69 fonsSetBlur(fs, 0);
70 fonsSetAlign(fs, NVGAlign.Left|NVGAlign.Baseline);
73 ~this () { freeFontStash(); }
75 void freeFontStash () {
76 if (killFontStash && fs !is null) {
77 fs.fonsDeleteInternal();
79 killFontStash = false;
80 fs = null;
83 void addFont(T : const(char)[], TP : const(char)[]) (T name, TP path) {
84 static if (is(T == typeof(null))) {
85 throw new Exception("invalid font face name");
86 } else {
87 if (name.length == 0) throw new Exception("invalid font face name");
88 if (name in fontfaces) throw new Exception("duplicate font '"~name.idup~"'");
89 int fid = fs.fonsAddFont(name, path);
90 if (fid < 0) throw new Exception("font '"~name~"' is not found at '"~path.idup~"'");
91 static if (is(T == string)) fontfaces[name] = fid; else fontfaces[name.idup] = fid;
95 @property int fontFace (const(char)[] name) {
96 if (auto fid = name in fontfaces) return *fid;
97 return -1;
100 void setFont() (in auto ref LayFontStyle style) {
101 int fsz = style.fontsize;
102 if (fsz < 1) fsz = 1;
103 if (!fontWasSet || fsz != lastStyle.fontsize || style.fontface != lastStyle.fontface) {
104 if (style.fontface != lastStyle.fontface) fonsSetFont(fs, style.fontface);
105 if (fsz != lastStyle.fontsize) fonsSetSize(fs, fsz);
106 lastStyle = style;
107 lastStyle.fontsize = fsz;
111 int textWidth(T) (const(T)[] str) if (is(T == char) || is(T == dchar)) {
112 import std.algorithm : max;
113 import core.stdc.math : lrintf;
114 float[4] b = void;
115 float adv = fs.fonsTextBounds(0, 0, str, b[]);
116 float w = b[2]-b[0];
117 return lrintf(max(adv, w));
120 int spacesWidth (int count) {
121 import core.stdc.math : lrintf;
122 if (count < 1) return 0;
123 auto it = FonsTextBoundsIterator(fs, 0, 0);
124 it.put(' ');
125 return lrintf(it.advance*count);
128 void textWidth2(T) (const(T)[] str, int* w=null, int* wsp=null, int* whyph=null) if (is(T == char) || is(T == dchar)) {
129 import core.stdc.math : lrintf;
130 import std.algorithm : max;
131 if (w is null && wsp is null && whyph is null) return;
132 float minx, maxx;
133 auto it = FonsTextBoundsIterator(fs, 0, 0);
134 it.put(str);
135 if (w !is null) {
136 it.getHBounds(minx, maxx);
137 *w = lrintf(max(it.advance, maxx-minx));
139 if (wsp !is null && whyph is null) {
140 it.put(" ");
141 it.getHBounds(minx, maxx);
142 *wsp = lrintf(max(it.advance, maxx-minx));
143 } else if (wsp is null && whyph !is null) {
144 it.put(cast(dchar)45);
145 it.getHBounds(minx, maxx);
146 *whyph = lrintf(max(it.advance, maxx-minx));
147 } else if (wsp !is null && whyph !is null) {
148 auto sit = it;
149 it.put(" ");
150 it.getHBounds(minx, maxx);
151 *wsp = lrintf(max(it.advance, maxx-minx));
152 sit.put(cast(dchar)45);
153 sit.getHBounds(minx, maxx);
154 *whyph = lrintf(max(sit.advance, maxx-minx));
158 int textHeight () {
159 import core.stdc.math : lrintf;
160 // use line bounds for height
161 float y0 = void, y1 = void;
162 fs.fonsLineBounds(0, &y0, &y1);
163 return lrintf(y1-y0);
166 void textMetrics (int* asc, int* desc, int* lineh) {
167 import core.stdc.math : lrintf;
168 float a = void, d = void, h = void;
169 fs.fonsVertMetrics(&a, &d, &h);
170 if (asc !is null) *asc = lrintf(a);
171 if (desc !is null) *desc = lrintf(d);
172 if (lineh !is null) *lineh = lrintf(h);
177 // ////////////////////////////////////////////////////////////////////////// //
178 // generic text style
179 align(1) struct LayFontStyle {
180 align(1):
181 enum Flag : uint {
182 Italic = 1<<0,
183 Bold = 1<<1,
184 Strike = 1<<2,
185 Underline = 1<<3,
186 Overline = 1<<4,
188 ubyte flags; // see above
189 int fontface = -1; // i can't use strings here, as this struct inside LayWord will not be GC-scanned
190 int fontsize;
191 uint color = 0xff000000; // AABBGGRR; AA usually ignored by renderer, but i'll keep it anyway
192 string toString () const {
193 import std.format : format;
194 string res = "font:%s;size:%s;color:0x%08X".format(fontface, fontsize, color);
195 if (flags&Flag.Italic) res ~= ";italic";
196 if (flags&Flag.Bold) res ~= ";bold";
197 if (flags&Flag.Strike) res ~= ";strike";
198 if (flags&Flag.Underline) res ~= ";under";
199 if (flags&Flag.Overline) res ~= ";over";
200 return res;
202 mixin({
203 import std.conv : to;
204 import std.ascii : toLower;
205 string res;
206 foreach (string s; __traits(allMembers, Flag)) {
207 //pragma(msg, s);
208 res ~= "@property bool "~s[0].toLower~s[1..$]~" () const pure nothrow @safe @nogc { pragma(inline, true); return ((flags&Flag."~s~") != 0); }\n";
209 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";
211 return res;
212 }());
213 bool opEquals() (in auto ref LayFontStyle s) const pure nothrow @safe @nogc { pragma(inline, true); return (flags == s.flags && fontface == s.fontface && color == s.color && fontsize == s.fontsize); }
217 // ////////////////////////////////////////////////////////////////////////// //
218 // line align style
219 align(1) struct LayLineStyle {
220 align(1):
221 enum Justify : ubyte {
222 Left,
223 Right,
224 Center,
225 Justify,
227 Justify mode = Justify.Left;
228 short lpad, rpad, tpad, bpad;
229 ubyte paraIndent; // in spaces
230 string toString () const {
231 import std.format : format;
232 string res;
233 final switch (mode) {
234 case Justify.Left: res = "left"; break;
235 case Justify.Right: res = "right"; break;
236 case Justify.Center: res = "center"; break;
237 case Justify.Justify: res = "justify"; break;
239 if (lpad) res ~= ";lpad:%s".format(lpad);
240 if (rpad) res ~= ";rpad:%s".format(rpad);
241 if (tpad) res ~= ";tpad:%s".format(tpad);
242 if (bpad) res ~= ";bpad:%s".format(bpad);
243 return res;
245 mixin({
246 import std.conv : to;
247 import std.ascii : toLower;
248 string res;
249 foreach (string s; __traits(allMembers, Justify)) {
250 //pragma(msg, s);
251 res ~= "@property bool "~s[0].toLower~s[1..$]~" () const pure nothrow @safe @nogc { pragma(inline, true); return (mode == Justify."~s~"); }\n";
252 res ~= "ref LayLineStyle set"~s~" () pure nothrow @safe @nogc { mode = Justify."~s~"; return this; }\n";
254 return res;
255 }());
256 bool opEquals() (in auto ref LayLineStyle s) const pure nothrow @safe @nogc { pragma(inline, true); return (mode == s.mode && lpad == s.lpad); }
257 @property pure nothrow @safe @nogc {
258 int leftpad () const { pragma(inline, true); return lpad; }
259 void leftpad (int v) { pragma(inline, true); lpad = (v < short.min ? short.min : v > short.max ? short.max : cast(short)v); }
260 int rightpad () const { pragma(inline, true); return rpad; }
261 void rightpad (int v) { pragma(inline, true); rpad = (v < short.min ? short.min : v > short.max ? short.max : cast(short)v); }
262 int toppad () const { pragma(inline, true); return tpad; }
263 void toppad (int v) { pragma(inline, true); tpad = (v < 0 ? 0 : v > short.max ? short.max : cast(short)v); }
264 int bottompad () const { pragma(inline, true); return bpad; }
265 void bottompad (int v) { pragma(inline, true); bpad = (v < 0 ? 0 : v > short.max ? short.max : cast(short)v); }
270 // ////////////////////////////////////////////////////////////////////////// //
271 // layouted text word
272 align(1) struct LayWord {
273 align(1):
274 static align(1) struct Props {
275 align(1):
276 enum Flag : uint {
277 CanBreak = 1<<0, // can i break line at this word?
278 Spaced = 1<<1, // should this word be whitespaced at the end?
279 Hypen = 1<<2, // if i'll break at this word, should i add hyphen mark?
280 LineEnd = 1<<3, // this word ends current line
281 ParaEnd = 1<<4, // this word ends current paragraph (and, implicitly, line)
283 ubyte flags; // see above
284 @property pure nothrow @safe @nogc:
285 bool canbreak () const { pragma(inline, true); return ((flags&Flag.CanBreak) != 0); }
286 void canbreak (bool v) { pragma(inline, true); if (v) flags |= Flag.CanBreak; else flags &= ~Flag.CanBreak; }
287 bool spaced () const { pragma(inline, true); return ((flags&Flag.Spaced) != 0); }
288 void spaced (bool v) { pragma(inline, true); if (v) flags |= Flag.Spaced; else flags &= ~Flag.Spaced; }
289 bool hyphen () const { pragma(inline, true); return ((flags&Flag.Hypen) != 0); }
290 void hyphen (bool v) { pragma(inline, true); if (v) flags |= Flag.Hypen; else flags &= ~Flag.Hypen; }
291 bool lineend () const { pragma(inline, true); return ((flags&Flag.LineEnd) != 0); }
292 void lineend (bool v) { pragma(inline, true); if (v) flags |= Flag.LineEnd; else flags &= ~Flag.LineEnd; }
293 bool paraend () const { pragma(inline, true); return ((flags&Flag.ParaEnd) != 0); }
294 void paraend (bool v) { pragma(inline, true); if (v) flags |= Flag.ParaEnd; else flags &= ~Flag.ParaEnd; }
295 // note that if word is softhyphen candidate, i have hyphen mark at [wend]
296 // if props.hyphen is set, wend is including that mark, otherwise it isn't
298 uint wstart, wend; // in LayText text buffer
299 LayFontStyle style; // font style
300 uint wordNum; // word number (index in LayText word array)
301 Props propsOrig; // original properties, used for relayouting
302 // calculated values
303 Props props; // effective props after layouting
304 int x; // horizontal word position in line
305 int h; // word height (full)
306 int asc; // ascent (positive)
307 int desc; // descent (negative)
308 int w; // word width, without hyphen and spacing
309 int wsp; // word width with spacing (i.e. with space added at the end)
310 int whyph; // word width with hyphen (i.e. with hyphen mark added at the end)
311 @property int width () const pure nothrow @safe @nogc { pragma(inline, true); return (props.hyphen ? whyph : w); }
312 // width with spacing/hyphen
313 @property int fullwidth () const pure nothrow @safe @nogc { pragma(inline, true); return (props.hyphen ? whyph : props.spaced ? wsp : w); }
314 // space width based on original props
315 @property int spacewidth () const pure nothrow @safe @nogc { pragma(inline, true); return (propsOrig.spaced ? wsp-w : 0); }
316 //FIXME: find better place for this! keep that in separate pool, or something, and look there with word index
317 LayLineStyle just;
321 // ////////////////////////////////////////////////////////////////////////// //
322 // layouted text line
323 struct LayLine {
324 uint wstart, wend; // indicies in word array
325 LayLineStyle just; // line style
326 // calculated properties
327 int x, y, w; // starting x and y positions, width
328 // on finish, layouter will calculate minimal ('cause it is negative) descent
329 int h, desc; // height, descent (negative)
330 @property int wordCount () const pure nothrow @safe @nogc { pragma(inline, true); return cast(int)(wend-wstart); }
334 // ////////////////////////////////////////////////////////////////////////// //
335 // layouted text
336 final class LayText {
337 public:
338 // special control characters
339 enum dchar EndLineCh = 0x2028; // 0x0085 is treated like whitespace
340 enum dchar EndParaCh = 0x2029;
342 private:
343 void ensurePool(ubyte pow2, bool clear, T) (uint want, ref T* ptr, ref uint used, ref uint alloced) {
344 if (want == 0) return;
345 static assert(pow2 < 24, "wtf?!");
346 uint cursz = used*cast(uint)T.sizeof;
347 if (cursz >= int.max/2) throw new Exception("pool overflow");
348 auto lsz = cast(ulong)want*T.sizeof;
349 if (lsz >= int.max/2 || lsz+cursz >= int.max/2) throw new Exception("pool overflow");
350 want = cast(uint)lsz;
351 uint cural = alloced*cast(uint)T.sizeof;
352 if (cursz+want > cural) {
353 import core.stdc.stdlib : realloc;
354 // grow it
355 uint newsz = ((cursz+want)|((1<<pow2)-1))+1;
356 if (newsz >= int.max/2) throw new Exception("pool overflow");
357 auto np = cast(T*)realloc(ptr, newsz);
358 if (np is null) throw new Exception("out of memory for pool");
359 static if (clear) {
360 import core.stdc.string : memset;
361 memset(np+used, 0, newsz-cursz);
363 ptr = np;
364 alloced = newsz/cast(uint)T.sizeof;
368 dchar* ltext;
369 uint charsUsed, charsAllocated;
371 void putChars (const(dchar)[] str...) {
372 import core.stdc.string : memcpy;
373 if (str.length == 0) return;
374 if (str.length > int.max/2) throw new Exception("text too big");
375 ensurePool!(16, false)(str.length, ltext, charsUsed, charsAllocated);
376 memcpy(ltext+charsUsed, str.ptr, cast(uint)str.length*cast(uint)ltext[0].sizeof);
377 charsUsed += cast(uint)str.length;
380 LayWord* words;
381 uint wordsUsed, wordsAllocated;
383 LayWord* allocWord () {
384 ensurePool!(16, true)(1, words, wordsUsed, wordsAllocated);
385 auto res = words+wordsUsed;
386 res.wordNum = wordsUsed++;
387 return res;
390 LayLine* lines;
391 uint linesUsed, linesAllocated;
393 LayLine* allocLine () {
394 ensurePool!(16, true)(1, lines, linesUsed, linesAllocated);
395 return lines+(linesUsed++);
398 LayLine* lastLine () { pragma(inline, true); return (linesUsed > 0 ? lines+linesUsed-1 : null); }
400 bool lastLineHasWords () { pragma(inline, true); return (linesUsed > 0 ? (lines[linesUsed-1].wend > lines[linesUsed-1].wstart) : false); }
402 // should not be called when there are no lines, or no words in last line
403 LayWord* lastLineLastWord () { pragma(inline, true); return words+lastLine.wend-1; }
405 static struct StyleStackItem {
406 LayFontStyle fs;
407 LayLineStyle ls;
409 StyleStackItem* styleStack;
410 uint ststackUsed, ststackAllocated;
412 public void pushStyles () {
413 ensurePool!(4, false)(1, styleStack, ststackUsed, ststackAllocated);
414 auto si = styleStack+(ststackUsed++);
415 si.fs = newStyle;
416 si.ls = newJust;
419 public void popStyles () {
420 if (ststackUsed == 0) throw new Exception("style stack underflow");
421 auto si = styleStack+(--ststackUsed);
422 newStyle = si.fs;
423 newJust = si.ls;
426 private:
427 bool firstParaLine = true;
428 uint lastWordStart; // in fulltext
429 uint firstWordNotFlushed;
431 @property bool hasWordChars () const pure nothrow @safe @nogc { pragma(inline, true); return (lastWordStart < charsUsed); }
433 private:
434 // current attributes
435 LayLineStyle just; // for current paragraph
436 LayFontStyle style;
437 // user can change this alot, so don't apply that immediately
438 LayFontStyle newStyle;
439 LayLineStyle newJust;
441 private:
442 Utf8Decoder dec;
443 bool lastWasUtf;
444 bool lastWasSoftHypen;
445 int maxWidth; // maximum text width
446 LayFontStash laf;
448 public:
449 int textHeight = 0; // total text height
450 int textWidth = 0; // maximum text width
452 public:
453 // compare function should return (roughly): key-l
454 alias CmpFn = int delegate (LayLine* l) nothrow @nogc;
456 int findLineBinary (scope CmpFn cmpfn) {
457 if (linesUsed == 0) return -1;
458 int bot = 0, i = cast(int)linesUsed-1;
459 while (bot != i) {
460 int mid = i-(i-bot)/2;
461 int cmp = cmpfn(lines+mid);
462 if (cmp < 0) i = mid-1;
463 else if (cmp > 0) bot = mid;
464 else return mid;
466 return (cmpfn(lines+i) == 0 ? i : -1);
469 // find line with this word index
470 int findLineWithWord (uint idx) {
471 return findLineBinary((LayLine* l) {
472 if (idx < l.wstart) return -1;
473 if (idx >= l.wend) return 1;
474 return 0;
478 // find line which contains this coordinate
479 int findLineWithY (int y) {
480 if (linesUsed == 0) return 0;
481 if (y < 0) return 0;
482 if (y >= textHeight) return cast(int)linesUsed-1;
483 auto res = findLineBinary((LayLine* l) {
484 if (y < l.y) return -1;
485 if (y >= l.y+l.h) return 1;
486 return 0;
488 //if (res == -1) { import std.stdio; writeln("*** y=", y, "; th=", textHeight); }
489 assert(res != -1);
490 return res;
493 @property const(dchar)[] wordText (in ref LayWord w) const pure nothrow @trusted @nogc { pragma(inline, true); return ltext[w.wstart..w.wend]; }
495 @property int lineCount () const pure nothrow @safe @nogc { pragma(inline, true); return cast(int)linesUsed; }
497 // word iterator
498 @property auto lineWords (int lidx) {
499 static struct Range {
500 private:
501 LayWord* w;
502 int wordsLeft; // not including current
503 nothrow @trusted @nogc:
504 private:
505 this (LayText lay, int lidx) {
506 if (lidx >= 0 && lidx < lay.linesUsed) {
507 auto ln = lay.lines+lidx;
508 if (ln.wend > ln.wstart) {
509 w = lay.words+ln.wstart;
510 wordsLeft = ln.wend-ln.wstart-1;
514 public:
515 @property bool empty () const pure { pragma(inline, true); return (w is null); }
516 //@property ref LayWord front () pure { pragma(inline, true); assert(w !is null); return *w; }
517 @property ref LayWord front () pure { pragma(inline, true); assert(w !is null); return *w; }
518 void popFront () { if (wordsLeft) { ++w; --wordsLeft; } else w = null; }
519 Range save () { Range res = void; res.w = w; res.wordsLeft = wordsLeft; return res; }
520 @property int length () const pure { pragma(inline, true); return (w !is null ? wordsLeft+1 : 0); }
521 alias opDollar = length;
522 @property LayWord[] opSlice () { return (w !is null ? w[0..wordsLeft+1] : null); }
523 @property LayWord[] opSlice (int lo, int hi) {
524 if (lo < 0) lo = 0;
525 if (w is null || hi <= lo || lo > wordsLeft) return null;
526 if (hi > wordsLeft+1) hi = wordsLeft+1;
527 return w[lo..hi];
530 return Range(this, lidx);
533 LayLine* line (int lidx) { pragma(inline, true); return (lidx >= 0 && lidx < linesUsed ? lines+lidx : null); }
535 public:
536 this (LayFontStash alaf, int awidth) {
537 if (alaf is null) assert(0, "no layout fonts");
538 if (awidth < 1) awidth = 1;
539 laf = alaf;
540 maxWidth = awidth;
543 ~this () { freeMemory(); }
545 void freeMemory () {
546 import core.stdc.stdlib : free;
547 if (lines !is null) { free(lines); lines = null; }
548 if (words !is null) { free(words); words = null; }
549 if (ltext !is null) { free(ltext); ltext = null; }
550 wordsUsed = wordsAllocated = linesUsed = linesAllocated = charsUsed = charsAllocated = 0;
553 @property int width () const pure nothrow @safe @nogc { pragma(inline, true); return maxWidth; }
555 @property uint curWordIndex () const pure nothrow @safe @nogc { pragma(inline, true); return (wordsUsed ? wordsUsed-1 : 0); }
557 @property ref LayFontStyle fontStyle () pure nothrow @safe @nogc { pragma(inline, true); return newStyle; }
558 @property ref LayLineStyle lineStyle () pure nothrow @safe @nogc { pragma(inline, true); return newJust; }
560 @property int fontFace (const(char)[] name) {
561 if (laf !is null) {
562 int fid = laf.fontFace(name);
563 if (fid >= 0) return fid;
565 throw new Exception("unknown font face '"~name.idup~"'");
568 void endLine () { put(EndLineCh); }
569 void endPara () { put(EndParaCh); }
571 // add text to layouter
572 void put(T) (const(T)[] str...) if (is(T == char) || is(T == dchar)) {
573 if (str.length == 0) return;
575 dchar curCh; // 0: no more chars
576 usize stpos;
578 static if (is(T == char)) {
579 // utf-8 stream
580 if (!lastWasUtf) { lastWasUtf = true; dec.reset; }
581 void skipCh () @trusted {
582 while (stpos < str.length) {
583 curCh = dec.decode(cast(ubyte)str.ptr[stpos++]);
584 if (curCh <= dchar.max) return;
586 curCh = 0;
588 // load first char
589 skipCh();
590 } else {
591 // dchar stream
592 void skipCh () @trusted {
593 if (stpos < str.length) {
594 curCh = str.ptr[stpos++];
595 if (curCh > dchar.max) curCh = '?';
596 } else {
597 curCh = 0;
600 // load first char
601 if (lastWasUtf) {
602 lastWasUtf = false;
603 if (!dec.complete) curCh = '?'; else skipCh();
604 } else {
605 skipCh();
609 // process stream dchars
610 if (curCh == 0) return;
611 if (!hasWordChars) style = newStyle;
612 if (firstWordNotFlushed >= wordsUsed) just = newJust;
613 while (curCh) {
614 import std.uni;
615 dchar ch = curCh;
616 skipCh();
617 if (ch == EndLineCh || ch == EndParaCh) {
618 lastWasSoftHypen = false;
619 // ignore leading empty lines
620 if (hasWordChars || linesUsed) {
621 LayWord* lw;
622 if (hasWordChars) {
623 // has some word data, flush it now
624 flushWord();
625 lw = words+wordsUsed-1;
626 } else if (wordsUsed == 0) {
627 // create empty word to set attributes on it
628 lw = allocWord();
629 } else {
630 lw = words+wordsUsed-1;
631 // do i need to add empty word for attrs?
632 if (lw.propsOrig.lineend || lw.propsOrig.paraend) lw = allocWord();
634 // fix word properties
635 lw.propsOrig.canbreak = true;
636 lw.propsOrig.spaced = false;
637 lw.propsOrig.hyphen = false;
638 lw.propsOrig.lineend = (ch == EndLineCh);
639 lw.propsOrig.paraend = (ch == EndParaCh);
640 // build layout part
641 flushLines();
643 if (ch == EndParaCh) just = newJust;
644 firstParaLine = (ch == EndParaCh);
645 } else if (ch == 0x00a0) {
646 // non-breaking space
647 lastWasSoftHypen = false;
648 if (hasWordChars && style != newStyle) flushWord();
649 putChars(' ');
650 } else if (ch == 0x0ad) {
651 // soft hyphen
652 if (!lastWasSoftHypen && hasWordChars) {
653 putChars('-');
654 lastWasSoftHypen = true; // word flusher is using this flag
655 flushWord();
657 lastWasSoftHypen = true;
658 } else if (ch <= ' ' || isWhite(ch)) {
659 lastWasSoftHypen = false;
660 if (hasWordChars) {
661 flushWord();
662 auto lw = words+wordsUsed-1;
663 lw.propsOrig.canbreak = true;
664 lw.propsOrig.spaced = true;
665 } else {
666 style = newStyle;
668 } else {
669 lastWasSoftHypen = false;
670 if (ch > dchar.max || ch.isSurrogate || ch.isPrivateUse || ch.isNonCharacter || ch.isMark || ch.isFormat || ch.isControl) ch = '?';
671 if (hasWordChars && style != newStyle) flushWord();
672 putChars(ch);
673 if (isDash(ch) && charsUsed-lastWordStart > 1 && !isDash(ltext[charsUsed-2])) flushWord();
678 void finalize () {
679 flushWord();
680 flushLines();
683 public:
684 void save (VFile fl) {
685 fl.rawWriteExact("");
688 public:
689 void dump (VFile fl) const {
690 fl.writeln("LINES: ", linesUsed);
691 foreach (immutable idx, const ref ln; lines[0..linesUsed]) {
692 fl.writeln("LINE #", idx, ": ", ln.wordCount, " words; just=", ln.just.toString, "; jlpad=", ln.just.lpad, "; y=", ln.y, "; h=", ln.h, "; desc=", ln.desc);
693 foreach (immutable widx, const ref w; words[ln.wstart..ln.wend]) {
694 fl.writeln(" WORD #", widx, "(", w.wordNum, ")[", w.wstart, "..", w.wend, "]: ", wordText(w));
695 fl.writeln(" wbreak=", w.props.canbreak, "; wspaced=", w.props.spaced, "; whyphen=", w.props.hyphen, "; style=", w.style.toString);
696 fl.writeln(" x=", w.x, "; w=", w.w, "; h=", w.h, "; asc=", w.asc, "; desc=", w.desc);
701 private:
702 static bool isDash (dchar ch) {
703 pragma(inline, true);
704 return (ch == '-' || (ch >= 0x2013 && ch == 0x2015) || ch == 0x2212);
707 void flushWord () {
708 if (hasWordChars) {
709 auto w = allocWord();
710 w.wstart = lastWordStart;
711 w.wend = charsUsed;
712 //{ import iv.encoding, std.conv : to; writeln("adding word: [", wordText(*w).to!string.recodeToKOI8, "]"); }
713 w.propsOrig.hyphen = lastWasSoftHypen;
714 if (lastWasSoftHypen) {
715 w.propsOrig.canbreak = true;
716 w.propsOrig.spaced = false;
717 --w.wend; // remove hyphen mark (for now)
719 w.style = style;
720 w.props = w.propsOrig;
721 w.props.hyphen = false;
722 // set word dimensions
723 if (w.style.fontface < 0) throw new Exception("invalid font face in word style");
724 laf.setFont(w.style);
725 // i may need spacing later, and anyway most words should be with spacing, so calc it unconditionally
726 if (w.wend > w.wstart) {
727 auto t = wordText(*w);
728 laf.textWidth2(t, &w.w, &w.wsp, (w.propsOrig.hyphen ? &w.whyph : null));
729 if (!w.propsOrig.hyphen) w.whyph = w.w;
730 if (isDash(t[$-1])) { w.propsOrig.canbreak = true; w.props.canbreak = true; }
731 } else {
732 w.w = w.wsp = w.whyph = 0;
734 // calculate ascent, descent and height
735 laf.textMetrics(&w.asc, &w.desc, &w.h);
736 w.just = just;
737 lastWordStart = charsUsed;
739 style = newStyle;
742 void flushLines () {
743 uint curw = firstWordNotFlushed;
744 uint endw = wordsUsed;
745 if (curw < endw) {
746 debug(xlay_line_flush) writeln("flushing ", endw-curw, " words");
747 uint stline = linesUsed; // reformat from this
748 // fix word styles
749 foreach (ref LayWord w; words[curw..endw]) {
750 if (w.props.hyphen) --w.wend; // remove hyphen mark
751 w.props = w.propsOrig;
752 w.props.hyphen = false;
754 LayLine* ln;
755 LayWord* w = words+curw;
756 while (curw < endw) {
757 debug(xlay_line_flush) writeln(" ", endw-curw, " words left");
758 if (ln is null) {
759 // add line to work with
760 ln = allocLine();
761 ln.wstart = ln.wend = curw;
762 ln.just = w.just;
763 ln.w = just.lpad+just.rpad;
764 // indent first line of paragraph
765 if (firstParaLine) {
766 firstParaLine = false;
767 // left-side or justified lines has paragraph indent
768 if (ln.just.paraIndent > 0 && (just.left || just.justify)) {
769 laf.setFont(w.style);
770 int ind = laf.spacesWidth(ln.just.paraIndent);
771 ln.w += ind;
772 ln.just.lpad += ind;
775 //writeln("new line; maxWidth=", maxWidth, "; starting line width=", ln.w);
777 debug(xlay_line_flush) writefln(" (%s:0x%04x) 0x%08x : 0x%08x : 0x%08x : %s", LayLine.sizeof, LayLine.sizeof, cast(uint)lines, cast(uint)ln, cast(uint)(lines+linesUsed-1), cast(int)(ln-((lines+linesUsed-1))));
778 // add words until i hit breaking point
779 // if it will end beyond maximum width, and this line
780 // has some words, flush the line and start new one
781 uint startIndex = curw;
782 int curwdt = ln.w, lastwsp = 0;
783 while (curw < endw) {
784 // add word width with spacing (i will compensate for that after loop)
785 lastwsp = (w.propsOrig.spaced ? w.wsp-w.w : 0);
786 curwdt += w.w+lastwsp;
787 ++curw; // advance counter here...
788 if (w.props.canbreak) break; // done with this span
789 ++w; // ...and word pointer here (skipping one inc at the end ;-)
791 debug(xlay_line_flush) writeln(" ", curw-startIndex, " words processed");
792 // can i add the span? if this is first span in line, add it unconditionally
793 if (ln.wordCount == 0 || curwdt-lastwsp <= maxWidth) {
794 // yay, i can!
795 ln.wend = curw;
796 ln.w = curwdt;
797 ++w; // advance to curw
798 debug(xlay_line_flush) writeln("curwdt=", curwdt, "; maxWidth=", maxWidth, "; wc=", ln.wordCount, "(", ln.wend-ln.wstart, ")");
799 } else {
800 // nope, start new line here
801 debug(xlay_line_flush) writeln("added line with ", ln.wordCount, " words");
802 // last word in the line should not be spaced
803 auto ww = words+ln.wend-1;
804 // compensate for spacing at last word
805 ln.w -= lastwsp;
806 ww.props.spaced = false;
807 // and should have hyphen mark if it is necessary
808 if (ww.propsOrig.hyphen) {
809 assert(!ww.props.hyphen);
810 ww.props.hyphen = true;
811 ++ww.wend;
812 // fix line width (word layouter will use that)
813 ln.w += ww.whyph-ww.w;
815 ln = null;
816 curw = startIndex;
817 w = words+curw;
820 debug(xlay_line_flush) writeln("added line with ", ln.wordCount, " words; new lines range: [", stline, "..", linesUsed, "]");
821 debug(xlay_line_flush) writefln("(%s:0x%04x) 0x%08x : 0x%08x : 0x%08x : %s", LayLine.sizeof, LayLine.sizeof, cast(uint)lines, cast(uint)ln, cast(uint)(lines+linesUsed-1), cast(int)(ln-((lines+linesUsed-1))));
822 // last line should not be justified
823 if (just.justify) ln.just.setLeft;
824 // do real word layouting and fix line metrics
825 debug(xlay_line_flush) writeln("added ", linesUsed-stline, " lines");
826 foreach (uint lidx; stline..linesUsed) {
827 debug(xlay_line_flush) writeln(": lidx=", lidx, "; wc=", lines[lidx].wordCount);
828 layoutLine(lidx);
830 firstWordNotFlushed = endw;
834 // do word layouting and fix line metrics
835 void layoutLine (uint lidx) {
836 import std.algorithm : max, min;
837 assert(lidx < linesUsed);
838 auto ln = lines+lidx;
839 debug(xlay_line_layout) writeln("lidx=", lidx, "; wc=", ln.wordCount);
840 // y position
841 ln.y = (lidx ? ln[-1].y+ln[-1].h : 0);
842 auto lwords = lineWords(lidx);
843 assert(!lwords.empty); // i should have at least one word in each line
844 // line width is calculated for us by `flushLines()`
845 // calculate line metrics and number of words with spacing
846 int lineH, lineDesc, wspCount;
847 foreach (ref LayWord w; lwords.save) {
848 lineH = max(lineH, w.h);
849 lineDesc = min(lineDesc, w.desc);
850 if (w.props.spaced) ++wspCount;
852 // vertical padding; clamp it, as i can't have line over line (it will break too many things)
853 lineH += max(0, ln.just.tpad)+max(0, ln.just.bpad);
854 ln.h = lineH;
855 ln.desc = lineDesc;
856 if (ln.w >= maxWidth) {
857 // way too long; (almost) easy deal
858 // calculate free space to spare in case i'll need to compensate hyphen mark
859 int x = ln.just.lpad, spc = 0;
860 foreach (ref LayWord w; lwords.save) {
861 w.x = x;
862 x += w.fullwidth;
863 if (w.props.spaced) spc += w.wsp-w.w;
865 // if last word ends with hyphen, try to compensate it
866 if (words[ln.wend-1].props.hyphen) {
867 int needspc = ln.w-maxWidth;
868 // no more than 8 pix or 2/3 of free space
869 if (needspc <= 8 && needspc <= spc/3*2) {
870 // compensate (i can do fractional math here, but meh...)
871 while (needspc > 0) {
872 // excellence in coding!
873 foreach_reverse (immutable widx; ln.wstart..ln.wend) {
874 if (words[widx].props.spaced) {
875 --ln.w;
876 foreach (immutable c; widx+1..ln.wend) words[c].x -= 1;
877 if (--needspc == 0) break;
883 } else if (ln.just.justify && wspCount > 0) {
884 // fill the whole line
885 int spc = maxWidth-ln.w; // space left to distribute
886 int xadvsp = spc/wspCount;
887 int frac = spc-xadvsp*wspCount;
888 int x = ln.just.lpad;
889 // no need to save range here, i'll do it in one pass
890 foreach (ref LayWord w; lwords) {
891 w.x = x;
892 x += w.fullwidth;
893 if (w.props.spaced) {
894 x += xadvsp;
895 if (frac-- > 0) ++x;
898 } else {
899 int x;
900 if (ln.just.left || ln.just.justify) x = ln.just.lpad;
901 else if (ln.just.right) x = maxWidth-ln.w+ln.just.lpad;
902 else if (ln.just.center) x = (maxWidth-(ln.w-ln.just.lpad-ln.just.rpad))/2;
903 else assert(0, "wtf?!");
904 // no need to save range here, i'll do it in one pass
905 foreach (ref LayWord w; lwords) {
906 w.x += x;
907 x += w.fullwidth;
910 if (ln.h < 1) ln.h = 1;
911 textWidth = max(textWidth, ln.w);
912 textHeight = ln.y+ln.h;
913 debug(xlay_line_layout) writeln("lidx=", lidx, "; wc=", ln.wordCount);