egedit: do not save cursor movement in undo -- this is my stupid habit, and it comple...
[iv.d.git] / nanovega / textlayouter.d
blob1ed848c73a4e81e9363b2ea4afb2fab007ace071
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, version 3 of the License ONLY.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 /**
17 FAST! Text Layouting Engine with support for various text alignmen, justification,
18 and other fancy stuff. Did I mentioned that it is FAST!?
20 module iv.nanovega.textlayouter is aliced;
22 import arsd.image;
24 import iv.cmdcon;
25 import iv.meta;
26 import iv.nanovega.nanovega;
27 import iv.utfutil;
28 import iv.vfs;
31 version(laytest) import iv.encoding;
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 //debug = xlay_line_flush;
39 //debug = xlay_line_flush_ex;
42 // ////////////////////////////////////////////////////////////////////////// //
44 /// non-text object (image, for example); can be inserted in text.
45 /// all object's properties should be constant
46 public abstract class LayObject {
47 abstract int width (); /// object width
48 abstract int spacewidth (); /// space width for this object
49 abstract int height (); /// object height
50 abstract int ascent (); /// should be positive
51 abstract int descent (); /// should be negative
52 abstract bool canbreak (); /// can we do line break after this object?
53 abstract bool spaced (); /// should we automatically add space after this object?
54 /// y is at baseline
55 abstract void draw (NVGContext ctx, float x, float y);
59 // ////////////////////////////////////////////////////////////////////////// //
61 /// This object is used to get various text dimensions.
62 public final class LayFontStash {
63 public:
64 FONSContext fs; ///
66 private:
67 bool killFontStash;
68 bool fontWasSet; // to ensure that first call to `setFont()` will do it's work
69 LayFontStyle lastStyle;
71 public:
72 /// you can set this delegate, and it will be called if fontstyle's fontface is -1; it should set it to proper font id
73 void delegate (LayFontStash las, ref LayFontStyle st) nothrow @safe @nogc fixFontDG;
75 public:
76 /// create new fontstash
77 /// if `nvg` is not null, use its fontstash.
78 /// WARNING! this object SHOULD NOT outlive `nvg`!
79 this (NVGContext nvg=null) {
80 if (nvg !is null && nvg.fonsContext !is null) {
81 killFontStash = false;
82 fs = nvg.fonsContext;
83 //{ import core.stdc.stdio; printf("*** reusing font stash!\n"); }
84 } else {
85 // image size doesn't matter, as we won't create font bitmaps here anyway (we only interested in dimensions)
86 fs = FONSContext.create(FONSParams.init);
87 if (fs is null) throw new Exception("error creating font stash");
88 killFontStash = true;
89 fs.spacing = 0;
90 fs.blur = 0;
91 fs.textAlign = NVGTextAlign(NVGTextAlign.H.Left, NVGTextAlign.V.Baseline);
95 ///
96 ~this () nothrow @nogc { freeFontStash(); }
98 ///
99 @property ownsFontContext () const pure nothrow @safe @nogc => killFontStash;
101 private void freeFontStash () nothrow @nogc {
102 if (killFontStash && fs !is null) fs.kill();
103 killFontStash = false;
104 fs = null;
107 /// add new font to stash
108 void addFont (const(char)[] name, const(char)[] path) {
109 if (name.length == 0) throw new Exception("invalid font face name");
110 int fid = fs.addFont(name, path, NVG_INVERT_FONT_AA);
111 if (fid == FONS_INVALID) throw new Exception("font '"~name.idup~"' is not found at '"~path.idup~"'");
114 /// returns "font id" which can be used in `fontFace()`
115 @property int fontFaceId (const(char)[] name) const pure nothrow @safe @nogc {
116 return fs.getFontByName(name);
119 /// returns font name for the given id (or `null`)
120 @property const(char)[] fontFace (int fid) const pure nothrow @safe @nogc {
121 if (fid < 0) return null;
122 return fs.getNameById(fid);
125 /// set current font
126 void size (int fsz) nothrow @safe @nogc {
127 if (fsz < 1) fsz = 1;
128 fs.size = fsz;
129 lastStyle.fontsize = fsz;
132 /// set current font
133 void font (int fid) nothrow @safe @nogc {
134 if (fid == FONS_INVALID) {
135 fontWasSet = false;
136 lastStyle.fontface = -1;
137 } else if (!fontWasSet || lastStyle.fontface != fid) {
138 fs.fontId = fid;
139 lastStyle.fontface = fid;
140 fontWasSet = true;
144 /// set current font
145 void font (const(char)[] aface) nothrow @safe @nogc {
146 pragma(inline, true);
147 font(fontFaceId(aface));
150 /// set current font according to the given style
151 void font() (in auto ref LayFontStyle style) nothrow @safe @nogc {
152 int fsz = style.fontsize;
153 if (fsz < 1) fsz = 1;
154 if (!fontWasSet || fsz != lastStyle.fontsize || style.fontface != lastStyle.fontface) {
155 if (style.fontface != lastStyle.fontface) fs.fontId = style.fontface;
156 if (fsz != lastStyle.fontsize) fs.size = fsz;
157 lastStyle = style;
158 lastStyle.fontsize = fsz;
159 fontWasSet = true;
163 /// calculate text width
164 int textWidth(T) (const(T)[] str) nothrow @safe @nogc if (isAnyCharType!T) {
165 import std.algorithm : max;
166 if (!fontWasSet) assert(0, "LayFontStash: font is not set");
167 float[4] b = void;
168 float adv = fs.getTextBounds(0, 0, str, b[]);
169 float w = b[2]-b[0];
170 return cast(int)lrintf(max(adv, w));
173 /// calculate spaces width
174 int spacesWidth (int count) nothrow @safe @nogc {
175 if (!fontWasSet) assert(0, "LayFontStash: font is not set");
176 if (count < 1) return 0;
177 auto it = FONSTextBoundsIterator(fs, 0, 0);
178 it.put(' ');
179 return cast(int)lrintf(it.advance*count);
182 /// this returns "width", "width with trailing whitespace", and "width with trailing hypen"
183 /// all `*` args can be omited
184 void textWidth2(T) (const(T)[] str, int* w=null, int* wsp=null, int* whyph=null) nothrow @safe @nogc if (isAnyCharType!T) {
185 import std.algorithm : max;
186 if (!fontWasSet) assert(0, "LayFontStash: font is not set");
187 if (w is null && wsp is null && whyph is null) return;
188 float minx, maxx;
189 auto it = FONSTextBoundsIterator(fs, 0, 0);
190 it.put(str);
191 if (w !is null) {
192 it.getHBounds(minx, maxx);
193 *w = cast(int)lrintf(max(it.advance, maxx-minx));
195 if (wsp !is null && whyph is null) {
196 it.put(" ");
197 it.getHBounds(minx, maxx);
198 *wsp = cast(int)lrintf(max(it.advance, maxx-minx));
199 } else if (wsp is null && whyph !is null) {
200 it.put(cast(dchar)45);
201 it.getHBounds(minx, maxx);
202 *whyph = cast(int)lrintf(max(it.advance, maxx-minx));
203 } else if (wsp !is null && whyph !is null) {
204 auto sit = it;
205 it.put(" ");
206 it.getHBounds(minx, maxx);
207 *wsp = cast(int)lrintf(max(it.advance, maxx-minx));
208 sit.put(cast(dchar)45);
209 sit.getHBounds(minx, maxx);
210 *whyph = cast(int)lrintf(max(sit.advance, maxx-minx));
214 /// calculate text height
215 int textHeight () nothrow @trusted @nogc {
216 // use line bounds for height
217 if (!fontWasSet) assert(0, "LayFontStash: font is not set");
218 float y0 = void, y1 = void;
219 fs.getLineBounds(0, &y0, &y1);
220 return cast(int)lrintf(y1-y0);
223 /// calculate text metrics: ascent, descent, line height
224 /// any argument can be `null`
225 void textMetrics (int* asc, int* desc, int* lineh) nothrow @trusted @nogc {
226 if (!fontWasSet) assert(0, "LayFontStash: font is not set");
227 float a = void, d = void, h = void;
228 fs.getVertMetrics(&a, &d, &h);
229 if (asc !is null) *asc = cast(int)lrintf(a);
230 if (desc !is null) *desc = cast(int)lrintf(d);
231 if (lineh !is null) *lineh = cast(int)lrintf(h);
236 // ////////////////////////////////////////////////////////////////////////// //
238 /// generic text style
239 /// note that you must manually fix `fontface` after changing attrs. sorry.
240 public align(1) struct LayFontStyle {
241 align(1):
243 enum Flag : uint {
244 Italic = 1<<0, ///
245 Bold = 1<<1, ///
246 Strike = 1<<2, ///
247 Underline = 1<<3, ///
248 Overline = 1<<4, ///
249 Monospace = 1<<6, ///
250 Href = 1<<7, /// this is cross-reference (not actually a style flag, but it somewhat fits here)
251 DontResetFont = 1U<<31, /// Don't reset font on style change
253 enum StyleMask = Flag.Italic|Flag.Bold|Flag.Strike|Flag.Underline|Flag.Overline|Flag.Monospace;
254 uint flags; /// see above
255 int fontface = -1; /// i can't use strings here, as this struct inside LayWord will not be GC-scanned
256 int fontsize; ///
257 uint color = 0xff000000; /// AABBGGRR; AA usually ignored by renderer, but i'll keep it anyway
258 uint bgcolor = 0xff000000; /// AABBGGRR; AA usually ignored by renderer, but i'll keep it anyway
259 string toString () const {
260 import std.format : format;
261 string res = "font:%s;size:%s;color:0x%08X".format(fontface, fontsize, color);
262 if (flags&Flag.Italic) res ~= ";italic";
263 if (flags&Flag.Bold) res ~= ";bold";
264 if (flags&Flag.Strike) res ~= ";strike";
265 if (flags&Flag.Underline) res ~= ";under";
266 if (flags&Flag.Overline) res ~= ";over";
267 if (flags&Flag.Monospace) res ~= ";mono";
268 return res;
270 // this generates getter and setter for each `Flag`
271 mixin((){
272 auto res = CTFECharBuffer!false(3000);
273 foreach (immutable string s; __traits(allMembers, Flag)) {
274 // getter
275 res.put("@property bool ");
276 res.putStrLoCasedFirst(s);
277 res.put(" () const pure nothrow @safe @nogc { pragma(inline, true); return ((flags&Flag."~s~") != 0); }\n");
278 // setter
279 res.put("@property void ");
280 res.putStrLoCasedFirst(s);
281 res.put(" (bool v) ");
282 static if ((__traits(getMember, Flag, s)&StyleMask) == 0) res.put("pure ");
283 res.put("nothrow @safe @nogc { pragma(inline, true); ");
284 static if (__traits(getMember, Flag, s)&StyleMask) {
285 res.put("if ((flags&Flag.DontResetFont) == 0 && (!!(flags&Flag.");
286 res.put(s);
287 res.put(")) != v) fontface = -1; ");
289 res.put("if (v) flags |= Flag.");
290 res.put(s);
291 res.put("; else flags &= ~Flag.");
292 res.put(s);
293 res.put("; ");
294 res.put("}\n");
296 return res.asString; // it is safe to cast here
297 }());
298 /// this doesn't touch `Flag.DontResetFont`
299 void resetAttrs () nothrow @safe @nogc {
300 if ((flags&Flag.DontResetFont) == 0) {
301 if (flags&StyleMask) fontface = -1;
303 flags = 0;
305 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 && bgcolor == s.bgcolor && fontsize == s.fontsize); }
309 // ////////////////////////////////////////////////////////////////////////// //
311 /// line align style
312 public align(1) struct LayLineStyle {
313 align(1):
315 enum Justify : ubyte {
316 Left, ///
317 Right, ///
318 Center, ///
319 Justify, ///
321 Justify mode = Justify.Left; ///
322 short lpad, rpad, tpad, bpad; /// paddings; left and right can be negative
323 ubyte paraIndent; /// in spaces
324 string toString () const {
325 import std.format : format;
326 string res;
327 final switch (mode) {
328 case Justify.Left: res = "left"; break;
329 case Justify.Right: res = "right"; break;
330 case Justify.Center: res = "center"; break;
331 case Justify.Justify: res = "justify"; break;
333 if (lpad) res ~= ";lpad:%s".format(lpad);
334 if (rpad) res ~= ";rpad:%s".format(rpad);
335 if (tpad) res ~= ";tpad:%s".format(tpad);
336 if (bpad) res ~= ";bpad:%s".format(bpad);
337 return res;
339 // this generates getter and setter for each `Justify` mode
340 mixin((){
341 auto res = CTFECharBuffer!false(1024); // currently it is ~900
342 foreach (immutable string s; __traits(allMembers, Justify)) {
343 // getter
344 res.put("@property bool ");
345 res.putStrLoCasedFirst(s);
346 res.put(" () const pure nothrow @safe @nogc { pragma(inline, true); return (mode == Justify.");
347 res.put(s);
348 res.put("); }\n");
349 // setter (in the form of `setLeft`, etc.)
350 res.put("ref LayLineStyle set");
351 res.put(s);
352 res.put(" () pure nothrow @safe @nogc { mode = Justify.");
353 res.put(s);
354 res.put("; return this; }\n");
356 return res.asString;
357 }());
358 //bool opEquals() (in auto ref LayLineStyle s) const pure nothrow @safe @nogc { pragma(inline, true); return (mode == s.mode && lpad == s.lpad); }
359 @property pure nothrow @safe @nogc {
360 int leftpad () const { pragma(inline, true); return lpad; } ///
361 void leftpad (int v) { pragma(inline, true); lpad = (v < short.min ? short.min : v > short.max ? short.max : cast(short)v); } ///
362 int rightpad () const { pragma(inline, true); return rpad; } ///
363 void rightpad (int v) { pragma(inline, true); rpad = (v < short.min ? short.min : v > short.max ? short.max : cast(short)v); } ///
364 int toppad () const { pragma(inline, true); return tpad; } ///
365 void toppad (int v) { pragma(inline, true); tpad = (v < 0 ? 0 : v > short.max ? short.max : cast(short)v); } ///
366 int bottompad () const { pragma(inline, true); return bpad; } ///
367 void bottompad (int v) { pragma(inline, true); bpad = (v < 0 ? 0 : v > short.max ? short.max : cast(short)v); } ///
372 // ////////////////////////////////////////////////////////////////////////// //
374 /// layouted text word
375 public align(1) struct LayWord {
376 align(1):
378 static align(1) struct Props {
379 align(1):
380 /// note that if word is softhyphen candidate, i have hyphen mark at [wend].
381 /// if props.hyphen is set, [wend] is including that mark, otherwise it isn't.
382 /// this will prevent correct kerning, but meh...
383 enum Flag : uint {
384 CanBreak = 1<<0, /// can i break line at this word?
385 Spaced = 1<<1, /// should this word be whitespaced at the end?
386 Hypen = 1<<2, /// if i'll break at this word, should i add hyphen mark?
387 LineEnd = 1<<3, /// this word ends current line
388 ParaEnd = 1<<4, /// this word ends current paragraph (and, implicitly, line)
389 Object = 1<<5, /// wstart is actually object index in object array
390 Expander = 1<<6, /// this is special "expander" word
391 HardSpace = 1<<7, /// "hard space": nonbreakable space with fixed size
393 ubyte flags; /// see above
394 @property pure nothrow @safe @nogc:
395 bool canbreak () const { pragma(inline, true); return ((flags&Flag.CanBreak) != 0); } ///
396 void canbreak (bool v) { pragma(inline, true); if (v) flags |= Flag.CanBreak; else flags &= ~Flag.CanBreak; } ///
397 bool spaced () const { pragma(inline, true); return ((flags&Flag.Spaced) != 0); } ///
398 void spaced (bool v) { pragma(inline, true); if (v) flags |= Flag.Spaced; else flags &= ~Flag.Spaced; } ///
399 bool hyphen () const { pragma(inline, true); return ((flags&Flag.Hypen) != 0); } ///
400 void hyphen (bool v) { pragma(inline, true); if (v) flags |= Flag.Hypen; else flags &= ~Flag.Hypen; } ///
401 bool lineend () const { pragma(inline, true); return ((flags&Flag.LineEnd) != 0); } ///
402 void lineend (bool v) { pragma(inline, true); if (v) flags |= Flag.LineEnd; else flags &= ~Flag.LineEnd; } ///
403 bool paraend () const { pragma(inline, true); return ((flags&Flag.ParaEnd) != 0); } ///
404 void paraend (bool v) { pragma(inline, true); if (v) flags |= Flag.ParaEnd; else flags &= ~Flag.ParaEnd; } ///
405 bool someend () const { pragma(inline, true); return ((flags&(Flag.ParaEnd|Flag.LineEnd)) != 0); } ///
406 bool obj () const { pragma(inline, true); return ((flags&Flag.Object) != 0); } ///
407 void obj (bool v) { pragma(inline, true); if (v) flags |= Flag.Object; else flags &= ~Flag.Object; } ///
408 bool expander () const { pragma(inline, true); return ((flags&Flag.Expander) != 0); } ///
409 void expander (bool v) { pragma(inline, true); if (v) flags |= Flag.Expander; else flags &= ~Flag.Expander; } ///
410 bool hardspace () const { pragma(inline, true); return ((flags&Flag.HardSpace) != 0); } ///
411 void hardspace (bool v) { pragma(inline, true); if (v) flags |= Flag.HardSpace; else flags &= ~Flag.HardSpace; } ///
413 uint wstart, wend; /// in LayText text buffer
414 LayFontStyle style; /// font style
415 uint wordNum; /// word number (index in LayText word array)
416 Props propsOrig; /// original properties, used for relayouting
417 // calculated values
418 Props props; /// effective props after layouting
419 short x; /// horizontal word position in line
420 short h; /// word height (full)
421 short asc; /// ascent (positive)
422 short desc; /// descent (negative)
423 short w; /// word width, without hyphen and spacing
424 short wsp; /// word width with spacing (i.e. with space added at the end)
425 short whyph; /// word width with hyphen (i.e. with hyphen mark added at the end)
426 /// word width (with hypen mark, if necessary)
427 @property short width () const pure nothrow @safe @nogc => (props.hyphen ? whyph : w);
428 /// width with spacing/hyphen
429 @property short fullwidth () const pure nothrow @safe @nogc => (props.hyphen ? whyph : props.spaced ? wsp : w);
430 /// space width based on original props
431 @property short spacewidth () const pure nothrow @safe @nogc => cast(short)(propsOrig.spaced ? wsp-w : 0);
432 //FIXME: find better place for this! keep that in separate pool, or something, and look there with word index
433 LayLineStyle just; ///
434 short paraPad; /// to not recalcuate it on each relayouting; set to -1 to recalculate ;-)
435 /// returns `-1` if this is not an object
436 @property int objectIdx () const pure nothrow @safe @nogc => (propsOrig.obj ? wstart : -1);
437 @property bool expander () const pure nothrow @safe @nogc => propsOrig.expander; ///
438 @property bool hardspace () const pure nothrow @safe @nogc => propsOrig.hardspace; ///
439 usize udata; /// used-defined data, won't be touched by the engine
443 // ////////////////////////////////////////////////////////////////////////// //
445 /// layouted text line
446 public struct LayLine {
447 uint wstart, wend; /// indicies in word array
448 LayLineStyle just; /// line style
449 // calculated properties
450 int x, y, w; /// starting x and y positions, width
451 // on finish, layouter will calculate minimal ('cause it is negative) descent
452 int h, desc; /// height, descent (negative)
453 @property int wordCount () const pure nothrow @safe @nogc { pragma(inline, true); return cast(int)(wend-wstart); } ///
457 // ////////////////////////////////////////////////////////////////////////// //
459 public alias LayTextC = LayTextImpl!char; ///
460 public alias LayTextW = LayTextImpl!wchar; ///
461 public alias LayTextD = LayTextImpl!dchar; ///
463 // layouted text
464 public final class LayTextImpl(TBT=char) if (isAnyCharType!TBT) {
465 public:
466 alias CharType = TBT; ///
468 // special control characters
469 enum dchar LTRMarkCh = 0x200e; // left-to-right mark
470 enum dchar RTLMarkCh = 0x200f; // right-to-left mark
472 enum dchar LTREmbedCh = 0x202a; // left-to-right embedding
473 enum dchar RTLEmbedCh = 0x202b; // right-to-left embedding
475 enum dchar LTROverrideCh = 0x202d; // left-to-right override
476 enum dchar RTLOverrideCh = 0x202e; // right-to-left override
478 enum dchar LTRIsolateCh = 0x2066; // left-to-right isolate
479 enum dchar RTLIsolateCh = 0x2068; // right-to-left isolate
481 enum dchar HyphenCh = 0x2010;
482 enum dchar NBHyphenCh = 0x2011;
483 enum dchar HyphenPointCh = 0x2027;
484 enum dchar SoftHyphenCh = 0x00ad;
486 enum dchar WordJoinerCh = 0x2060;
488 enum dchar EllipsisCh = 0x2026;
490 enum dchar ZWSpaceCh = 0x200b; // zero width space
491 enum dchar ZWNBSpaceCh = 0xfeff; // zero width non-breaking space
492 enum dchar NBSpaceCh = 0x00a0;
493 enum dchar NarrowNBSpaceCh = 0x202f;
495 enum dchar EndLineCh = 0x2028; // 0x0085 is treated like whitespace
496 enum dchar EndParaCh = 0x2029;
498 enum dchar AnnoStartCh = 0xfff9; // interlinear annotation anchor, marks start of annotated text
499 enum dchar AnnoSepCh = 0xfffa; // interlinear annotation separator, marks start of annotating character(s)
500 enum dchar AnnoEndCh = 0xfffb; // interlinear annotation terminator, marks end of annotation block
502 enum dchar ObjectCh = 0xfffc; // object replacement character
503 enum dchar UnknownCh = 0xfffd; // replacement character used to replace an unknown or unrepresentable character
505 private:
506 void ensurePool(ubyte pow2, bool clear, T) (uint want, ref T* ptr, ref uint used, ref uint alloced) nothrow @nogc {
507 if (want == 0) return;
508 static assert(pow2 < 24, "wtf?!");
509 uint cursz = used*cast(uint)T.sizeof;
510 if (cursz >= int.max/2) assert(0, "pool overflow");
511 auto lsz = cast(ulong)want*T.sizeof;
512 if (lsz >= int.max/2 || lsz+cursz >= int.max/2) assert(0, "pool overflow");
513 want = cast(uint)lsz;
514 uint cural = alloced*cast(uint)T.sizeof;
515 if (cursz+want > cural) {
516 import core.stdc.stdlib : realloc;
517 // grow it
518 uint newsz = ((cursz+want)|((1<<pow2)-1))+1;
519 if (newsz >= int.max/2) assert(0, "pool overflow");
520 auto np = cast(T*)realloc(ptr, newsz);
521 if (np is null) assert(0, "out of memory for pool");
522 static if (clear) {
523 import core.stdc.string : memset;
524 memset(np+used, 0, newsz-cursz);
526 ptr = np;
527 alloced = newsz/cast(uint)T.sizeof;
531 CharType* ltext;
532 uint charsUsed, charsAllocated;
534 static char[] utfEncode (char[] buf, dchar ch) nothrow @trusted @nogc {
535 if (buf.length < 4) assert(0, "please provide at least 4-char buffer");
536 if (!Utf8Decoder.isValidDC(ch)) ch = Utf8Decoder.replacement;
537 if (ch <= 0x7F) {
538 buf.ptr[0] = cast(char)(ch&0xff);
539 return buf.ptr[0..1];
541 if (ch <= 0x7FF) {
542 buf.ptr[0] = cast(char)(0xC0|(ch>>6));
543 buf.ptr[1] = cast(char)(0x80|(ch&0x3F));
544 return buf.ptr[0..2];
546 if (ch <= 0xFFFF) {
547 buf.ptr[0] = cast(char)(0xE0|(ch>>12));
548 buf.ptr[1] = cast(char)(0x80|((ch>>6)&0x3F));
549 buf.ptr[2] = cast(char)(0x80|(ch&0x3F));
550 return buf.ptr[0..3];
552 if (ch <= 0x10FFFF) {
553 buf.ptr[0] = cast(char)(0xF0|(ch>>18));
554 buf.ptr[1] = cast(char)(0x80|((ch>>12)&0x3F));
555 buf.ptr[2] = cast(char)(0x80|((ch>>6)&0x3F));
556 buf.ptr[3] = cast(char)(0x80|(ch&0x3F));
557 return buf.ptr[0..4];
559 assert(0, "wtf?!");
562 static if (is(CharType == char)) {
563 void putChars (const(char)[] str...) nothrow @nogc {
564 import core.stdc.string : memcpy;
565 if (str.length == 0) return;
566 if (str.length >= int.max/4) assert(0, "string too long");
567 ensurePool!(16, false)(cast(uint)str.length, ltext, charsUsed, charsAllocated);
568 memcpy(ltext+charsUsed, str.ptr, cast(uint)str.length);
569 charsUsed += cast(uint)str.length;
571 void putChars(XCT) (const(XCT)[] str...) nothrow @nogc if (isWideCharType!XCT) {
572 import core.stdc.string : memcpy;
573 //if (str.length >= int.max/2) throw new Exception("string too long");
574 char[4] buf = void;
575 foreach (XCT ch; str[]) {
576 auto xbuf = utfEncode(buf[], cast(dchar)ch);
577 uint len = cast(uint)xbuf.length;
578 ensurePool!(16, false)(len, ltext, charsUsed, charsAllocated);
579 memcpy(ltext+charsUsed, xbuf.ptr, len);
580 charsUsed += len;
583 } else {
584 void putChars (const(char)[] str...) nothrow @nogc {
585 import core.stdc.string : memcpy;
586 if (str.length == 0) return;
587 if (str.length >= int.max/4/dchar.sizeof) assert(0, "string too long");
588 ensurePool!(16, false)(cast(uint)str.length, ltext, charsUsed, charsAllocated);
589 CharType* dp = ltext+charsUsed;
590 foreach (char xch; str) *dp++ = cast(CharType)xch;
591 charsUsed += cast(uint)str.length;
593 void putChars(XCT) (const(XCT)[] str...) nothrow @nogc if (isWideCharType!XCT) {
594 import core.stdc.string : memcpy;
595 if (str.length == 0) return;
596 if (str.length >= int.max/4/dchar.sizeof) assert(0, "string too long");
597 ensurePool!(16, false)(cast(uint)str.length, ltext, charsUsed, charsAllocated);
598 static if (is(XCT == CharType)) {
599 memcpy(ltext+charsUsed, str.ptr, cast(uint)str.length*dchar.sizeof);
600 } else {
601 CharType* dp = ltext+charsUsed;
602 foreach (XCT xch; str) {
603 static if (is(CharType == wchar)) {
604 *dp++ = cast(CharType)(xch > wchar.max ? '?' : xch);
605 } else {
606 *dp++ = cast(CharType)xch;
610 charsUsed += cast(uint)str.length;
614 LayWord* words;
615 uint wordsUsed, wordsAllocated;
617 LayWord* allocWord(bool clear) () nothrow @nogc {
618 ensurePool!(16, true)(1, words, wordsUsed, wordsAllocated);
619 auto res = words+wordsUsed;
620 static if (clear) {
621 import core.stdc.string : memset;
622 memset(res, 0, (*res).sizeof);
624 res.wordNum = wordsUsed++;
625 //res.userTag = wordTag;
626 return res;
629 LayLine* lines;
630 uint linesUsed, linesAllocated;
632 LayLine* allocLine(bool clear=false) () nothrow @nogc {
633 ensurePool!(16, true)(1, lines, linesUsed, linesAllocated);
634 static if (clear) {
635 import core.stdc.string : memset;
636 auto res = lines+(linesUsed++);
637 memset(res, 0, (*res).sizeof);
638 return res;
639 } else {
640 return lines+(linesUsed++);
644 LayLine* lastLine () nothrow @nogc => (linesUsed > 0 ? lines+linesUsed-1 : null);
646 bool lastLineHasWords () nothrow @nogc => (linesUsed > 0 ? (lines[linesUsed-1].wend > lines[linesUsed-1].wstart) : false);
648 // should not be called when there are no lines, or no words in last line
649 LayWord* lastLineLastWord () nothrow @nogc => words+lastLine.wend-1;
651 static struct StyleStackItem {
652 LayFontStyle fs;
653 LayLineStyle ls;
655 StyleStackItem* styleStack;
656 uint ststackUsed, ststackAllocated;
658 private:
659 bool firstParaLine = true;
660 uint lastWordStart; // in fulltext
661 uint firstWordNotFlushed;
663 @property bool hasWordChars () const pure nothrow @safe @nogc { pragma(inline, true); return (lastWordStart < charsUsed); }
665 private:
666 // current attributes
667 LayLineStyle just; // for current paragraph
668 LayFontStyle style;
669 // user can change this alot, so don't apply that immediately
670 LayFontStyle newStyle;
671 LayLineStyle newJust;
673 private:
674 Utf8Decoder dec;
675 bool lastWasUtf;
676 bool lastWasSoftHypen;
677 int maxWidth; // maximum text width
678 LayFontStash laf;
680 private:
681 int mTextHeight = 0; // total text height
682 int mTextWidth = 0; // maximum text width
684 public:
685 // compare function should return (roughly): key-l
686 alias CmpFn = int delegate (LayLine* l) nothrow @safe @nogc;
688 /// find line using binary search and predicate
689 /// returns line number or -1
690 int findLineBinary (scope CmpFn cmpfn) nothrow @trusted @nogc {
691 if (linesUsed == 0) return -1;
692 int bot = 0, i = cast(int)linesUsed-1;
693 while (bot != i) {
694 int mid = i-(i-bot)/2;
695 int cmp = cmpfn(lines+mid);
696 if (cmp < 0) i = mid-1;
697 else if (cmp > 0) bot = mid;
698 else return mid;
700 return (cmpfn(lines+i) == 0 ? i : -1);
703 private:
704 LayObject[] mObjects; /// all known object; DON'T MODIFY!
706 public:
708 this (LayFontStash alaf, int awidth) nothrow @trusted @nogc {
709 if (alaf is null) assert(0, "no layout fonts");
710 if (awidth < 1) awidth = 1;
711 laf = alaf;
712 maxWidth = awidth;
713 if (newStyle.fontface == -1 && alaf.fixFontDG !is null) alaf.fixFontDG(alaf, newStyle);
714 if (newStyle.fontsize == 0) newStyle.fontsize = 16;
715 style = newStyle;
719 ~this () nothrow @trusted @nogc { freeMemory(); }
722 void freeMemory () nothrow @trusted @nogc {
723 import core.stdc.stdlib : free;
724 if (lines !is null) { free(lines); lines = null; }
725 if (words !is null) { free(words); words = null; }
726 if (ltext !is null) { free(ltext); ltext = null; }
727 if (styleStack !is null) { free(styleStack); styleStack = null; }
728 wordsUsed = wordsAllocated = linesUsed = linesAllocated = charsUsed = charsAllocated = ststackUsed = ststackAllocated = 0;
729 lastWasSoftHypen = false;
732 /// wipe all text and stacks, but don't deallocate memory
733 /// if `killObjects` is `true`, call `delete` on each object
734 void wipeAll (bool killObjects=false) nothrow @trusted {
735 wordsUsed = linesUsed = charsUsed = ststackUsed = 0;
736 lastWasSoftHypen = false;
737 lastWasUtf = false;
738 dec.reset;
739 mTextHeight = mTextWidth = 0;
740 if (mObjects.length) {
741 if (killObjects) foreach (ref obj; mObjects) delete obj;
742 mObjects.length = 0;
743 mObjects.assumeSafeAppend;
745 if (newStyle.fontface == -1 && laf.fixFontDG !is null) laf.fixFontDG(laf, newStyle);
746 style = newStyle;
747 just = newJust;
748 firstParaLine = true;
749 lastWordStart = 0;
750 firstWordNotFlushed = 0;
753 /// get object with the given index (return `null` on invalid index)
754 @property objectAtIndex (uint idx) nothrow @trusted @nogc => (idx < mObjects.length ? mObjects.ptr[idx] : null);
757 @property isStyleStackEmpty () const pure nothrow @trusted @nogc => (ststackUsed == 0);
759 /// push current font and justify
760 void pushStyles () nothrow @trusted @nogc {
761 ensurePool!(4, false)(1, styleStack, ststackUsed, ststackAllocated);
762 if (newStyle.fontface == -1 && laf.fixFontDG !is null) laf.fixFontDG(laf, newStyle);
763 auto si = styleStack+(ststackUsed++);
764 si.fs = newStyle;
765 si.ls = newJust;
768 /// pop last pushed font and justify
769 void popStyles () nothrow @trusted @nogc {
770 if (ststackUsed == 0) assert(0, "style stack underflow");
771 auto si = styleStack+(--ststackUsed);
772 newStyle = si.fs;
773 newJust = si.ls;
774 if (newStyle.fontface == -1 && laf.fixFontDG !is null) laf.fixFontDG(laf, newStyle);
777 @property int textHeight () const pure nothrow @safe @nogc => mTextHeight; /// total text height
778 @property int textWidth () const pure nothrow @safe @nogc => mTextWidth; /// maximum text width
780 /// find line with this word index
781 /// returns line number or -1
782 int findLineWithWord (uint idx) nothrow @trusted @nogc {
783 if (linesUsed == 0) return -1;
784 return findLineBinary((LayLine* l) {
785 if (idx < l.wstart) return -1;
786 if (idx >= l.wend) return 1;
787 return 0;
791 /// find line at this pixel coordinate
792 /// returns line number or -1
793 int findLineAtY (int y) nothrow @trusted @nogc {
794 if (linesUsed == 0) return -1;
795 if (y < 0) return 0;
796 if (y >= mTextHeight) return cast(int)linesUsed-1;
797 auto res = findLineBinary((LayLine* l) {
798 if (y < l.y) return -1;
799 if (y >= l.y+l.h) return 1;
800 return 0;
802 assert(res != -1);
803 return res;
806 /// find word at the given coordinate in the given line
807 /// returns line number or -1
808 int findWordAtX (LayLine* ln, int x) nothrow @trusted @nogc {
809 int wcmp (int wnum) {
810 auto w = words+ln.wstart+wnum;
811 if (x < w.x) return -1;
812 return (x >= (wnum+1 < ln.wordCount ? w[1].x : w.x+w.w) ? 1 : 0);
814 if (linesUsed == 0 || ln is null || ln.wordCount == 0) return -1;
815 int bot = 0, i = ln.wordCount-1;
816 while (bot != i) {
817 int mid = i-(i-bot)/2;
818 switch (wcmp(mid)) {
819 case -1: i = mid-1; break;
820 case 1: bot = mid; break;
821 default: return ln.wstart+mid;
824 return (wcmp(i) == 0 ? ln.wstart+i : -1);
827 /// find word at the given coordinates
828 /// returns word index or -1
829 int wordAtXY (int x, int y) nothrow @trusted @nogc {
830 if (linesUsed == 0) return -1;
831 auto lidx = findLineAtY(y);
832 if (lidx < 0) return -1;
833 auto ln = lines+lidx;
834 if (y < ln.y || y >= ln.y+ln.h || ln.wordCount == 0) return -1;
835 return findWordAtX(ln, x);
838 /// get word by it's index; return `null` if index is invalid
839 LayWord* wordByIndex (uint idx) pure nothrow @trusted @nogc => (idx < wordsUsed ? words+idx : null);
841 /// total number of words
842 @property uint wordCount () const pure nothrow @safe @nogc => wordsUsed;
844 /// get textual representation of the given word
845 @property const(CharType)[] wordText (in ref LayWord w) const pure nothrow @trusted @nogc => (w.wstart <= w.wend ? ltext[w.wstart..w.wend] : null);
847 /// get number of lines
848 @property int lineCount () const pure nothrow @safe @nogc => cast(int)linesUsed;
850 /// returns range with all words in the given line
851 @property auto lineWords (int lidx) nothrow @trusted @nogc {
852 static struct Range {
853 private:
854 LayWord* w;
855 int wordsLeft; // not including current
856 nothrow @trusted @nogc:
857 private:
858 this(LT) (LT lay, int lidx) {
859 if (lidx >= 0 && lidx < lay.linesUsed) {
860 auto ln = lay.lines+lidx;
861 if (ln.wend > ln.wstart) {
862 w = lay.words+ln.wstart;
863 wordsLeft = ln.wend-ln.wstart-1;
867 public:
868 @property bool empty () const pure => (w is null);
869 @property ref LayWord front () pure { pragma(inline, true); assert(w !is null); return *w; }
870 void popFront () { if (wordsLeft) { ++w; --wordsLeft; } else w = null; }
871 Range save () { Range res = void; res.w = w; res.wordsLeft = wordsLeft; return res; }
872 @property int length () const pure => (w !is null ? wordsLeft+1 : 0);
873 alias opDollar = length;
874 @property LayWord[] opSlice () => (w !is null ? w[0..wordsLeft+1] : null);
875 @property LayWord[] opSlice (int lo, int hi) {
876 if (lo < 0) lo = 0;
877 if (w is null || hi <= lo || lo > wordsLeft) return null;
878 if (hi > wordsLeft+1) hi = wordsLeft+1;
879 return w[lo..hi];
882 return Range(this, lidx);
885 /// returns layouted line object, or `null` on invalid index
886 LayLine* line (int lidx) nothrow @trusted @nogc => (lidx >= 0 && lidx < linesUsed ? lines+lidx : null);
888 /// maximum width layouter can use; note that resulting `textWidth` can be less than this
889 @property int width () const pure nothrow @safe @nogc => maxWidth;
891 /// last flushed word index
892 @property uint lastWordIndex () const pure nothrow @safe @nogc => (wordsUsed ? wordsUsed-1 : 0);
894 /// current word index
895 @property uint nextWordIndex () const pure nothrow @safe @nogc => wordsUsed+hasWordChars;
897 /// get font style object; changes will take effect on next char
898 @property ref LayFontStyle fontStyle () pure nothrow @safe @nogc => newStyle;
899 /// get line style object; changes will take effect on next line
900 @property ref LayLineStyle lineStyle () pure nothrow @safe @nogc => newJust;
902 /// return "font id" for the given font face
903 @property int fontFaceId(bool fail=true) (const(char)[] name) nothrow @safe @nogc {
904 if (laf !is null) {
905 int fid = laf.fontFaceId(name);
906 if (fid >= 0) return fid;
908 static if (fail) assert(0, "unknown font face"); // '"~name.idup~"'");
909 return -1;
912 /// return font face for the given "font id"
913 @property const(char)[] fontFace (int fid) nothrow @safe @nogc => (laf !is null ? laf.fontFace(fid) : null);
915 /// end current line
916 void endLine () nothrow @trusted @nogc => put(EndLineCh);
918 /// end current paragraph
919 void endPara () nothrow @trusted @nogc => put(EndParaCh);
921 /// end current word
922 /// can be called to ensure that last word is put into text
923 void endWord (bool spaced=true) nothrow @trusted @nogc {
924 if (hasWordChars) {
925 flushWord();
926 auto lw = words+wordsUsed-1;
927 lw.propsOrig.canbreak = true;
928 lw.propsOrig.spaced = spaced;
932 /// put non-breaking space
933 void putNBSP () nothrow @trusted @nogc => put(NBSpaceCh);
935 /// put soft hypen
936 void putSoftHypen () nothrow @trusted @nogc => put(SoftHyphenCh);
938 /// add "object" into text -- special thing that knows it's dimensions
939 void putObject (LayObject obj) @trusted {
940 import std.algorithm : max, min;
941 if (lastWasUtf) {
942 lastWasUtf = false;
943 if (!dec.complete) { dec.reset; put(' '); }
945 flushWord();
946 lastWasSoftHypen = false;
947 if (obj is null) return;
948 if (mObjects.length >= int.max/2) throw new Exception("too many mObjects");
949 just = newJust;
950 // create special word
951 auto w = allocWord!true();
952 w.wstart = cast(uint)mObjects.length; // store object index
953 w.wend = 0;
954 mObjects ~= obj;
955 w.style = style;
956 w.propsOrig.obj = true;
957 w.propsOrig.spaced = obj.spaced;
958 w.propsOrig.canbreak = obj.canbreak;
959 w.props = w.propsOrig;
960 w.w = cast(short)min(max(0, obj.width), short.max);
961 w.whyph = w.wsp = cast(short)min(w.w+max(0, obj.spacewidth), short.max);
962 w.h = cast(short)min(max(0, obj.height), short.max);
963 w.asc = cast(short)min(max(0, obj.ascent), short.max);
964 if (w.asc < 0) throw new Exception("object ascent should be positive");
965 w.desc = cast(short)min(max(0, obj.descent), short.max);
966 if (w.desc > 0) throw new Exception("object descent should be negative");
967 w.just = just;
968 w.paraPad = -1;
971 /// put "expander" (it will expand to take all unused line width on finalization).
972 /// it line contains more than one expander, all expanders will try to get same width.
973 void putExpander () nothrow @trusted @nogc {
974 if (lastWasUtf) {
975 lastWasUtf = false;
976 if (!dec.complete) { dec.reset; put(' '); }
978 flushWord();
979 if (wordsUsed > 0) {
980 auto lw = words+wordsUsed-1;
981 lw.propsOrig.canbreak = false; // cannot break before expander
982 lw.propsOrig.spaced = false;
984 lastWasSoftHypen = false;
985 // create special expander word
986 auto w = createEmptyWord();
987 // fix word properties
988 w.propsOrig.canbreak = false; // cannot break after expander
989 w.propsOrig.spaced = false;
990 w.propsOrig.hyphen = false;
991 w.propsOrig.expander = true;
992 w.propsOrig.lineend = false;
993 w.propsOrig.paraend = false;
994 w.wstart = charsUsed;
995 w.wend = charsUsed;
998 /// put "hard space" (it will always takes the given number of pixels).
999 void putHardSpace (int wdt) nothrow @trusted @nogc {
1000 putExpander(); // hack: i am too lazy to refactor the code
1001 auto lw = words+wordsUsed-1;
1002 lw.propsOrig.expander = false;
1003 lw.propsOrig.hardspace = true;
1004 if (wdt > 8192) wdt = 8192;
1005 lw.w = cast(short)(wdt < 1 ? 0 : wdt);
1008 /// add text to layouter; it is ok to mix (valid) utf-8 and dchars here
1009 void put(T) (const(T)[] str...) nothrow @trusted @nogc if (isAnyCharType!T) {
1010 if (str.length == 0) return;
1012 dchar curCh; // 0: no more chars
1013 usize stpos;
1015 static if (is(T == char)) {
1016 // utf-8 stream
1017 if (!lastWasUtf) { lastWasUtf = true; dec.reset; }
1018 void skipCh () @trusted {
1019 while (stpos < str.length) {
1020 curCh = dec.decode(cast(ubyte)str.ptr[stpos++]);
1021 if (curCh <= dchar.max) return;
1023 curCh = 0;
1025 // load first char
1026 skipCh();
1027 } else {
1028 // dchar stream
1029 void skipCh () @trusted {
1030 if (stpos < str.length) {
1031 curCh = str.ptr[stpos++];
1032 if (curCh > dchar.max) curCh = '?';
1033 } else {
1034 curCh = 0;
1037 // load first char
1038 if (lastWasUtf) {
1039 lastWasUtf = false;
1040 if (!dec.complete) curCh = '?'; else skipCh();
1041 } else {
1042 skipCh();
1046 // process stream dchars
1047 if (curCh == 0) return;
1048 if (!hasWordChars) {
1049 if (newStyle.fontface == -1 && laf.fixFontDG !is null) laf.fixFontDG(laf, newStyle);
1050 style = newStyle;
1052 if (wordsUsed == 0 || words[wordsUsed-1].propsOrig.someend) just = newJust;
1053 while (curCh) {
1054 import std.uni;
1055 dchar ch = curCh;
1056 skipCh();
1057 if (ch == EndLineCh || ch == EndParaCh) {
1058 // ignore leading empty lines
1059 if (hasWordChars) flushWord(); // has some word data, flush it now
1060 lastWasSoftHypen = false; // word flusher is using this flag
1061 auto lw = (wordsUsed ? words+wordsUsed-1 : createEmptyWord());
1062 // do i need to add empty word for attrs?
1063 if (lw.propsOrig.someend) lw = createEmptyWord();
1064 // fix word properties
1065 lw.propsOrig.canbreak = true;
1066 lw.propsOrig.spaced = false;
1067 lw.propsOrig.hyphen = false;
1068 lw.propsOrig.lineend = (ch == EndLineCh);
1069 lw.propsOrig.paraend = (ch == EndParaCh);
1070 flushWord();
1071 just = newJust;
1072 firstParaLine = (ch == EndParaCh);
1073 } else if (ch == NBSpaceCh || ch == NarrowNBSpaceCh) {
1074 // non-breaking space
1075 lastWasSoftHypen = false;
1076 if (newStyle.fontface == -1 && laf.fixFontDG !is null) laf.fixFontDG(laf, newStyle);
1077 if (hasWordChars && style != newStyle) flushWord();
1078 putChars(' ');
1079 } else if (ch == SoftHyphenCh) {
1080 // soft hyphen
1081 if (!lastWasSoftHypen && hasWordChars) {
1082 putChars('-');
1083 lastWasSoftHypen = true; // word flusher is using this flag
1084 flushWord();
1085 } else {
1086 lastWasSoftHypen = true;
1088 } else if (ch <= ' ' || isWhite(ch)) {
1089 if (hasWordChars) {
1090 flushWord();
1091 auto lw = words+wordsUsed-1;
1092 lw.propsOrig.canbreak = true;
1093 lw.propsOrig.spaced = true;
1094 } else {
1095 if (newStyle.fontface == -1 && laf.fixFontDG !is null) laf.fixFontDG(laf, newStyle);
1096 style = newStyle;
1098 lastWasSoftHypen = false;
1099 } else {
1100 lastWasSoftHypen = false;
1101 if (ch > dchar.max || ch.isSurrogate || ch.isPrivateUse || ch.isNonCharacter || ch.isMark || ch.isFormat || ch.isControl) ch = '?';
1102 if (newStyle.fontface == -1 && laf.fixFontDG !is null) laf.fixFontDG(laf, newStyle);
1103 if (hasWordChars && style != newStyle) flushWord();
1104 putChars(ch);
1105 if (isDash(ch) && charsUsed-lastWordStart > 1 && !isDash(ltext[charsUsed-2])) flushWord();
1110 /// remove `count` words from index `idx`.
1111 /// you will need to call `finalize()` or forced relayouting after this.
1112 /// make sure to clear `udata` in deleted words
1113 void removeWordsAt (uint idx, int count) nothrow @trusted @nogc {
1114 flushWord();
1115 lastWasSoftHypen = false;
1116 if (count < 1 || idx >= wordsUsed) return;
1117 if (count > wordsUsed-idx) count = cast(int)(wordsUsed-idx);
1118 if (idx+count >= wordsUsed) {
1119 // just trim it
1120 wordsUsed = idx;
1121 } else {
1122 import core.stdc.string : memmove;
1123 memmove(words+idx, words+idx+count, (wordsUsed-idx-count)*words[0].sizeof);
1124 wordsUsed -= count;
1125 // fix word numbers
1126 foreach (ref LayWord w; words[idx..wordsUsed]) w.wordNum = idx++;
1130 /// "finalize" layout: calculate lines, layout words...
1131 /// call this after you done feeding text
1132 void finalize () nothrow @trusted @nogc {
1133 flushWord();
1134 lastWasSoftHypen = false;
1135 relayout(maxWidth, true);
1138 /// relayout everything using the existing words
1139 void relayout (int newWidth, bool forced=false) nothrow @trusted @nogc {
1140 if (newWidth < 1) newWidth = 1;
1141 if (!forced && newWidth == maxWidth) return;
1142 auto odepth = ststackUsed;
1143 scope(exit) {
1144 while (ststackUsed > odepth) popStyles();
1146 if (newStyle.fontface == -1 && laf.fixFontDG !is null) laf.fixFontDG(laf, newStyle);
1147 maxWidth = newWidth;
1148 linesUsed = 0;
1149 if (linesAllocated > 0) {
1150 import core.stdc.string : memset;
1151 memset(lines, 0, linesAllocated*lines[0].sizeof);
1153 uint widx = 0;
1154 uint wu = wordsUsed;
1155 mTextWidth = 0;
1156 mTextHeight = 0;
1157 firstParaLine = true;
1158 scope(exit) firstWordNotFlushed = wu;
1159 while (widx < wu) {
1160 uint lend = widx;
1161 while (lend < wu) {
1162 auto w = words+(lend++);
1163 if (w.expander) w.w = w.wsp = w.whyph = 0; // will be fixed in `flushLines()`
1164 if (w.propsOrig.someend) break;
1166 flushLines(widx, lend);
1167 widx = lend;
1168 firstParaLine = words[widx-1].propsOrig.paraend;
1172 public:
1174 // don't use
1175 void save (VFile fl) {
1176 fl.rawWriteExact("XLL0");
1177 fl.rawWriteExact(ltext[0..charsUsed]);
1178 fl.rawWriteExact(words[0..wordsUsed]);
1182 public:
1183 // don't use
1184 debug(xlayouter_dump) void dump (VFile fl) const {
1185 import iv.vfs.io;
1186 fl.writeln("LINES: ", linesUsed);
1187 foreach (immutable idx, const ref ln; lines[0..linesUsed]) {
1188 fl.writeln("LINE #", idx, ": ", ln.wordCount, " words; just=", ln.just.toString, "; jlpad=", ln.just.leftpad, "; y=", ln.y, "; h=", ln.h, "; desc=", ln.desc);
1189 foreach (immutable widx, const ref w; words[ln.wstart..ln.wend]) {
1190 fl.writeln(" WORD #", widx, "(", w.wordNum, ")[", w.wstart, "..", w.wend, "]: ", wordText(w));
1191 fl.writeln(" wbreak=", w.props.canbreak, "; wspaced=", w.props.spaced, "; whyphen=", w.props.hyphen, "; style=", w.style.toString);
1192 fl.writeln(" x=", w.x, "; w=", w.w, "; h=", w.h, "; asc=", w.asc, "; desc=", w.desc);
1197 private:
1198 static bool isDash (dchar ch) pure nothrow @trusted @nogc {
1199 pragma(inline, true);
1200 return (ch == '-' || (ch >= 0x2013 && ch == 0x2015) || ch == 0x2212);
1203 LayWord* createEmptyWord () nothrow @trusted @nogc {
1204 assert(!hasWordChars);
1205 auto w = allocWord!true();
1206 w.style = style;
1207 w.props = w.propsOrig;
1208 // set word dimensions
1209 if (w.style.fontface < 0) assert(0, "invalid font face in word style");
1210 laf.font = w.style;
1211 w.w = w.wsp = w.whyph = 0;
1212 w.udata = 0;
1213 // calculate ascent, descent and height
1215 int a, d, h;
1216 laf.textMetrics(&a, &d, &h);
1217 w.asc = cast(short)a;
1218 w.desc = cast(short)d;
1219 w.h = cast(short)h;
1221 w.just = just;
1222 w.paraPad = -1;
1223 if (newStyle.fontface == -1 && laf.fixFontDG !is null) laf.fixFontDG(laf, newStyle);
1224 style = newStyle;
1225 return w;
1228 void flushWord () nothrow @trusted @nogc {
1229 if (hasWordChars) {
1230 auto w = allocWord!true();
1231 w.wstart = lastWordStart;
1232 w.wend = charsUsed;
1233 //{ import iv.encoding, std.conv : to; writeln("adding word: [", wordText(*w).to!string.recodeToKOI8, "]"); }
1234 w.propsOrig.hyphen = lastWasSoftHypen;
1235 if (lastWasSoftHypen) {
1236 w.propsOrig.canbreak = true;
1237 w.propsOrig.spaced = false;
1238 --w.wend; // remove hyphen mark (for now)
1240 w.style = style;
1241 w.props = w.propsOrig;
1242 w.props.hyphen = false;
1243 // set word dimensions
1244 if (w.style.fontface < 0) assert(0, "invalid font face in word style");
1245 laf.font = w.style;
1246 // i may need spacing later, and anyway most words should be with spacing, so calc it unconditionally
1247 if (w.wend > w.wstart) {
1248 auto t = wordText(*w);
1249 int ww, wsp, whyph;
1250 laf.textWidth2(t, &ww, &wsp, (w.propsOrig.hyphen ? &whyph : null));
1251 w.w = cast(short)ww;
1252 w.wsp = cast(short)wsp;
1253 if (!w.propsOrig.hyphen) w.whyph = w.w; else w.whyph = cast(short)whyph;
1254 if (isDash(t[$-1])) { w.propsOrig.canbreak = true; w.props.canbreak = true; }
1255 } else {
1256 w.w = w.wsp = w.whyph = 0;
1258 // calculate ascent, descent and height
1260 int a, d, h;
1261 laf.textMetrics(&a, &d, &h);
1262 w.asc = cast(short)a;
1263 w.desc = cast(short)d;
1264 w.h = cast(short)h;
1266 w.just = just;
1267 w.paraPad = -1;
1268 lastWordStart = charsUsed;
1270 if (newStyle.fontface == -1 && laf.fixFontDG !is null) laf.fixFontDG(laf, newStyle);
1271 style = newStyle;
1274 // [curw..endw)"
1275 void flushLines (uint curw, uint endw) nothrow @trusted @nogc {
1276 if (curw < endw) {
1277 debug(xlay_line_flush) conwriteln("flushing ", endw-curw, " words");
1278 uint stline = linesUsed; // reformat from this
1279 // fix word styles
1280 foreach (ref LayWord w; words[curw..endw]) {
1281 if (w.props.hyphen) --w.wend; // remove hyphen mark
1282 w.props = w.propsOrig;
1283 w.props.hyphen = false;
1285 LayLine *ln;
1286 LayWord *w = words+curw;
1287 while (curw < endw) {
1288 debug(xlay_line_flush) conwriteln(" ", endw-curw, " words left");
1289 if (ln is null) {
1290 // add line to work with
1291 ln = allocLine();
1292 ln.wstart = ln.wend = curw;
1293 ln.just = w.just;
1294 ln.w = w.just.leftpad+w.just.rightpad;
1295 // indent first line of paragraph
1296 if (firstParaLine) {
1297 firstParaLine = false;
1298 // left-side or justified lines has paragraph indent
1299 if (ln.just.paraIndent > 0 && (w.just.left || w.just.justify)) {
1300 auto ind = w.paraPad;
1301 if (ind < 0) {
1302 laf.font = w.style;
1303 ind = cast(short)laf.spacesWidth(ln.just.paraIndent);
1304 w.paraPad = ind;
1306 ln.w += ind;
1307 ln.just.leftpad = ln.just.leftpad+ind;
1308 } else {
1309 w.paraPad = 0;
1312 //conwriteln("new line; maxWidth=", maxWidth, "; starting line width=", ln.w);
1313 //conwriteln("* maxWidth=", maxWidth, "; ln.w=", ln.w, "; leftpad=", ln.just.leftpad, "; rightpad=", ln.just.rightpad);
1315 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))));
1316 // add words until i hit breaking point
1317 // if it will end beyond maximum width, and this line
1318 // has some words, flush the line and start new one
1319 uint startIndex = curw;
1320 int curwdt = ln.w, lastwsp = 0;
1321 int hyphenWdt = 0;
1322 while (curw < endw) {
1323 // add word width with spacing (i will compensate for that after loop)
1324 lastwsp = (w.propsOrig.spaced ? w.wsp-w.w : 0);
1325 curwdt += w.w+lastwsp;
1326 ++curw; // advance counter here...
1327 if (w.propsOrig.hyphen) { hyphenWdt = w.whyph-w.w; if (hyphenWdt < 0) hyphenWdt = 0; } else hyphenWdt = 0;
1328 if (w.props.canbreak) break; // done with this span
1329 ++w; // ...and word pointer here (skipping one inc at the end ;-)
1331 debug(xlay_line_flush) conwriteln(" ", curw-startIndex, " words processed");
1332 // can i add the span? if this is first span in line, add it unconditionally
1333 if (ln.wordCount == 0 || curwdt+hyphenWdt-lastwsp <= maxWidth) {
1334 //if (hyphenWdt) { import core.stdc.stdio; printf("curwdt=%d; hwdt=%d; next=%d; max=%d\n", curwdt, hyphenWdt, curwdt+hyphenWdt-lastwsp, maxWidth); }
1335 // yay, i can!
1336 ln.wend = curw;
1337 ln.w = curwdt;
1338 ++w; // advance to curw
1339 debug(xlay_line_flush) conwriteln("curwdt=", curwdt, "; maxWidth=", maxWidth, "; wc=", ln.wordCount, "(", ln.wend-ln.wstart, ")");
1340 } else {
1341 // nope, start new line here
1342 debug(xlay_line_flush) conwriteln("added line with ", ln.wordCount, " words");
1343 // last word in the line should not be spaced
1344 auto ww = words+ln.wend-1;
1345 // compensate for spacing at last word
1346 ln.w -= (ww.props.spaced ? ww.wsp-ww.w : 0);
1347 ww.props.spaced = false;
1348 // and should have hyphen mark if it is necessary
1349 if (ww.propsOrig.hyphen) {
1350 assert(!ww.props.hyphen);
1351 ww.props.hyphen = true;
1352 ++ww.wend;
1353 // fix line width (word layouter will use that)
1354 ln.w += ww.whyph-ww.w;
1356 ln = null;
1357 curw = startIndex;
1358 w = words+curw;
1361 debug(xlay_line_flush) conwriteln("added line with ", ln.wordCount, " words; new lines range: [", stline, "..", linesUsed, "]");
1362 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))));
1363 // last line should not be justified
1364 if (ln.just.justify) ln.just.setLeft;
1365 // do real word layouting and fix line metrics
1366 debug(xlay_line_flush) conwriteln("added ", linesUsed-stline, " lines");
1367 foreach (uint lidx; stline..linesUsed) {
1368 debug(xlay_line_flush) conwriteln(": lidx=", lidx, "; wc=", lines[lidx].wordCount);
1369 layoutLine(lidx);
1370 debug(xlay_line_flush_ex) conwriteln(": lidx=", lidx, "; wc=", lines[lidx].wordCount, "; y=", lines[lidx].y);
1375 // do word layouting and fix line metrics
1376 void layoutLine (uint lidx) nothrow @trusted @nogc {
1377 import std.algorithm : max, min;
1378 assert(lidx < linesUsed);
1379 auto ln = lines+lidx;
1380 //conwriteln("maxWidth=", maxWidth, "; ln.w=", ln.w, "; leftpad=", ln.just.leftpad, "; rightpad=", ln.just.rightpad);
1381 debug(xlay_line_layout) conwriteln("lidx=", lidx, "; wc=", ln.wordCount);
1382 // y position
1383 ln.y = (lidx ? ln[-1].y+ln[-1].h : 0);
1384 auto lwords = lineWords(lidx);
1385 assert(!lwords.empty); // i should have at least one word in each line
1386 // line width is calculated for us by `flushLines()`
1387 // calculate line metrics and number of words with spacing
1388 int expanderCount = 0;
1389 int lineH, lineDesc, wspCount;
1390 foreach (ref LayWord w; lwords.save) {
1391 lineH = max(lineH, w.h);
1392 lineDesc = min(lineDesc, w.desc);
1393 if (w.props.spaced) ++wspCount;
1394 if (w.expander) ++expanderCount;
1396 // process expanders
1397 if (expanderCount > 0 && ln.w < maxWidth) {
1398 int expanderWdt = (maxWidth-ln.w)/expanderCount;
1399 int expanderLeft = (maxWidth-ln.w)-expanderWdt*expanderCount;
1400 debug(xlayouter_expander) conwriteln("expanderWdt=", expanderWdt, "; expanderLeft=", expanderLeft);
1401 foreach (ref LayWord w; lwords) {
1402 if (w.propsOrig.expander) {
1403 assert(w.w == 0);
1404 w.w = cast(short)(expanderWdt+(expanderLeft-- > 0 ? 1 : 0));
1405 ln.w += w.w;
1409 // vertical padding; clamp it, as i can't have line over line (it will break too many things)
1410 lineH += ln.just.toppad+ln.just.bottompad;
1411 ln.h = lineH;
1412 ln.desc = lineDesc;
1413 if (ln.w >= maxWidth) {
1414 //conwriteln("*** ln.w=", ln.w, "; maxWidth=", maxWidth);
1415 // way too long; (almost) easy deal
1416 // calculate free space to spare in case i'll need to compensate hyphen mark
1417 int x = ln.just.leftpad, spc = 0;
1418 foreach (ref LayWord w; lwords.save) {
1419 w.x = cast(short)x;
1420 x += w.fullwidth;
1421 if (w.props.spaced) spc += w.wsp-w.w;
1423 // if last word ends with hyphen, try to compensate it
1424 if (words[ln.wend-1].props.hyphen) {
1425 int needspc = ln.w-maxWidth;
1426 // no more than 8 pix or 2/3 of free space
1427 if (needspc <= 8 && needspc <= spc/3*2) {
1428 // compensate (i can do fractional math here, but meh...)
1429 while (needspc > 0) {
1430 // excellence in coding!
1431 foreach_reverse (immutable widx; ln.wstart..ln.wend) {
1432 if (words[widx].props.spaced) {
1433 --ln.w;
1434 foreach (immutable c; widx+1..ln.wend) words[c].x -= 1;
1435 if (--needspc == 0) break;
1441 } else if (ln.just.justify && wspCount > 0) {
1442 // fill the whole line
1443 int spc = maxWidth-ln.w; // space left to distribute
1444 int xadvsp = spc/wspCount;
1445 int frac = spc-xadvsp*wspCount;
1446 int x = ln.just.leftpad;
1447 // no need to save range here, i'll do it in one pass
1448 foreach (ref LayWord w; lwords) {
1449 w.x = cast(short)x;
1450 x += w.fullwidth;
1451 if (w.props.spaced) {
1452 x += xadvsp;
1453 //spc -= xadvsp;
1454 if (frac-- > 0) {
1455 ++x;
1456 //--spc;
1460 //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);
1461 //assert(x == maxWidth-ln.just.rightpad);
1462 } else {
1463 int x;
1464 if (ln.just.left || ln.just.justify) x = ln.just.leftpad;
1465 else if (ln.just.right) x = maxWidth-ln.w+ln.just.leftpad;
1466 else if (ln.just.center) x = (maxWidth-(ln.w-ln.just.leftpad-ln.just.rightpad))/2;
1467 else assert(0, "wtf?!");
1468 // no need to save range here, i'll do it in one pass
1469 foreach (ref LayWord w; lwords) {
1470 w.x = cast(short)x;
1471 x += w.fullwidth;
1474 if (ln.h < 1) ln.h = 1;
1475 // done
1476 mTextWidth = max(mTextWidth, ln.w);
1477 mTextHeight = ln.y+ln.h;
1478 debug(xlay_line_layout) conwriteln("lidx=", lidx, "; wc=", ln.wordCount);