more shithacks in formatter
[xreader.git] / xlayouter.d
blob3e946c1d45869ad77831882f5ebe6c9332e0503b
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.image;
21 import iv.cmdcon;
22 import iv.nanovg;
23 import iv.nanovg.oui.blendish;
24 import iv.utfutil;
25 import iv.vfs;
26 import iv.vfs.io;
28 import booktext;
30 version(laytest) import iv.encoding;
31 version(aliced) {} else private alias usize = size_t;
32 version(Windows) {
33 private int lrintf (float f) { pragma(inline, true); return cast(int)(f+0.5); }
34 } else {
35 import core.stdc.math : lrintf;
38 // ////////////////////////////////////////////////////////////////////////// //
39 abstract class LayObject {
40 abstract int width ();
41 abstract int spacewidth ();
42 abstract int height ();
43 abstract int ascent (); // should be positive
44 abstract int descent (); // should be negative
45 abstract bool canbreak ();
46 abstract bool spaced ();
47 // y is at baseline
48 abstract void draw (NVGContext ctx, float x, float y);
52 // ////////////////////////////////////////////////////////////////////////// //
53 // this needs such fonts in stash:
54 // text -- normal
55 // texti -- italic
56 // textb -- bold
57 // textz -- italic and bold
58 // mono -- normal
59 // monoi -- italic
60 // monob -- bold
61 // monoz -- italic and bold
62 final class LayFontStash {
63 public:
64 FONScontext* fs;
65 // list of known font faces, should be filled by caller when this object created
66 // DO NOT MODIFY!
67 int[string] fontfaces;
68 string[int] fontfaceids;
70 private:
71 bool killFontStash;
72 bool fontWasSet; // to ensure that first call to `setFont()` will do it's work
73 LayFontStyle lastStyle;
75 public:
76 this () {
77 // create new fontstash
78 FONSparams fontParams;
79 fontParams.width = 1024/*NVG_INIT_FONTIMAGE_SIZE*/;
80 fontParams.height = 1024/*NVG_INIT_FONTIMAGE_SIZE*/;
81 fontParams.flags = FONS_ZERO_TOPLEFT;
82 fs = fonsCreateInternal(&fontParams);
83 if (fs is null) throw new Exception("error creating font stash");
84 killFontStash = true;
85 fs.fonsResetAtlas(1024, 1024);
86 fonsSetSpacing(fs, 0);
87 fonsSetBlur(fs, 0);
88 fonsSetAlign(fs, NVGTextAlign(NVGTextAlign.H.Left, NVGTextAlign.V.Baseline));
91 ~this () { freeFontStash(); }
93 void freeFontStash () {
94 if (killFontStash && fs !is null) {
95 fs.fonsDeleteInternal();
97 killFontStash = false;
98 fs = null;
101 void addFont(T : const(char)[], TP : const(char)[]) (T name, TP path) {
102 static if (is(T == typeof(null))) {
103 throw new Exception("invalid font face name");
104 } else {
105 if (name.length == 0) throw new Exception("invalid font face name");
106 if (name in fontfaces) throw new Exception("duplicate font '"~name.idup~"'");
107 int fid = fs.fonsAddFont(name, path);
108 if (fid < 0) throw new Exception("font '"~name~"' is not found at '"~path.idup~"'");
109 static if (is(T == string)) {
110 fontfaces[name] = fid;
111 fontfaceids[fid] = name;
112 } else {
113 string n = name.idup;
114 fontfaces[n] = fid;
115 fontfaceids[fid] = n;
120 // returns "font id" which can be used in `fontFace()`
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 // this returns "width", "width with trailing whitespace", and "width with trailing hypen"
158 // all `*` args can be omited
159 void textWidth2(T) (const(T)[] str, int* w=null, int* wsp=null, int* whyph=null) if (is(T == char) || is(T == dchar)) {
160 import std.algorithm : max;
161 if (w is null && wsp is null && whyph is null) return;
162 float minx, maxx;
163 auto it = FonsTextBoundsIterator(fs, 0, 0);
164 it.put(str);
165 if (w !is null) {
166 it.getHBounds(minx, maxx);
167 *w = lrintf(max(it.advance, maxx-minx));
169 if (wsp !is null && whyph is null) {
170 it.put(" ");
171 it.getHBounds(minx, maxx);
172 *wsp = lrintf(max(it.advance, maxx-minx));
173 } else if (wsp is null && whyph !is null) {
174 it.put(cast(dchar)45);
175 it.getHBounds(minx, maxx);
176 *whyph = lrintf(max(it.advance, maxx-minx));
177 } else if (wsp !is null && whyph !is null) {
178 auto sit = it;
179 it.put(" ");
180 it.getHBounds(minx, maxx);
181 *wsp = lrintf(max(it.advance, maxx-minx));
182 sit.put(cast(dchar)45);
183 sit.getHBounds(minx, maxx);
184 *whyph = lrintf(max(sit.advance, maxx-minx));
188 int textHeight () {
189 // use line bounds for height
190 float y0 = void, y1 = void;
191 fs.fonsLineBounds(0, &y0, &y1);
192 return lrintf(y1-y0);
195 void textMetrics (int* asc, int* desc, int* lineh) {
196 float a = void, d = void, h = void;
197 fs.fonsVertMetrics(&a, &d, &h);
198 if (asc !is null) *asc = lrintf(a);
199 if (desc !is null) *desc = lrintf(d);
200 if (lineh !is null) *lineh = lrintf(h);
205 // ////////////////////////////////////////////////////////////////////////// //
206 // generic text style
207 align(1) struct LayFontStyle {
208 align(1):
209 enum Flag : uint {
210 Italic = 1<<0,
211 Bold = 1<<1,
212 Strike = 1<<2,
213 Underline = 1<<3,
214 Overline = 1<<4,
215 Href = 1<<5, // this is cross-reference (not actually a style flag, but it somewhat fits here)
217 ubyte flags; // see above
218 int fontface = -1; // i can't use strings here, as this struct inside LayWord will not be GC-scanned
219 int fontsize;
220 uint color = 0xff000000; // AABBGGRR; AA usually ignored by renderer, but i'll keep it anyway
221 string toString () const {
222 import std.format : format;
223 string res = "font:%s;size:%s;color:0x%08X".format(fontface, fontsize, color);
224 if (flags&Flag.Italic) res ~= ";italic";
225 if (flags&Flag.Bold) res ~= ";bold";
226 if (flags&Flag.Strike) res ~= ";strike";
227 if (flags&Flag.Underline) res ~= ";under";
228 if (flags&Flag.Overline) res ~= ";over";
229 return res;
231 mixin({
232 import std.conv : to;
233 import std.ascii : toLower;
234 string res;
235 foreach (string s; __traits(allMembers, Flag)) {
236 //pragma(msg, s);
237 res ~= "@property bool "~s[0].toLower~s[1..$]~" () const pure nothrow @safe @nogc { pragma(inline, true); return ((flags&Flag."~s~") != 0); }\n";
238 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";
240 return res;
241 }());
242 void resetAttrs () pure nothrow @safe @nogc { pragma(inline, true); flags = 0; }
243 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); }
247 // ////////////////////////////////////////////////////////////////////////// //
248 // line align style
249 align(1) struct LayLineStyle {
250 align(1):
251 enum Justify : ubyte {
252 Left,
253 Right,
254 Center,
255 Justify,
257 Justify mode = Justify.Left;
258 short lpad, rpad, tpad, bpad; // paddings; left and right can be negative
259 ubyte paraIndent; // in spaces
260 string toString () const {
261 import std.format : format;
262 string res;
263 final switch (mode) {
264 case Justify.Left: res = "left"; break;
265 case Justify.Right: res = "right"; break;
266 case Justify.Center: res = "center"; break;
267 case Justify.Justify: res = "justify"; break;
269 if (lpad) res ~= ";lpad:%s".format(lpad);
270 if (rpad) res ~= ";rpad:%s".format(rpad);
271 if (tpad) res ~= ";tpad:%s".format(tpad);
272 if (bpad) res ~= ";bpad:%s".format(bpad);
273 return res;
275 mixin({
276 import std.conv : to;
277 import std.ascii : toLower;
278 string res;
279 foreach (string s; __traits(allMembers, Justify)) {
280 //pragma(msg, s);
281 res ~= "@property bool "~s[0].toLower~s[1..$]~" () const pure nothrow @safe @nogc { pragma(inline, true); return (mode == Justify."~s~"); }\n";
282 res ~= "ref LayLineStyle set"~s~" () pure nothrow @safe @nogc { mode = Justify."~s~"; return this; }\n";
284 return res;
285 }());
286 //bool opEquals() (in auto ref LayLineStyle s) const pure nothrow @safe @nogc { pragma(inline, true); return (mode == s.mode && lpad == s.lpad); }
287 @property pure nothrow @safe @nogc {
288 int leftpad () const { pragma(inline, true); return lpad; }
289 void leftpad (int v) { pragma(inline, true); lpad = (v < short.min ? short.min : v > short.max ? short.max : cast(short)v); }
290 int rightpad () const { pragma(inline, true); return rpad; }
291 void rightpad (int v) { pragma(inline, true); rpad = (v < short.min ? short.min : v > short.max ? short.max : cast(short)v); }
292 int toppad () const { pragma(inline, true); return tpad; }
293 void toppad (int v) { pragma(inline, true); tpad = (v < 0 ? 0 : v > short.max ? short.max : cast(short)v); }
294 int bottompad () const { pragma(inline, true); return bpad; }
295 void bottompad (int v) { pragma(inline, true); bpad = (v < 0 ? 0 : v > short.max ? short.max : cast(short)v); }
300 // ////////////////////////////////////////////////////////////////////////// //
301 // layouted text word
302 align(1) struct LayWord {
303 align(1):
304 static align(1) struct Props {
305 align(1):
306 enum Flag : uint {
307 CanBreak = 1<<0, // can i break line at this word?
308 Spaced = 1<<1, // should this word be whitespaced at the end?
309 Hypen = 1<<2, // if i'll break at this word, should i add hyphen mark?
310 LineEnd = 1<<3, // this word ends current line
311 ParaEnd = 1<<4, // this word ends current paragraph (and, implicitly, line)
312 Object = 1<<5, // wstart is actually object index in object array
314 // note that if word is softhyphen candidate, i have hyphen mark at [wend]
315 // if props.hyphen is set, wend is including that mark, otherwise it isn't
316 ubyte flags; // see above
317 @property pure nothrow @safe @nogc:
318 bool canbreak () const { pragma(inline, true); return ((flags&Flag.CanBreak) != 0); }
319 void canbreak (bool v) { pragma(inline, true); if (v) flags |= Flag.CanBreak; else flags &= ~Flag.CanBreak; }
320 bool spaced () const { pragma(inline, true); return ((flags&Flag.Spaced) != 0); }
321 void spaced (bool v) { pragma(inline, true); if (v) flags |= Flag.Spaced; else flags &= ~Flag.Spaced; }
322 bool hyphen () const { pragma(inline, true); return ((flags&Flag.Hypen) != 0); }
323 void hyphen (bool v) { pragma(inline, true); if (v) flags |= Flag.Hypen; else flags &= ~Flag.Hypen; }
324 bool lineend () const { pragma(inline, true); return ((flags&Flag.LineEnd) != 0); }
325 void lineend (bool v) { pragma(inline, true); if (v) flags |= Flag.LineEnd; else flags &= ~Flag.LineEnd; }
326 bool paraend () const { pragma(inline, true); return ((flags&Flag.ParaEnd) != 0); }
327 void paraend (bool v) { pragma(inline, true); if (v) flags |= Flag.ParaEnd; else flags &= ~Flag.ParaEnd; }
328 bool someend () const { pragma(inline, true); return ((flags&(Flag.ParaEnd|Flag.LineEnd)) != 0); }
329 bool object () const { pragma(inline, true); return ((flags&Flag.Object) != 0); }
330 void object (bool v) { pragma(inline, true); if (v) flags |= Flag.Object; else flags &= ~Flag.Object; }
332 uint wstart, wend; // in LayText text buffer
333 LayFontStyle style; // font style
334 uint wordNum; // word number (index in LayText word array)
335 Props propsOrig; // original properties, used for relayouting
336 // calculated values
337 Props props; // effective props after layouting
338 short x; // horizontal word position in line
339 short h; // word height (full)
340 short asc; // ascent (positive)
341 short desc; // descent (negative)
342 short w; // word width, without hyphen and spacing
343 short wsp; // word width with spacing (i.e. with space added at the end)
344 short whyph; // word width with hyphen (i.e. with hyphen mark added at the end)
345 @property short width () const pure nothrow @safe @nogc { pragma(inline, true); return (props.hyphen ? whyph : w); }
346 // width with spacing/hyphen
347 @property short fullwidth () const pure nothrow @safe @nogc { pragma(inline, true); return (props.hyphen ? whyph : props.spaced ? wsp : w); }
348 // space width based on original props
349 @property short spacewidth () const pure nothrow @safe @nogc { pragma(inline, true); return cast(short)(propsOrig.spaced ? wsp-w : 0); }
350 //FIXME: find better place for this! keep that in separate pool, or something, and look there with word index
351 LayLineStyle just;
352 short paraPad; // to not recalcuate it on each relayouting; set to -1 to recalculate ;-)
353 @property int objectIdx () const pure nothrow @safe @nogc { pragma(inline, true); return (propsOrig.object ? wstart : -1); }
354 //int userTag;
358 // ////////////////////////////////////////////////////////////////////////// //
359 // layouted text line
360 struct LayLine {
361 uint wstart, wend; // indicies in word array
362 LayLineStyle just; // line style
363 // calculated properties
364 int x, y, w; // starting x and y positions, width
365 // on finish, layouter will calculate minimal ('cause it is negative) descent
366 int h, desc; // height, descent (negative)
367 @property int wordCount () const pure nothrow @safe @nogc { pragma(inline, true); return cast(int)(wend-wstart); }
371 // ////////////////////////////////////////////////////////////////////////// //
372 alias LayText = LayTextImpl!char;
374 // layouted text
375 final class LayTextImpl(TBT=char) if (is(TBT == char) || is(TBT == dchar)) {
376 public:
377 // special control characters
378 enum dchar EndLineCh = 0x2028; // 0x0085 is treated like whitespace
379 enum dchar EndParaCh = 0x2029;
381 private:
382 void ensurePool(ubyte pow2, bool clear, T) (uint want, ref T* ptr, ref uint used, ref uint alloced) {
383 if (want == 0) return;
384 static assert(pow2 < 24, "wtf?!");
385 uint cursz = used*cast(uint)T.sizeof;
386 if (cursz >= int.max/2) throw new Exception("pool overflow");
387 auto lsz = cast(ulong)want*T.sizeof;
388 if (lsz >= int.max/2 || lsz+cursz >= int.max/2) throw new Exception("pool overflow");
389 want = cast(uint)lsz;
390 uint cural = alloced*cast(uint)T.sizeof;
391 if (cursz+want > cural) {
392 import core.stdc.stdlib : realloc;
393 // grow it
394 uint newsz = ((cursz+want)|((1<<pow2)-1))+1;
395 if (newsz >= int.max/2) throw new Exception("pool overflow");
396 auto np = cast(T*)realloc(ptr, newsz);
397 if (np is null) throw new Exception("out of memory for pool");
398 static if (clear) {
399 import core.stdc.string : memset;
400 memset(np+used, 0, newsz-cursz);
402 ptr = np;
403 alloced = newsz/cast(uint)T.sizeof;
407 TBT* ltext;
408 uint charsUsed, charsAllocated;
410 static if (is(TBT == char)) {
411 void putChars (const(dchar)[] str...) {
412 import core.stdc.string : memcpy;
413 //if (str.length >= int.max/2) throw new Exception("string too long");
414 char[4] buf = void;
415 foreach (dchar ch; str[]) {
416 //auto len = utf8Encode(buf[], ch);
417 //if (len < 0) { buf.ptr[0] = '?'; len = 1; }
418 if (!Utf8Decoder.isValidDC(ch)) ch = Utf8Decoder.replacement;
420 if (ch <= 0x7F) {
421 ensurePool!(16, false)(1, ltext, charsUsed, charsAllocated);
422 ltext[charsUsed++] = cast(char)ch;
423 } else {
424 ubyte len;
425 if (ch <= 0x7FF) {
426 buf.ptr[0] = cast(char)(0xC0|(ch>>6));
427 buf.ptr[1] = cast(char)(0x80|(ch&0x3F));
428 len = 2;
429 } else if (ch <= 0xFFFF) {
430 buf.ptr[0] = cast(char)(0xE0|(ch>>12));
431 buf.ptr[1] = cast(char)(0x80|((ch>>6)&0x3F));
432 buf.ptr[2] = cast(char)(0x80|(ch&0x3F));
433 len = 3;
434 } else if (ch <= 0x10FFFF) {
435 buf.ptr[0] = cast(char)(0xF0|(ch>>18));
436 buf.ptr[1] = cast(char)(0x80|((ch>>12)&0x3F));
437 buf.ptr[2] = cast(char)(0x80|((ch>>6)&0x3F));
438 buf.ptr[3] = cast(char)(0x80|(ch&0x3F));
439 len = 4;
440 } else {
441 assert(0, "wtf?!");
443 ensurePool!(16, false)(len, ltext, charsUsed, charsAllocated);
444 memcpy(ltext+charsUsed, buf.ptr, len);
445 charsUsed += len;
450 } else {
451 void putChars (const(dchar)[] str...) {
452 import core.stdc.string : memcpy;
453 if (str.length == 0) return;
454 if (str.length >= int.max/2/dchar.sizeof) throw new Exception("string too long");
455 ensurePool!(16, false)(cast(uint)str.length, ltext, charsUsed, charsAllocated);
456 memcpy(ltext+charsUsed, str.ptr, cast(uint)str.length*dchar.sizeof);
457 charsUsed += cast(uint)str.length;
461 LayWord* words;
462 uint wordsUsed, wordsAllocated;
464 LayWord* allocWord(bool clear=false) () {
465 ensurePool!(16, true)(1, words, wordsUsed, wordsAllocated);
466 auto res = words+wordsUsed;
467 static if (clear) {
468 import core.stdc.string : memset;
469 memset(res, 0, (*res).sizeof);
471 res.wordNum = wordsUsed++;
472 //res.userTag = wordTag;
473 return res;
476 LayLine* lines;
477 uint linesUsed, linesAllocated;
479 LayLine* allocLine(bool clear=false) () {
480 ensurePool!(16, true)(1, lines, linesUsed, linesAllocated);
481 static if (clear) {
482 import core.stdc.string : memset;
483 auto res = lines+(linesUsed++);
484 memset(res, 0, (*res).sizeof);
485 return res;
486 } else {
487 return lines+(linesUsed++);
491 LayLine* lastLine () { pragma(inline, true); return (linesUsed > 0 ? lines+linesUsed-1 : null); }
493 bool lastLineHasWords () { pragma(inline, true); return (linesUsed > 0 ? (lines[linesUsed-1].wend > lines[linesUsed-1].wstart) : false); }
495 // should not be called when there are no lines, or no words in last line
496 LayWord* lastLineLastWord () { pragma(inline, true); return words+lastLine.wend-1; }
498 static struct StyleStackItem {
499 LayFontStyle fs;
500 LayLineStyle ls;
502 StyleStackItem* styleStack;
503 uint ststackUsed, ststackAllocated;
505 public void pushStyles () {
506 ensurePool!(4, false)(1, styleStack, ststackUsed, ststackAllocated);
507 auto si = styleStack+(ststackUsed++);
508 si.fs = newStyle;
509 si.ls = newJust;
512 public void popStyles () {
513 if (ststackUsed == 0) throw new Exception("style stack underflow");
514 auto si = styleStack+(--ststackUsed);
515 newStyle = si.fs;
516 newJust = si.ls;
519 private:
520 bool firstParaLine = true;
521 uint lastWordStart; // in fulltext
522 uint firstWordNotFlushed;
524 @property bool hasWordChars () const pure nothrow @safe @nogc { pragma(inline, true); return (lastWordStart < charsUsed); }
526 private:
527 // current attributes
528 LayLineStyle just; // for current paragraph
529 LayFontStyle style;
530 // user can change this alot, so don't apply that immediately
531 LayFontStyle newStyle;
532 LayLineStyle newJust;
534 private:
535 Utf8Decoder dec;
536 bool lastWasUtf;
537 bool lastWasSoftHypen;
538 int maxWidth; // maximum text width
539 LayFontStash laf;
541 public:
542 int textHeight = 0; // total text height
543 int textWidth = 0; // maximum text width
545 public:
546 // compare function should return (roughly): key-l
547 alias CmpFn = int delegate (LayLine* l) nothrow @nogc;
549 int findLineBinary (scope CmpFn cmpfn) {
550 if (linesUsed == 0) return -1;
551 int bot = 0, i = cast(int)linesUsed-1;
552 while (bot != i) {
553 int mid = i-(i-bot)/2;
554 int cmp = cmpfn(lines+mid);
555 if (cmp < 0) i = mid-1;
556 else if (cmp > 0) bot = mid;
557 else return mid;
559 return (cmpfn(lines+i) == 0 ? i : -1);
562 // find line with this word index
563 int findLineWithWord (uint idx) {
564 return findLineBinary((LayLine* l) {
565 if (idx < l.wstart) return -1;
566 if (idx >= l.wend) return 1;
567 return 0;
571 // find line which contains this coordinate
572 int findLineAtY (int y) {
573 if (linesUsed == 0) return 0;
574 if (y < 0) return 0;
575 if (y >= textHeight) return cast(int)linesUsed-1;
576 auto res = findLineBinary((LayLine* l) {
577 if (y < l.y) return -1;
578 if (y >= l.y+l.h) return 1;
579 return 0;
581 assert(res != -1);
582 return res;
585 // find word at given coordinate in the given line
586 int findWordAtX (LayLine* ln, int x) {
587 int wcmp (int wnum) {
588 auto w = words+ln.wstart+wnum;
589 if (x < w.x) return -1;
590 return (x >= (wnum+1 < ln.wordCount ? w[1].x : w.x+w.w) ? 1 : 0);
592 if (ln is null || ln.wordCount == 0) return -1;
593 int bot = 0, i = ln.wordCount-1;
594 while (bot != i) {
595 int mid = i-(i-bot)/2;
596 switch (wcmp(mid)) {
597 case -1: i = mid-1; break;
598 case 1: bot = mid; break;
599 default: return ln.wstart+mid;
602 return (wcmp(i) == 0 ? ln.wstart+i : -1);
605 // find word at the given coordinates
606 int wordAtXY (int x, int y) {
607 auto lidx = findLineAtY(y);
608 if (lidx < 0) return -1;
609 auto ln = lines+lidx;
610 if (y < ln.y || y >= ln.y+ln.h || ln.wordCount == 0) return -1;
611 return findWordAtX(ln, x);
614 LayWord* wordByIndex (uint idx) pure nothrow @trusted @nogc { pragma(inline, true); return (idx < wordsUsed ? words+idx : null); }
616 @property const(TBT)[] wordText (in ref LayWord w) const pure nothrow @trusted @nogc { pragma(inline, true); return (w.wstart <= w.wend ? ltext[w.wstart..w.wend] : null); }
618 @property int lineCount () const pure nothrow @safe @nogc { pragma(inline, true); return cast(int)linesUsed; }
620 // word iterator
621 @property auto lineWords (int lidx) {
622 static struct Range {
623 private:
624 LayWord* w;
625 int wordsLeft; // not including current
626 nothrow @trusted @nogc:
627 private:
628 this(LT) (LT lay, int lidx) {
629 if (lidx >= 0 && lidx < lay.linesUsed) {
630 auto ln = lay.lines+lidx;
631 if (ln.wend > ln.wstart) {
632 w = lay.words+ln.wstart;
633 wordsLeft = ln.wend-ln.wstart-1;
637 public:
638 @property bool empty () const pure { pragma(inline, true); return (w is null); }
639 @property ref LayWord front () pure { pragma(inline, true); assert(w !is null); return *w; }
640 void popFront () { if (wordsLeft) { ++w; --wordsLeft; } else w = null; }
641 Range save () { Range res = void; res.w = w; res.wordsLeft = wordsLeft; return res; }
642 @property int length () const pure { pragma(inline, true); return (w !is null ? wordsLeft+1 : 0); }
643 alias opDollar = length;
644 @property LayWord[] opSlice () { return (w !is null ? w[0..wordsLeft+1] : null); }
645 @property LayWord[] opSlice (int lo, int hi) {
646 if (lo < 0) lo = 0;
647 if (w is null || hi <= lo || lo > wordsLeft) return null;
648 if (hi > wordsLeft+1) hi = wordsLeft+1;
649 return w[lo..hi];
652 return Range(this, lidx);
655 LayLine* line (int lidx) { pragma(inline, true); return (lidx >= 0 && lidx < linesUsed ? lines+lidx : null); }
657 public:
658 LayObject[] objects;
660 public:
661 this (LayFontStash alaf, int awidth) {
662 if (alaf is null) assert(0, "no layout fonts");
663 if (awidth < 1) awidth = 1;
664 laf = alaf;
665 maxWidth = awidth;
668 ~this () { freeMemory(); }
670 void freeMemory () {
671 import core.stdc.stdlib : free;
672 if (lines !is null) { free(lines); lines = null; }
673 if (words !is null) { free(words); words = null; }
674 if (ltext !is null) { free(ltext); ltext = null; }
675 wordsUsed = wordsAllocated = linesUsed = linesAllocated = charsUsed = charsAllocated = 0;
678 @property int width () const pure nothrow @safe @nogc { pragma(inline, true); return maxWidth; }
680 // last flushed word index
681 @property uint lastWordIndex () const pure nothrow @safe @nogc { pragma(inline, true); return (wordsUsed ? wordsUsed-1 : 0); }
683 // current word index
684 @property uint nextWordIndex () const pure nothrow @safe @nogc { pragma(inline, true); return wordsUsed+hasWordChars; }
686 // use those to change current style. font style will take an effect on next char, line style on next line
687 @property ref LayFontStyle fontStyle () pure nothrow @safe @nogc { pragma(inline, true); return newStyle; }
688 @property ref LayLineStyle lineStyle () pure nothrow @safe @nogc { pragma(inline, true); return newJust; }
690 // return "font id" for the given font face
691 @property int fontFaceId (const(char)[] name) {
692 if (laf !is null) {
693 int fid = laf.fontFaceId(name);
694 if (fid >= 0) return fid;
696 throw new Exception("unknown font face '"~name.idup~"'");
699 // return font face for the given "font id"
700 @property string fontFace (int fid) { pragma(inline, true); return (laf !is null ? laf.fontFace(fid) : null); }
702 // end current line
703 void endLine () { put(EndLineCh); }
704 // end current paragraph
705 void endPara () { put(EndParaCh); }
707 // add "object" into text -- special thing that knows it's dimensions
708 void putObject (LayObject obj) {
709 import std.algorithm : max, min;
710 flushWord();
711 lastWasSoftHypen = false;
712 if (obj is null) return;
713 if (objects.length >= int.max/2) throw new Exception("too many objects");
714 just = newJust;
715 // create special word
716 auto w = allocWord();
717 w.wstart = cast(dchar)objects.length; // store object index
718 w.wend = 0;
719 objects ~= obj;
720 w.style = style;
721 w.propsOrig.object = true;
722 w.propsOrig.spaced = obj.spaced;
723 w.propsOrig.canbreak = obj.canbreak;
724 w.props = w.propsOrig;
725 w.w = cast(short)min(max(0, obj.width), short.max);
726 w.whyph = w.wsp = cast(short)min(w.w+max(0, obj.spacewidth), short.max);
727 w.h = cast(short)min(max(0, obj.height), short.max);
728 w.asc = cast(short)min(max(0, obj.ascent), short.max);
729 if (w.asc < 0) throw new Exception("object ascent should be positive");
730 w.desc = cast(short)min(max(0, obj.descent), short.max);
731 if (w.desc > 0) throw new Exception("object descent should be negative");
732 w.just = just;
733 w.paraPad = -1;
736 // add text to layouter; it is ok to mix (valid) utf-8 and dchars here
737 void put(T) (const(T)[] str...) if (is(T == char) || is(T == dchar)) {
738 if (str.length == 0) return;
740 dchar curCh; // 0: no more chars
741 usize stpos;
743 static if (is(T == char)) {
744 // utf-8 stream
745 if (!lastWasUtf) { lastWasUtf = true; dec.reset; }
746 void skipCh () @trusted {
747 while (stpos < str.length) {
748 curCh = dec.decode(cast(ubyte)str.ptr[stpos++]);
749 if (curCh <= dchar.max) return;
751 curCh = 0;
753 // load first char
754 skipCh();
755 } else {
756 // dchar stream
757 void skipCh () @trusted {
758 if (stpos < str.length) {
759 curCh = str.ptr[stpos++];
760 if (curCh > dchar.max) curCh = '?';
761 } else {
762 curCh = 0;
765 // load first char
766 if (lastWasUtf) {
767 lastWasUtf = false;
768 if (!dec.complete) curCh = '?'; else skipCh();
769 } else {
770 skipCh();
774 // process stream dchars
775 if (curCh == 0) return;
776 if (!hasWordChars) style = newStyle;
777 if (wordsUsed == 0 || words[wordsUsed-1].propsOrig.someend) just = newJust;
778 while (curCh) {
779 import std.uni;
780 dchar ch = curCh;
781 skipCh();
782 if (ch == EndLineCh || ch == EndParaCh) {
783 // ignore leading empty lines
784 if (hasWordChars) flushWord(); // has some word data, flush it now
785 lastWasSoftHypen = false; // word flusher is using this flag
786 auto lw = (wordsUsed ? words+wordsUsed-1 : createEmptyWord());
787 // do i need to add empty word for attrs?
788 if (lw.propsOrig.someend) lw = createEmptyWord();
789 // fix word properties
790 lw.propsOrig.canbreak = true;
791 lw.propsOrig.spaced = false;
792 lw.propsOrig.hyphen = false;
793 lw.propsOrig.lineend = (ch == EndLineCh);
794 lw.propsOrig.paraend = (ch == EndParaCh);
795 flushWord();
796 just = newJust;
797 firstParaLine = (ch == EndParaCh);
798 } else if (ch == 0x00a0) {
799 // non-breaking space
800 lastWasSoftHypen = false;
801 if (hasWordChars && style != newStyle) flushWord();
802 putChars(' ');
803 } else if (ch == 0x0ad) {
804 // soft hyphen
805 if (!lastWasSoftHypen && hasWordChars) {
806 putChars('-');
807 lastWasSoftHypen = true; // word flusher is using this flag
808 flushWord();
810 lastWasSoftHypen = true;
811 } else if (ch <= ' ' || isWhite(ch)) {
812 if (hasWordChars) {
813 flushWord();
814 auto lw = words+wordsUsed-1;
815 lw.propsOrig.canbreak = true;
816 lw.propsOrig.spaced = true;
817 } else {
818 style = newStyle;
820 lastWasSoftHypen = false;
821 } else {
822 lastWasSoftHypen = false;
823 if (ch > dchar.max || ch.isSurrogate || ch.isPrivateUse || ch.isNonCharacter || ch.isMark || ch.isFormat || ch.isControl) ch = '?';
824 if (hasWordChars && style != newStyle) flushWord();
825 putChars(ch);
826 if (isDash(ch) && charsUsed-lastWordStart > 1 && !isDash(ltext[charsUsed-2])) flushWord();
831 // "finalize" layout: calculate lines, layout words...
832 // call this after you done feeding text to this
833 void finalize () {
834 flushWord();
835 lastWasSoftHypen = false;
836 relayout(maxWidth, true);
839 // relayout everything using the existing words
840 void relayout (int newWidth, bool forced) {
841 if (newWidth < 1) newWidth = 1;
842 if (!forced && newWidth == maxWidth) return;
843 maxWidth = newWidth;
844 linesUsed = 0;
845 if (linesAllocated > 0) {
846 import core.stdc.string : memset;
847 memset(lines, 0, linesAllocated*lines[0].sizeof);
849 uint widx = 0;
850 uint wu = wordsUsed;
851 textWidth = 0;
852 textHeight = 0;
853 firstParaLine = true;
854 scope(exit) firstWordNotFlushed = wu;
855 while (widx < wu) {
856 uint lend = widx;
857 while (lend < wu) {
858 auto w = words+(lend++);
859 if (w.propsOrig.someend) break;
861 flushLines(widx, lend);
862 widx = lend;
863 firstParaLine = words[widx-1].propsOrig.paraend;
867 public:
868 // don't use
869 void save (VFile fl) {
870 fl.rawWriteExact("XLL0");
871 fl.rawWriteExact(ltext[0..charsUsed]);
872 fl.rawWriteExact(words[0..wordsUsed]);
875 public:
876 // don't use
877 void dump (VFile fl) const {
878 fl.writeln("LINES: ", linesUsed);
879 foreach (immutable idx, const ref ln; lines[0..linesUsed]) {
880 fl.writeln("LINE #", idx, ": ", ln.wordCount, " words; just=", ln.just.toString, "; jlpad=", ln.just.leftpad, "; y=", ln.y, "; h=", ln.h, "; desc=", ln.desc);
881 foreach (immutable widx, const ref w; words[ln.wstart..ln.wend]) {
882 fl.writeln(" WORD #", widx, "(", w.wordNum, ")[", w.wstart, "..", w.wend, "]: ", wordText(w));
883 fl.writeln(" wbreak=", w.props.canbreak, "; wspaced=", w.props.spaced, "; whyphen=", w.props.hyphen, "; style=", w.style.toString);
884 fl.writeln(" x=", w.x, "; w=", w.w, "; h=", w.h, "; asc=", w.asc, "; desc=", w.desc);
889 private:
890 static bool isDash (dchar ch) {
891 pragma(inline, true);
892 return (ch == '-' || (ch >= 0x2013 && ch == 0x2015) || ch == 0x2212);
895 LayWord* createEmptyWord () {
896 assert(!hasWordChars);
897 auto w = allocWord();
898 w.style = style;
899 w.props = w.propsOrig;
900 // set word dimensions
901 if (w.style.fontface < 0) throw new Exception("invalid font face in word style");
902 laf.setFont(w.style);
903 w.w = w.wsp = w.whyph = 0;
904 // calculate ascent, descent and height
906 int a, d, h;
907 laf.textMetrics(&a, &d, &h);
908 w.asc = cast(short)a;
909 w.desc = cast(short)d;
910 w.h = cast(short)h;
912 w.just = just;
913 w.paraPad = -1;
914 style = newStyle;
915 return w;
918 void flushWord () {
919 if (hasWordChars) {
920 auto w = allocWord();
921 w.wstart = lastWordStart;
922 w.wend = charsUsed;
923 //{ import iv.encoding, std.conv : to; writeln("adding word: [", wordText(*w).to!string.recodeToKOI8, "]"); }
924 w.propsOrig.hyphen = lastWasSoftHypen;
925 if (lastWasSoftHypen) {
926 w.propsOrig.canbreak = true;
927 w.propsOrig.spaced = false;
928 --w.wend; // remove hyphen mark (for now)
930 w.style = style;
931 w.props = w.propsOrig;
932 w.props.hyphen = false;
933 // set word dimensions
934 if (w.style.fontface < 0) throw new Exception("invalid font face in word style");
935 laf.setFont(w.style);
936 // i may need spacing later, and anyway most words should be with spacing, so calc it unconditionally
937 if (w.wend > w.wstart) {
938 auto t = wordText(*w);
939 int ww, wsp, whyph;
940 laf.textWidth2(t, &ww, &wsp, (w.propsOrig.hyphen ? &whyph : null));
941 w.w = cast(short)ww;
942 w.wsp = cast(short)wsp;
943 if (!w.propsOrig.hyphen) w.whyph = w.w; else w.whyph = cast(short)whyph;
944 if (isDash(t[$-1])) { w.propsOrig.canbreak = true; w.props.canbreak = true; }
945 } else {
946 w.w = w.wsp = w.whyph = 0;
948 // calculate ascent, descent and height
950 int a, d, h;
951 laf.textMetrics(&a, &d, &h);
952 w.asc = cast(short)a;
953 w.desc = cast(short)d;
954 w.h = cast(short)h;
956 w.just = just;
957 w.paraPad = -1;
958 lastWordStart = charsUsed;
960 style = newStyle;
963 // [curw..endw)"
964 void flushLines (uint curw, uint endw) {
965 if (curw < endw) {
966 debug(xlay_line_flush) conwriteln("flushing ", endw-curw, " words");
967 uint stline = linesUsed; // reformat from this
968 // fix word styles
969 foreach (ref LayWord w; words[curw..endw]) {
970 if (w.props.hyphen) --w.wend; // remove hyphen mark
971 w.props = w.propsOrig;
972 w.props.hyphen = false;
974 LayLine* ln;
975 LayWord* w = words+curw;
976 while (curw < endw) {
977 debug(xlay_line_flush) conwriteln(" ", endw-curw, " words left");
978 if (ln is null) {
979 // add line to work with
980 ln = allocLine();
981 ln.wstart = ln.wend = curw;
982 ln.just = w.just;
983 ln.w = w.just.leftpad+w.just.rightpad;
984 // indent first line of paragraph
985 if (firstParaLine) {
986 firstParaLine = false;
987 // left-side or justified lines has paragraph indent
988 if (ln.just.paraIndent > 0 && (w.just.left || w.just.justify)) {
989 auto ind = w.paraPad;
990 if (ind < 0) {
991 laf.setFont(w.style);
992 ind = cast(short)laf.spacesWidth(ln.just.paraIndent);
993 w.paraPad = ind;
995 ln.w += ind;
996 ln.just.leftpad = ln.just.leftpad+ind;
997 } else {
998 w.paraPad = 0;
1001 //conwriteln("new line; maxWidth=", maxWidth, "; starting line width=", ln.w);
1002 //conwriteln("* maxWidth=", maxWidth, "; ln.w=", ln.w, "; leftpad=", ln.just.leftpad, "; rightpad=", ln.just.rightpad);
1004 debug(xlay_line_flush) conwritefln!" (%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))));
1005 // add words until i hit breaking point
1006 // if it will end beyond maximum width, and this line
1007 // has some words, flush the line and start new one
1008 uint startIndex = curw;
1009 int curwdt = ln.w, lastwsp = 0;
1010 while (curw < endw) {
1011 // add word width with spacing (i will compensate for that after loop)
1012 lastwsp = (w.propsOrig.spaced ? w.wsp-w.w : 0);
1013 curwdt += w.w+lastwsp;
1014 ++curw; // advance counter here...
1015 if (w.props.canbreak) break; // done with this span
1016 ++w; // ...and word pointer here (skipping one inc at the end ;-)
1018 debug(xlay_line_flush) conwriteln(" ", curw-startIndex, " words processed");
1019 // can i add the span? if this is first span in line, add it unconditionally
1020 if (ln.wordCount == 0 || curwdt-lastwsp <= maxWidth) {
1021 // yay, i can!
1022 ln.wend = curw;
1023 ln.w = curwdt;
1024 ++w; // advance to curw
1025 debug(xlay_line_flush) conwriteln("curwdt=", curwdt, "; maxWidth=", maxWidth, "; wc=", ln.wordCount, "(", ln.wend-ln.wstart, ")");
1026 } else {
1027 // nope, start new line here
1028 debug(xlay_line_flush) conwriteln("added line with ", ln.wordCount, " words");
1029 // last word in the line should not be spaced
1030 auto ww = words+ln.wend-1;
1031 // compensate for spacing at last word
1032 ln.w -= (ww.props.spaced ? ww.wsp-ww.w : 0);
1033 ww.props.spaced = false;
1034 // and should have hyphen mark if it is necessary
1035 if (ww.propsOrig.hyphen) {
1036 assert(!ww.props.hyphen);
1037 ww.props.hyphen = true;
1038 ++ww.wend;
1039 // fix line width (word layouter will use that)
1040 ln.w += ww.whyph-ww.w;
1042 ln = null;
1043 curw = startIndex;
1044 w = words+curw;
1047 debug(xlay_line_flush) conwriteln("added line with ", ln.wordCount, " words; new lines range: [", stline, "..", linesUsed, "]");
1048 debug(xlay_line_flush) conwritefln!"(%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))));
1049 // last line should not be justified
1050 if (ln.just.justify) ln.just.setLeft;
1051 // do real word layouting and fix line metrics
1052 debug(xlay_line_flush) conwriteln("added ", linesUsed-stline, " lines");
1053 foreach (uint lidx; stline..linesUsed) {
1054 debug(xlay_line_flush) conwriteln(": lidx=", lidx, "; wc=", lines[lidx].wordCount);
1055 layoutLine(lidx);
1060 // do word layouting and fix line metrics
1061 void layoutLine (uint lidx) {
1062 import std.algorithm : max, min;
1063 assert(lidx < linesUsed);
1064 auto ln = lines+lidx;
1065 //conwriteln("maxWidth=", maxWidth, "; ln.w=", ln.w, "; leftpad=", ln.just.leftpad, "; rightpad=", ln.just.rightpad);
1066 debug(xlay_line_layout) conwriteln("lidx=", lidx, "; wc=", ln.wordCount);
1067 // y position
1068 ln.y = (lidx ? ln[-1].y+ln[-1].h : 0);
1069 auto lwords = lineWords(lidx);
1070 assert(!lwords.empty); // i should have at least one word in each line
1071 // line width is calculated for us by `flushLines()`
1072 // calculate line metrics and number of words with spacing
1073 int lineH, lineDesc, wspCount;
1074 foreach (ref LayWord w; lwords.save) {
1075 lineH = max(lineH, w.h);
1076 lineDesc = min(lineDesc, w.desc);
1077 if (w.props.spaced) ++wspCount;
1079 // vertical padding; clamp it, as i can't have line over line (it will break too many things)
1080 lineH += ln.just.toppad+ln.just.bottompad;
1081 ln.h = lineH;
1082 ln.desc = lineDesc;
1083 if (ln.w >= maxWidth) {
1084 //conwriteln("*** ln.w=", ln.w, "; maxWidth=", maxWidth);
1085 // way too long; (almost) easy deal
1086 // calculate free space to spare in case i'll need to compensate hyphen mark
1087 int x = ln.just.leftpad, spc = 0;
1088 foreach (ref LayWord w; lwords.save) {
1089 w.x = cast(short)x;
1090 x += w.fullwidth;
1091 if (w.props.spaced) spc += w.wsp-w.w;
1093 // if last word ends with hyphen, try to compensate it
1094 if (words[ln.wend-1].props.hyphen) {
1095 int needspc = ln.w-maxWidth;
1096 // no more than 8 pix or 2/3 of free space
1097 if (needspc <= 8 && needspc <= spc/3*2) {
1098 // compensate (i can do fractional math here, but meh...)
1099 while (needspc > 0) {
1100 // excellence in coding!
1101 foreach_reverse (immutable widx; ln.wstart..ln.wend) {
1102 if (words[widx].props.spaced) {
1103 --ln.w;
1104 foreach (immutable c; widx+1..ln.wend) words[c].x -= 1;
1105 if (--needspc == 0) break;
1111 } else if (ln.just.justify && wspCount > 0) {
1112 // fill the whole line
1113 int spc = maxWidth-ln.w; // space left to distribute
1114 int xadvsp = spc/wspCount;
1115 int frac = spc-xadvsp*wspCount;
1116 int x = ln.just.leftpad;
1117 // no need to save range here, i'll do it in one pass
1118 foreach (ref LayWord w; lwords) {
1119 w.x = cast(short)x;
1120 x += w.fullwidth;
1121 if (w.props.spaced) {
1122 x += xadvsp;
1123 //spc -= xadvsp;
1124 if (frac-- > 0) {
1125 ++x;
1126 //--spc;
1130 //if (x != maxWidth-ln.just.rightpad) conwriteln("x=", x, "; but it should be ", maxWidth-ln.just.rightpad, "; spcleft=", spc, "; ln.w=", ln.w, "; maxWidth=", maxWidth-ln.w);
1131 //assert(x == maxWidth-ln.just.rightpad);
1132 } else {
1133 int x;
1134 if (ln.just.left || ln.just.justify) x = ln.just.leftpad;
1135 else if (ln.just.right) x = maxWidth-ln.w+ln.just.leftpad;
1136 else if (ln.just.center) x = (maxWidth-(ln.w-ln.just.leftpad-ln.just.rightpad))/2;
1137 else assert(0, "wtf?!");
1138 // no need to save range here, i'll do it in one pass
1139 foreach (ref LayWord w; lwords) {
1140 w.x = cast(short)x;
1141 x += w.fullwidth;
1144 if (ln.h < 1) ln.h = 1;
1145 textWidth = max(textWidth, ln.w);
1146 textHeight = ln.y+ln.h;
1147 debug(xlay_line_layout) conwriteln("lidx=", lidx, "; wc=", ln.wordCount);