"adapted" to shitdoze; adapted to new iv.vfs API
[xreader.git] / xlayouter.d
blob544f1532a4fd8e2800c03b1b87fd9a142ab5096d
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;
33 version(Windows) {
34 private int lrintf (float f) { pragma(inline, true); return cast(int)(f+0.5); }
35 } else {
36 import core.stdc.math : lrintf;
39 // ////////////////////////////////////////////////////////////////////////// //
40 abstract class LayObject {
41 abstract int width ();
42 abstract int spacewidth ();
43 abstract int height ();
44 abstract int ascent (); // should be positive
45 abstract int descent (); // should be negative
46 abstract bool canbreak ();
47 abstract bool spaced ();
48 // y is at baseline
49 abstract void draw (NVGContext ctx, float x, float y);
53 // ////////////////////////////////////////////////////////////////////////// //
54 // this needs such fonts in stash:
55 // text -- normal
56 // texti -- italic
57 // textb -- bold
58 // textz -- italic and bold
59 // mono -- normal
60 // monoi -- italic
61 // monob -- bold
62 // monoz -- italic and bold
63 final class LayFontStash {
64 public:
65 FONScontext* fs;
66 // list of known font faces, should be filled by caller when this object created
67 // DO NOT MODIFY!
68 int[string] fontfaces;
69 string[int] fontfaceids;
71 private:
72 bool killFontStash;
73 bool fontWasSet; // to ensure that first call to `setFont()` will do it's work
74 LayFontStyle lastStyle;
76 public:
77 this () {
78 // create new fontstash
79 FONSparams fontParams;
80 fontParams.width = 1024/*NVG_INIT_FONTIMAGE_SIZE*/;
81 fontParams.height = 1024/*NVG_INIT_FONTIMAGE_SIZE*/;
82 fontParams.flags = FONS_ZERO_TOPLEFT;
83 fs = fonsCreateInternal(&fontParams);
84 if (fs is null) throw new Exception("error creating font stash");
85 killFontStash = true;
86 fs.fonsResetAtlas(1024, 1024);
87 fonsSetSpacing(fs, 0);
88 fonsSetBlur(fs, 0);
89 fonsSetAlign(fs, NVGAlign.Left|NVGAlign.Baseline);
92 ~this () { freeFontStash(); }
94 void freeFontStash () {
95 if (killFontStash && fs !is null) {
96 fs.fonsDeleteInternal();
98 killFontStash = false;
99 fs = null;
102 void addFont(T : const(char)[], TP : const(char)[]) (T name, TP path) {
103 static if (is(T == typeof(null))) {
104 throw new Exception("invalid font face name");
105 } else {
106 if (name.length == 0) throw new Exception("invalid font face name");
107 if (name in fontfaces) throw new Exception("duplicate font '"~name.idup~"'");
108 int fid = fs.fonsAddFont(name, path);
109 if (fid < 0) throw new Exception("font '"~name~"' is not found at '"~path.idup~"'");
110 static if (is(T == string)) {
111 fontfaces[name] = fid;
112 fontfaceids[fid] = name;
113 } else {
114 string n = name.idup;
115 fontfaces[n] = fid;
116 fontfaceids[fid] = n;
121 @property int fontFaceId (const(char)[] name) {
122 if (auto fid = name in fontfaces) return *fid;
123 return -1;
126 @property string fontFace (int fid) {
127 if (auto ff = fid in fontfaceids) return *ff;
128 return null;
131 void setFont() (in auto ref LayFontStyle style) {
132 int fsz = style.fontsize;
133 if (fsz < 1) fsz = 1;
134 if (!fontWasSet || fsz != lastStyle.fontsize || style.fontface != lastStyle.fontface) {
135 if (style.fontface != lastStyle.fontface) fonsSetFont(fs, style.fontface);
136 if (fsz != lastStyle.fontsize) fonsSetSize(fs, fsz);
137 lastStyle = style;
138 lastStyle.fontsize = fsz;
142 int textWidth(T) (const(T)[] str) if (is(T == char) || is(T == dchar)) {
143 import std.algorithm : max;
144 float[4] b = void;
145 float adv = fs.fonsTextBounds(0, 0, str, b[]);
146 float w = b[2]-b[0];
147 return lrintf(max(adv, w));
150 int spacesWidth (int count) {
151 if (count < 1) return 0;
152 auto it = FonsTextBoundsIterator(fs, 0, 0);
153 it.put(' ');
154 return lrintf(it.advance*count);
157 void textWidth2(T) (const(T)[] str, int* w=null, int* wsp=null, int* whyph=null) if (is(T == char) || is(T == dchar)) {
158 import std.algorithm : max;
159 if (w is null && wsp is null && whyph is null) return;
160 float minx, maxx;
161 auto it = FonsTextBoundsIterator(fs, 0, 0);
162 it.put(str);
163 if (w !is null) {
164 it.getHBounds(minx, maxx);
165 *w = lrintf(max(it.advance, maxx-minx));
167 if (wsp !is null && whyph is null) {
168 it.put(" ");
169 it.getHBounds(minx, maxx);
170 *wsp = lrintf(max(it.advance, maxx-minx));
171 } else if (wsp is null && whyph !is null) {
172 it.put(cast(dchar)45);
173 it.getHBounds(minx, maxx);
174 *whyph = lrintf(max(it.advance, maxx-minx));
175 } else if (wsp !is null && whyph !is null) {
176 auto sit = it;
177 it.put(" ");
178 it.getHBounds(minx, maxx);
179 *wsp = lrintf(max(it.advance, maxx-minx));
180 sit.put(cast(dchar)45);
181 sit.getHBounds(minx, maxx);
182 *whyph = lrintf(max(sit.advance, maxx-minx));
186 int textHeight () {
187 // use line bounds for height
188 float y0 = void, y1 = void;
189 fs.fonsLineBounds(0, &y0, &y1);
190 return lrintf(y1-y0);
193 void textMetrics (int* asc, int* desc, int* lineh) {
194 float a = void, d = void, h = void;
195 fs.fonsVertMetrics(&a, &d, &h);
196 if (asc !is null) *asc = lrintf(a);
197 if (desc !is null) *desc = lrintf(d);
198 if (lineh !is null) *lineh = lrintf(h);
203 // ////////////////////////////////////////////////////////////////////////// //
204 // generic text style
205 align(1) struct LayFontStyle {
206 align(1):
207 enum Flag : uint {
208 Italic = 1<<0,
209 Bold = 1<<1,
210 Strike = 1<<2,
211 Underline = 1<<3,
212 Overline = 1<<4,
213 Href = 1<<5,
215 ubyte flags; // see above
216 int fontface = -1; // i can't use strings here, as this struct inside LayWord will not be GC-scanned
217 int fontsize;
218 uint color = 0xff000000; // AABBGGRR; AA usually ignored by renderer, but i'll keep it anyway
219 string toString () const {
220 import std.format : format;
221 string res = "font:%s;size:%s;color:0x%08X".format(fontface, fontsize, color);
222 if (flags&Flag.Italic) res ~= ";italic";
223 if (flags&Flag.Bold) res ~= ";bold";
224 if (flags&Flag.Strike) res ~= ";strike";
225 if (flags&Flag.Underline) res ~= ";under";
226 if (flags&Flag.Overline) res ~= ";over";
227 return res;
229 mixin({
230 import std.conv : to;
231 import std.ascii : toLower;
232 string res;
233 foreach (string s; __traits(allMembers, Flag)) {
234 //pragma(msg, s);
235 res ~= "@property bool "~s[0].toLower~s[1..$]~" () const pure nothrow @safe @nogc { pragma(inline, true); return ((flags&Flag."~s~") != 0); }\n";
236 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";
238 return res;
239 }());
240 void resetAttrs () pure nothrow @safe @nogc { pragma(inline, true); flags = 0; }
241 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); }
245 // ////////////////////////////////////////////////////////////////////////// //
246 // line align style
247 align(1) struct LayLineStyle {
248 align(1):
249 enum Justify : ubyte {
250 Left,
251 Right,
252 Center,
253 Justify,
255 Justify mode = Justify.Left;
256 short lpad, rpad, tpad, bpad;
257 ubyte paraIndent; // in spaces
258 string toString () const {
259 import std.format : format;
260 string res;
261 final switch (mode) {
262 case Justify.Left: res = "left"; break;
263 case Justify.Right: res = "right"; break;
264 case Justify.Center: res = "center"; break;
265 case Justify.Justify: res = "justify"; break;
267 if (lpad) res ~= ";lpad:%s".format(lpad);
268 if (rpad) res ~= ";rpad:%s".format(rpad);
269 if (tpad) res ~= ";tpad:%s".format(tpad);
270 if (bpad) res ~= ";bpad:%s".format(bpad);
271 return res;
273 mixin({
274 import std.conv : to;
275 import std.ascii : toLower;
276 string res;
277 foreach (string s; __traits(allMembers, Justify)) {
278 //pragma(msg, s);
279 res ~= "@property bool "~s[0].toLower~s[1..$]~" () const pure nothrow @safe @nogc { pragma(inline, true); return (mode == Justify."~s~"); }\n";
280 res ~= "ref LayLineStyle set"~s~" () pure nothrow @safe @nogc { mode = Justify."~s~"; return this; }\n";
282 return res;
283 }());
284 bool opEquals() (in auto ref LayLineStyle s) const pure nothrow @safe @nogc { pragma(inline, true); return (mode == s.mode && lpad == s.lpad); }
285 @property pure nothrow @safe @nogc {
286 int leftpad () const { pragma(inline, true); return lpad; }
287 void leftpad (int v) { pragma(inline, true); lpad = (v < short.min ? short.min : v > short.max ? short.max : cast(short)v); }
288 int rightpad () const { pragma(inline, true); return rpad; }
289 void rightpad (int v) { pragma(inline, true); rpad = (v < short.min ? short.min : v > short.max ? short.max : cast(short)v); }
290 int toppad () const { pragma(inline, true); return tpad; }
291 void toppad (int v) { pragma(inline, true); tpad = (v < 0 ? 0 : v > short.max ? short.max : cast(short)v); }
292 int bottompad () const { pragma(inline, true); return bpad; }
293 void bottompad (int v) { pragma(inline, true); bpad = (v < 0 ? 0 : v > short.max ? short.max : cast(short)v); }
298 // ////////////////////////////////////////////////////////////////////////// //
299 // layouted text word
300 align(1) struct LayWord {
301 align(1):
302 static align(1) struct Props {
303 align(1):
304 enum Flag : uint {
305 CanBreak = 1<<0, // can i break line at this word?
306 Spaced = 1<<1, // should this word be whitespaced at the end?
307 Hypen = 1<<2, // if i'll break at this word, should i add hyphen mark?
308 LineEnd = 1<<3, // this word ends current line
309 ParaEnd = 1<<4, // this word ends current paragraph (and, implicitly, line)
310 Object = 1<<5, // dchar at wstart is actually object index in object array
312 ubyte flags; // see above
313 @property pure nothrow @safe @nogc:
314 bool canbreak () const { pragma(inline, true); return ((flags&Flag.CanBreak) != 0); }
315 void canbreak (bool v) { pragma(inline, true); if (v) flags |= Flag.CanBreak; else flags &= ~Flag.CanBreak; }
316 bool spaced () const { pragma(inline, true); return ((flags&Flag.Spaced) != 0); }
317 void spaced (bool v) { pragma(inline, true); if (v) flags |= Flag.Spaced; else flags &= ~Flag.Spaced; }
318 bool hyphen () const { pragma(inline, true); return ((flags&Flag.Hypen) != 0); }
319 void hyphen (bool v) { pragma(inline, true); if (v) flags |= Flag.Hypen; else flags &= ~Flag.Hypen; }
320 bool lineend () const { pragma(inline, true); return ((flags&Flag.LineEnd) != 0); }
321 void lineend (bool v) { pragma(inline, true); if (v) flags |= Flag.LineEnd; else flags &= ~Flag.LineEnd; }
322 bool paraend () const { pragma(inline, true); return ((flags&Flag.ParaEnd) != 0); }
323 void paraend (bool v) { pragma(inline, true); if (v) flags |= Flag.ParaEnd; else flags &= ~Flag.ParaEnd; }
324 // note that if word is softhyphen candidate, i have hyphen mark at [wend]
325 // if props.hyphen is set, wend is including that mark, otherwise it isn't
326 bool object () const { pragma(inline, true); return ((flags&Flag.Object) != 0); }
327 void object (bool v) { pragma(inline, true); if (v) flags |= Flag.Object; else flags &= ~Flag.Object; }
329 uint wstart, wend; // in LayText text buffer
330 LayFontStyle style; // font style
331 uint wordNum; // word number (index in LayText word array)
332 Props propsOrig; // original properties, used for relayouting
333 // calculated values
334 Props props; // effective props after layouting
335 int x; // horizontal word position in line
336 int h; // word height (full)
337 int asc; // ascent (positive)
338 int desc; // descent (negative)
339 int w; // word width, without hyphen and spacing
340 int wsp; // word width with spacing (i.e. with space added at the end)
341 int whyph; // word width with hyphen (i.e. with hyphen mark added at the end)
342 @property int width () const pure nothrow @safe @nogc { pragma(inline, true); return (props.hyphen ? whyph : w); }
343 // width with spacing/hyphen
344 @property int fullwidth () const pure nothrow @safe @nogc { pragma(inline, true); return (props.hyphen ? whyph : props.spaced ? wsp : w); }
345 // space width based on original props
346 @property int spacewidth () const pure nothrow @safe @nogc { pragma(inline, true); return (propsOrig.spaced ? wsp-w : 0); }
347 //FIXME: find better place for this! keep that in separate pool, or something, and look there with word index
348 LayLineStyle just;
349 int paraPad; // to not recalcuate it on each relayouting; set to -1 to recalculate ;-)
350 @property int objectIdx () const pure nothrow @safe @nogc { pragma(inline, true); return (propsOrig.object ? wstart : -1); }
351 //int userTag;
355 // ////////////////////////////////////////////////////////////////////////// //
356 // layouted text line
357 struct LayLine {
358 uint wstart, wend; // indicies in word array
359 LayLineStyle just; // line style
360 // calculated properties
361 int x, y, w; // starting x and y positions, width
362 // on finish, layouter will calculate minimal ('cause it is negative) descent
363 int h, desc; // height, descent (negative)
364 @property int wordCount () const pure nothrow @safe @nogc { pragma(inline, true); return cast(int)(wend-wstart); }
368 // ////////////////////////////////////////////////////////////////////////// //
369 // layouted text
370 final class LayText {
371 public:
372 // special control characters
373 enum dchar EndLineCh = 0x2028; // 0x0085 is treated like whitespace
374 enum dchar EndParaCh = 0x2029;
376 private:
377 void ensurePool(ubyte pow2, bool clear, T) (uint want, ref T* ptr, ref uint used, ref uint alloced) {
378 if (want == 0) return;
379 static assert(pow2 < 24, "wtf?!");
380 uint cursz = used*cast(uint)T.sizeof;
381 if (cursz >= int.max/2) throw new Exception("pool overflow");
382 auto lsz = cast(ulong)want*T.sizeof;
383 if (lsz >= int.max/2 || lsz+cursz >= int.max/2) throw new Exception("pool overflow");
384 want = cast(uint)lsz;
385 uint cural = alloced*cast(uint)T.sizeof;
386 if (cursz+want > cural) {
387 import core.stdc.stdlib : realloc;
388 // grow it
389 uint newsz = ((cursz+want)|((1<<pow2)-1))+1;
390 if (newsz >= int.max/2) throw new Exception("pool overflow");
391 auto np = cast(T*)realloc(ptr, newsz);
392 if (np is null) throw new Exception("out of memory for pool");
393 static if (clear) {
394 import core.stdc.string : memset;
395 memset(np+used, 0, newsz-cursz);
397 ptr = np;
398 alloced = newsz/cast(uint)T.sizeof;
402 dchar* ltext;
403 uint charsUsed, charsAllocated;
405 void putChars (const(dchar)[] str...) {
406 import core.stdc.string : memcpy;
407 if (str.length == 0) return;
408 if (str.length > int.max/2) throw new Exception("text too big");
409 ensurePool!(16, false)(str.length, ltext, charsUsed, charsAllocated);
410 memcpy(ltext+charsUsed, str.ptr, cast(uint)str.length*cast(uint)ltext[0].sizeof);
411 charsUsed += cast(uint)str.length;
414 LayWord* words;
415 uint wordsUsed, wordsAllocated;
417 LayWord* allocWord(bool clear=false) () {
418 ensurePool!(16, true)(1, words, wordsUsed, wordsAllocated);
419 auto res = words+wordsUsed;
420 static if (clear) {
421 import core.stdc.string : memset;
422 memset(res, 0, (*res).sizeof);
424 res.wordNum = wordsUsed++;
425 //res.userTag = wordTag;
426 return res;
429 LayLine* lines;
430 uint linesUsed, linesAllocated;
432 LayLine* allocLine(bool clear=false) () {
433 ensurePool!(16, true)(1, lines, linesUsed, linesAllocated);
434 static if (clear) {
435 import core.stdc.string : memset;
436 auto res = lines+(linesUsed++);
437 memset(res, 0, (*res).sizeof);
438 return res;
439 } else {
440 return lines+(linesUsed++);
444 LayLine* lastLine () { pragma(inline, true); return (linesUsed > 0 ? lines+linesUsed-1 : null); }
446 bool lastLineHasWords () { pragma(inline, true); return (linesUsed > 0 ? (lines[linesUsed-1].wend > lines[linesUsed-1].wstart) : false); }
448 // should not be called when there are no lines, or no words in last line
449 LayWord* lastLineLastWord () { pragma(inline, true); return words+lastLine.wend-1; }
451 static struct StyleStackItem {
452 LayFontStyle fs;
453 LayLineStyle ls;
455 StyleStackItem* styleStack;
456 uint ststackUsed, ststackAllocated;
458 public void pushStyles () {
459 ensurePool!(4, false)(1, styleStack, ststackUsed, ststackAllocated);
460 auto si = styleStack+(ststackUsed++);
461 si.fs = newStyle;
462 si.ls = newJust;
465 public void popStyles () {
466 if (ststackUsed == 0) throw new Exception("style stack underflow");
467 auto si = styleStack+(--ststackUsed);
468 newStyle = si.fs;
469 newJust = si.ls;
472 private:
473 bool firstParaLine = true;
474 uint lastWordStart; // in fulltext
475 uint firstWordNotFlushed;
477 @property bool hasWordChars () const pure nothrow @safe @nogc { pragma(inline, true); return (lastWordStart < charsUsed); }
479 private:
480 // current attributes
481 LayLineStyle just; // for current paragraph
482 LayFontStyle style;
483 // user can change this alot, so don't apply that immediately
484 LayFontStyle newStyle;
485 LayLineStyle newJust;
487 private:
488 Utf8Decoder dec;
489 bool lastWasUtf;
490 bool lastWasSoftHypen;
491 int maxWidth; // maximum text width
492 LayFontStash laf;
494 public:
495 int textHeight = 0; // total text height
496 int textWidth = 0; // maximum text width
497 //int wordTag = 0; // you are free to change this
499 public:
500 // compare function should return (roughly): key-l
501 alias CmpFn = int delegate (LayLine* l) nothrow @nogc;
503 int findLineBinary (scope CmpFn cmpfn) {
504 if (linesUsed == 0) return -1;
505 int bot = 0, i = cast(int)linesUsed-1;
506 while (bot != i) {
507 int mid = i-(i-bot)/2;
508 int cmp = cmpfn(lines+mid);
509 if (cmp < 0) i = mid-1;
510 else if (cmp > 0) bot = mid;
511 else return mid;
513 return (cmpfn(lines+i) == 0 ? i : -1);
516 // find line with this word index
517 int findLineWithWord (uint idx) {
518 return findLineBinary((LayLine* l) {
519 if (idx < l.wstart) return -1;
520 if (idx >= l.wend) return 1;
521 return 0;
525 // find line which contains this coordinate
526 int findLineAtY (int y) {
527 if (linesUsed == 0) return 0;
528 if (y < 0) return 0;
529 if (y >= textHeight) return cast(int)linesUsed-1;
530 auto res = findLineBinary((LayLine* l) {
531 if (y < l.y) return -1;
532 if (y >= l.y+l.h) return 1;
533 return 0;
535 //if (res == -1) { import std.stdio; writeln("*** y=", y, "; th=", textHeight); }
536 assert(res != -1);
537 return res;
540 int findWordAtX (LayLine* ln, int x) {
541 int wcmp (int wnum) {
542 auto w = words+ln.wstart+wnum;
543 if (x < w.x) return -1;
544 return (x >= (wnum+1 < ln.wordCount ? w[1].x : w.x+w.w) ? 1 : 0);
546 if (ln is null || ln.wordCount == 0) return -1;
547 int bot = 0, i = ln.wordCount-1;
548 while (bot != i) {
549 int mid = i-(i-bot)/2;
550 switch (wcmp(mid)) {
551 case -1: i = mid-1; break;
552 case 1: bot = mid; break;
553 default: return ln.wstart+mid;
556 return (wcmp(i) == 0 ? ln.wstart+i : -1);
559 // find word at the given coordinates
560 int wordAtXY (int x, int y) {
561 auto lidx = findLineAtY(y);
562 if (lidx < 0) return -1;
563 auto ln = lines+lidx;
564 if (y < ln.y || y >= ln.y+ln.h || ln.wordCount == 0) return -1;
565 return findWordAtX(ln, x);
568 LayWord* wordByIndex (uint idx) pure nothrow @trusted @nogc { pragma(inline, true); return (idx < wordsUsed ? words+idx : null); }
570 @property const(dchar)[] wordText (in ref LayWord w) const pure nothrow @trusted @nogc { pragma(inline, true); return ltext[w.wstart..w.wend]; }
572 @property int lineCount () const pure nothrow @safe @nogc { pragma(inline, true); return cast(int)linesUsed; }
574 // word iterator
575 @property auto lineWords (int lidx) {
576 static struct Range {
577 private:
578 LayWord* w;
579 int wordsLeft; // not including current
580 nothrow @trusted @nogc:
581 private:
582 this (LayText lay, int lidx) {
583 if (lidx >= 0 && lidx < lay.linesUsed) {
584 auto ln = lay.lines+lidx;
585 if (ln.wend > ln.wstart) {
586 w = lay.words+ln.wstart;
587 wordsLeft = ln.wend-ln.wstart-1;
591 public:
592 @property bool empty () const pure { pragma(inline, true); return (w is null); }
593 //@property ref LayWord front () pure { pragma(inline, true); assert(w !is null); return *w; }
594 @property ref LayWord front () pure { pragma(inline, true); assert(w !is null); return *w; }
595 void popFront () { if (wordsLeft) { ++w; --wordsLeft; } else w = null; }
596 Range save () { Range res = void; res.w = w; res.wordsLeft = wordsLeft; return res; }
597 @property int length () const pure { pragma(inline, true); return (w !is null ? wordsLeft+1 : 0); }
598 alias opDollar = length;
599 @property LayWord[] opSlice () { return (w !is null ? w[0..wordsLeft+1] : null); }
600 @property LayWord[] opSlice (int lo, int hi) {
601 if (lo < 0) lo = 0;
602 if (w is null || hi <= lo || lo > wordsLeft) return null;
603 if (hi > wordsLeft+1) hi = wordsLeft+1;
604 return w[lo..hi];
607 return Range(this, lidx);
610 LayLine* line (int lidx) { pragma(inline, true); return (lidx >= 0 && lidx < linesUsed ? lines+lidx : null); }
612 public:
613 LayObject[] objects;
615 public:
616 this (LayFontStash alaf, int awidth) {
617 if (alaf is null) assert(0, "no layout fonts");
618 if (awidth < 1) awidth = 1;
619 laf = alaf;
620 maxWidth = awidth;
623 ~this () { freeMemory(); }
625 void freeMemory () {
626 import core.stdc.stdlib : free;
627 if (lines !is null) { free(lines); lines = null; }
628 if (words !is null) { free(words); words = null; }
629 if (ltext !is null) { free(ltext); ltext = null; }
630 wordsUsed = wordsAllocated = linesUsed = linesAllocated = charsUsed = charsAllocated = 0;
633 @property int width () const pure nothrow @safe @nogc { pragma(inline, true); return maxWidth; }
635 // last flushed word index
636 @property uint lastWordIndex () const pure nothrow @safe @nogc { pragma(inline, true); return (wordsUsed ? wordsUsed-1 : 0); }
638 // current word index
639 @property uint nextWordIndex () const pure nothrow @safe @nogc { pragma(inline, true); return wordsUsed+hasWordChars; }
641 @property ref LayFontStyle fontStyle () pure nothrow @safe @nogc { pragma(inline, true); return newStyle; }
642 @property ref LayLineStyle lineStyle () pure nothrow @safe @nogc { pragma(inline, true); return newJust; }
644 @property int fontFaceId (const(char)[] name) {
645 if (laf !is null) {
646 int fid = laf.fontFaceId(name);
647 if (fid >= 0) return fid;
649 throw new Exception("unknown font face '"~name.idup~"'");
652 @property string fontFace (int fid) { pragma(inline, true); return (laf !is null ? laf.fontFace(fid) : null); }
654 void endLine () { put(EndLineCh); }
655 void endPara () { put(EndParaCh); }
657 void putObject (LayObject obj) {
658 flushWord();
659 lastWasSoftHypen = false;
660 if (obj is null) return;
661 if (objects.length >= int.max/2) throw new Exception("too many objects");
662 just = newJust;
663 // create special word
664 auto w = allocWord();
665 w.wstart = cast(dchar)objects.length; // store object index
666 w.wend = 0;
667 objects ~= obj;
668 w.style = style;
669 w.propsOrig.object = true;
670 w.propsOrig.spaced = obj.spaced;
671 w.propsOrig.canbreak = obj.canbreak;
672 w.props = w.propsOrig;
673 w.w = obj.width;
674 w.whyph = w.wsp = w.w+obj.spacewidth;
675 w.h = obj.height;
676 w.asc = obj.ascent;
677 if (w.asc < 0) throw new Exception("object ascent should be positive");
678 w.desc = obj.descent;
679 if (w.desc > 0) throw new Exception("object descent should be negative");
680 w.just = just;
681 w.paraPad = -1;
684 // add text to layouter
685 void put(T) (const(T)[] str...) if (is(T == char) || is(T == dchar)) {
686 if (str.length == 0) return;
688 dchar curCh; // 0: no more chars
689 usize stpos;
691 static if (is(T == char)) {
692 // utf-8 stream
693 if (!lastWasUtf) { lastWasUtf = true; dec.reset; }
694 void skipCh () @trusted {
695 while (stpos < str.length) {
696 curCh = dec.decode(cast(ubyte)str.ptr[stpos++]);
697 if (curCh <= dchar.max) return;
699 curCh = 0;
701 // load first char
702 skipCh();
703 } else {
704 // dchar stream
705 void skipCh () @trusted {
706 if (stpos < str.length) {
707 curCh = str.ptr[stpos++];
708 if (curCh > dchar.max) curCh = '?';
709 } else {
710 curCh = 0;
713 // load first char
714 if (lastWasUtf) {
715 lastWasUtf = false;
716 if (!dec.complete) curCh = '?'; else skipCh();
717 } else {
718 skipCh();
722 // process stream dchars
723 if (curCh == 0) return;
724 if (!hasWordChars) style = newStyle;
725 if (firstWordNotFlushed >= wordsUsed) just = newJust;
726 while (curCh) {
727 import std.uni;
728 dchar ch = curCh;
729 skipCh();
730 if (ch == EndLineCh || ch == EndParaCh) {
731 lastWasSoftHypen = false;
732 // ignore leading empty lines
733 if (hasWordChars || linesUsed) {
734 LayWord* lw;
735 if (hasWordChars) {
736 // has some word data, flush it now
737 flushWord();
738 lw = words+wordsUsed-1;
739 } else if (wordsUsed == 0) {
740 // create empty word to set attributes on it
741 //lw = allocWord();
742 } else {
743 lw = words+wordsUsed-1;
744 // do i need to add empty word for attrs?
745 if (lw.propsOrig.lineend || lw.propsOrig.paraend) lw = createEmptyWord();
747 // fix word properties
748 if (lw !is null) {
749 lw.propsOrig.canbreak = true;
750 lw.propsOrig.spaced = false;
751 lw.propsOrig.hyphen = false;
752 lw.propsOrig.lineend = (ch == EndLineCh);
753 lw.propsOrig.paraend = (ch == EndParaCh);
754 //if (lw.wend == lw.wstart) putChars(' ');
756 flushWord();
757 // build layout part
758 flushLines(firstWordNotFlushed, wordsUsed);
759 firstWordNotFlushed = wordsUsed;
761 /*if (ch == EndParaCh)*/ just = newJust;
762 firstParaLine = (ch == EndParaCh);
763 } else if (ch == 0x00a0) {
764 // non-breaking space
765 lastWasSoftHypen = false;
766 if (hasWordChars && style != newStyle) flushWord();
767 putChars(' ');
768 } else if (ch == 0x0ad) {
769 // soft hyphen
770 if (!lastWasSoftHypen && hasWordChars) {
771 putChars('-');
772 lastWasSoftHypen = true; // word flusher is using this flag
773 flushWord();
775 lastWasSoftHypen = true;
776 } else if (ch <= ' ' || isWhite(ch)) {
777 lastWasSoftHypen = false;
778 if (hasWordChars) {
779 flushWord();
780 auto lw = words+wordsUsed-1;
781 lw.propsOrig.canbreak = true;
782 lw.propsOrig.spaced = true;
783 } else {
784 style = newStyle;
786 } else {
787 lastWasSoftHypen = false;
788 if (ch > dchar.max || ch.isSurrogate || ch.isPrivateUse || ch.isNonCharacter || ch.isMark || ch.isFormat || ch.isControl) ch = '?';
789 if (hasWordChars && style != newStyle) flushWord();
790 putChars(ch);
791 if (isDash(ch) && charsUsed-lastWordStart > 1 && !isDash(ltext[charsUsed-2])) flushWord();
796 void finalize () {
797 flushWord();
798 flushLines(firstWordNotFlushed, wordsUsed);
799 firstWordNotFlushed = wordsUsed;
802 void relayout (int newWidth, bool forced) {
803 if (newWidth < 1) newWidth = 1;
804 if (!forced && newWidth == maxWidth) return;
805 maxWidth = newWidth;
806 linesUsed = 0;
807 if (linesAllocated > 0) {
808 import core.stdc.string : memset;
809 memset(lines, 0, linesAllocated*lines[0].sizeof);
811 uint widx = 0;
812 uint wu = wordsUsed;
813 textWidth = 0;
814 textHeight = 0;
815 firstParaLine = true;
816 scope(exit) firstWordNotFlushed = wu;
817 while (widx < wu) {
818 uint lend = widx;
819 while (lend < wu) {
820 auto w = words+(lend++);
821 if (w.propsOrig.lineend || w.propsOrig.paraend) break;
823 flushLines(widx, lend);
824 //assert(wordsUsed == lend);
825 widx = lend;
826 if (words[widx-1].propsOrig.paraend) firstParaLine = true;
830 public:
831 void save (VFile fl) {
832 fl.rawWriteExact("");
835 public:
836 void dump (VFile fl) const {
837 fl.writeln("LINES: ", linesUsed);
838 foreach (immutable idx, const ref ln; lines[0..linesUsed]) {
839 fl.writeln("LINE #", idx, ": ", ln.wordCount, " words; just=", ln.just.toString, "; jlpad=", ln.just.lpad, "; y=", ln.y, "; h=", ln.h, "; desc=", ln.desc);
840 foreach (immutable widx, const ref w; words[ln.wstart..ln.wend]) {
841 fl.writeln(" WORD #", widx, "(", w.wordNum, ")[", w.wstart, "..", w.wend, "]: ", wordText(w));
842 fl.writeln(" wbreak=", w.props.canbreak, "; wspaced=", w.props.spaced, "; whyphen=", w.props.hyphen, "; style=", w.style.toString);
843 fl.writeln(" x=", w.x, "; w=", w.w, "; h=", w.h, "; asc=", w.asc, "; desc=", w.desc);
848 private:
849 static bool isDash (dchar ch) {
850 pragma(inline, true);
851 return (ch == '-' || (ch >= 0x2013 && ch == 0x2015) || ch == 0x2212);
854 LayWord* createEmptyWord () {
855 assert(!hasWordChars);
856 auto w = allocWord();
857 w.style = style;
858 w.props = w.propsOrig;
859 // set word dimensions
860 if (w.style.fontface < 0) throw new Exception("invalid font face in word style");
861 laf.setFont(w.style);
862 w.w = w.wsp = w.whyph = 0;
863 // calculate ascent, descent and height
864 laf.textMetrics(&w.asc, &w.desc, &w.h);
865 w.just = just;
866 w.paraPad = -1;
867 style = newStyle;
868 return w;
871 void flushWord () {
872 if (hasWordChars) {
873 auto w = allocWord();
874 w.wstart = lastWordStart;
875 w.wend = charsUsed;
876 //{ import iv.encoding, std.conv : to; writeln("adding word: [", wordText(*w).to!string.recodeToKOI8, "]"); }
877 w.propsOrig.hyphen = lastWasSoftHypen;
878 if (lastWasSoftHypen) {
879 w.propsOrig.canbreak = true;
880 w.propsOrig.spaced = false;
881 --w.wend; // remove hyphen mark (for now)
883 w.style = style;
884 w.props = w.propsOrig;
885 w.props.hyphen = false;
886 // set word dimensions
887 if (w.style.fontface < 0) throw new Exception("invalid font face in word style");
888 laf.setFont(w.style);
889 // i may need spacing later, and anyway most words should be with spacing, so calc it unconditionally
890 if (w.wend > w.wstart) {
891 auto t = wordText(*w);
892 laf.textWidth2(t, &w.w, &w.wsp, (w.propsOrig.hyphen ? &w.whyph : null));
893 if (!w.propsOrig.hyphen) w.whyph = w.w;
894 if (isDash(t[$-1])) { w.propsOrig.canbreak = true; w.props.canbreak = true; }
895 } else {
896 w.w = w.wsp = w.whyph = 0;
898 // calculate ascent, descent and height
899 laf.textMetrics(&w.asc, &w.desc, &w.h);
900 w.just = just;
901 w.paraPad = -1;
902 lastWordStart = charsUsed;
904 style = newStyle;
907 // [curw..endw)"
908 void flushLines (uint curw, uint endw) {
909 if (curw < endw) {
910 debug(xlay_line_flush) writeln("flushing ", endw-curw, " words");
911 uint stline = linesUsed; // reformat from this
912 // fix word styles
913 foreach (ref LayWord w; words[curw..endw]) {
914 if (w.props.hyphen) --w.wend; // remove hyphen mark
915 w.props = w.propsOrig;
916 w.props.hyphen = false;
918 LayLine* ln;
919 LayWord* w = words+curw;
920 while (curw < endw) {
921 debug(xlay_line_flush) writeln(" ", endw-curw, " words left");
922 if (ln is null) {
923 // add line to work with
924 ln = allocLine();
925 ln.wstart = ln.wend = curw;
926 ln.just = w.just;
927 ln.w = just.lpad+just.rpad;
928 // indent first line of paragraph
929 if (firstParaLine) {
930 firstParaLine = false;
931 // left-side or justified lines has paragraph indent
932 if (ln.just.paraIndent > 0 && (just.left || just.justify)) {
933 laf.setFont(w.style);
934 int ind = (w.paraPad < 0 ? laf.spacesWidth(ln.just.paraIndent) : w.paraPad);
935 ln.w += ind;
936 ln.just.lpad += ind;
937 w.paraPad = ind;
938 } else {
939 w.paraPad = 0;
942 //writeln("new line; maxWidth=", maxWidth, "; starting line width=", ln.w);
944 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))));
945 // add words until i hit breaking point
946 // if it will end beyond maximum width, and this line
947 // has some words, flush the line and start new one
948 uint startIndex = curw;
949 int curwdt = ln.w, lastwsp = 0;
950 while (curw < endw) {
951 // add word width with spacing (i will compensate for that after loop)
952 lastwsp = (w.propsOrig.spaced ? w.wsp-w.w : 0);
953 curwdt += w.w+lastwsp;
954 ++curw; // advance counter here...
955 if (w.props.canbreak) break; // done with this span
956 ++w; // ...and word pointer here (skipping one inc at the end ;-)
958 debug(xlay_line_flush) writeln(" ", curw-startIndex, " words processed");
959 // can i add the span? if this is first span in line, add it unconditionally
960 if (ln.wordCount == 0 || curwdt-lastwsp <= maxWidth) {
961 // yay, i can!
962 ln.wend = curw;
963 ln.w = curwdt;
964 ++w; // advance to curw
965 debug(xlay_line_flush) writeln("curwdt=", curwdt, "; maxWidth=", maxWidth, "; wc=", ln.wordCount, "(", ln.wend-ln.wstart, ")");
966 } else {
967 // nope, start new line here
968 debug(xlay_line_flush) writeln("added line with ", ln.wordCount, " words");
969 // last word in the line should not be spaced
970 auto ww = words+ln.wend-1;
971 // compensate for spacing at last word
972 ln.w -= (ww.props.spaced ? ww.wsp-ww.w : 0);
973 ww.props.spaced = false;
974 // and should have hyphen mark if it is necessary
975 if (ww.propsOrig.hyphen) {
976 assert(!ww.props.hyphen);
977 ww.props.hyphen = true;
978 ++ww.wend;
979 // fix line width (word layouter will use that)
980 ln.w += ww.whyph-ww.w;
982 ln = null;
983 curw = startIndex;
984 w = words+curw;
987 debug(xlay_line_flush) writeln("added line with ", ln.wordCount, " words; new lines range: [", stline, "..", linesUsed, "]");
988 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))));
989 // last line should not be justified
990 if (ln.just.justify) ln.just.setLeft;
991 // do real word layouting and fix line metrics
992 debug(xlay_line_flush) writeln("added ", linesUsed-stline, " lines");
993 foreach (uint lidx; stline..linesUsed) {
994 debug(xlay_line_flush) writeln(": lidx=", lidx, "; wc=", lines[lidx].wordCount);
995 layoutLine(lidx);
1000 // do word layouting and fix line metrics
1001 void layoutLine (uint lidx) {
1002 import std.algorithm : max, min;
1003 assert(lidx < linesUsed);
1004 auto ln = lines+lidx;
1005 debug(xlay_line_layout) writeln("lidx=", lidx, "; wc=", ln.wordCount);
1006 // y position
1007 ln.y = (lidx ? ln[-1].y+ln[-1].h : 0);
1008 auto lwords = lineWords(lidx);
1009 assert(!lwords.empty); // i should have at least one word in each line
1010 // line width is calculated for us by `flushLines()`
1011 // calculate line metrics and number of words with spacing
1012 int lineH, lineDesc, wspCount;
1013 foreach (ref LayWord w; lwords.save) {
1014 lineH = max(lineH, w.h);
1015 lineDesc = min(lineDesc, w.desc);
1016 if (w.props.spaced) ++wspCount;
1018 // vertical padding; clamp it, as i can't have line over line (it will break too many things)
1019 lineH += max(0, ln.just.tpad)+max(0, ln.just.bpad);
1020 ln.h = lineH;
1021 ln.desc = lineDesc;
1022 if (ln.w >= maxWidth) {
1023 //writeln("*** ln.w=", ln.w, "; maxWidth=", maxWidth);
1024 // way too long; (almost) easy deal
1025 // calculate free space to spare in case i'll need to compensate hyphen mark
1026 int x = ln.just.lpad, spc = 0;
1027 foreach (ref LayWord w; lwords.save) {
1028 w.x = x;
1029 x += w.fullwidth;
1030 if (w.props.spaced) spc += w.wsp-w.w;
1032 // if last word ends with hyphen, try to compensate it
1033 if (words[ln.wend-1].props.hyphen) {
1034 int needspc = ln.w-maxWidth;
1035 // no more than 8 pix or 2/3 of free space
1036 if (needspc <= 8 && needspc <= spc/3*2) {
1037 // compensate (i can do fractional math here, but meh...)
1038 while (needspc > 0) {
1039 // excellence in coding!
1040 foreach_reverse (immutable widx; ln.wstart..ln.wend) {
1041 if (words[widx].props.spaced) {
1042 --ln.w;
1043 foreach (immutable c; widx+1..ln.wend) words[c].x -= 1;
1044 if (--needspc == 0) break;
1050 } else if (ln.just.justify && wspCount > 0) {
1051 // fill the whole line
1052 int spc = maxWidth-ln.w; // space left to distribute
1053 int xadvsp = spc/wspCount;
1054 int frac = spc-xadvsp*wspCount;
1055 int x = ln.just.lpad;
1056 // no need to save range here, i'll do it in one pass
1057 foreach (ref LayWord w; lwords) {
1058 w.x = x;
1059 x += w.fullwidth;
1060 if (w.props.spaced) {
1061 x += xadvsp;
1062 //spc -= xadvsp;
1063 if (frac-- > 0) {
1064 ++x;
1065 //--spc;
1069 //if (x != maxWidth-ln.just.rpad) writeln("x=", x, "; but it should be ", maxWidth-ln.just.rpad, "; spcleft=", spc, "; ln.w=", ln.w, "; maxWidth=", maxWidth-ln.w);
1070 //assert(x == maxWidth-ln.just.rpad);
1071 } else {
1072 int x;
1073 if (ln.just.left || ln.just.justify) x = ln.just.lpad;
1074 else if (ln.just.right) x = maxWidth-ln.w+ln.just.lpad;
1075 else if (ln.just.center) x = (maxWidth-(ln.w-ln.just.lpad-ln.just.rpad))/2;
1076 else assert(0, "wtf?!");
1077 // no need to save range here, i'll do it in one pass
1078 foreach (ref LayWord w; lwords) {
1079 w.x = x;
1080 x += w.fullwidth;
1083 if (ln.h < 1) ln.h = 1;
1084 textWidth = max(textWidth, ln.w);
1085 textHeight = ln.y+ln.h;
1086 debug(xlay_line_layout) writeln("lidx=", lidx, "; wc=", ln.wordCount);