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/>.
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
;
26 import iv
.nanovega
.nanovega
;
31 version(laytest
) import iv
.encoding
;
33 private int lrintf (float f
) { pragma(inline
, true); return cast(int)(f
+0.5); }
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?
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
{
68 bool fontWasSet
; // to ensure that first call to `setFont()` will do it's work
69 LayFontStyle lastStyle
;
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
;
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;
83 //{ import core.stdc.stdio; printf("*** reusing font stash!\n"); }
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");
91 fs
.textAlign
= NVGTextAlign(NVGTextAlign
.H
.Left
, NVGTextAlign
.V
.Baseline
);
96 ~this () nothrow @nogc { freeFontStash(); }
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;
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
);
126 void size (int fsz
) nothrow @safe @nogc {
127 if (fsz
< 1) fsz
= 1;
129 lastStyle
.fontsize
= fsz
;
133 void font (int fid
) nothrow @safe @nogc {
134 if (fid
== FONS_INVALID
) {
136 lastStyle
.fontface
= -1;
137 } else if (!fontWasSet || lastStyle
.fontface
!= fid
) {
139 lastStyle
.fontface
= fid
;
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
;
158 lastStyle
.fontsize
= fsz
;
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");
168 float adv
= fs
.getTextBounds(0, 0, str, b
[]);
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);
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;
189 auto it
= FONSTextBoundsIterator(fs
, 0, 0);
192 it
.getHBounds(minx
, maxx
);
193 *w
= cast(int)lrintf(max(it
.advance
, maxx
-minx
));
195 if (wsp
!is null && whyph
is null) {
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) {
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
{
247 Underline
= 1<<3, ///
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
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";
270 // this generates getter and setter for each `Flag`
272 auto res
= CTFECharBuffer
!false(3000);
273 foreach (immutable string s
; __traits(allMembers
, Flag
)) {
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");
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.");
287 res
.put(")) != v) fontface = -1; ");
289 res
.put("if (v) flags |= Flag.");
291 res
.put("; else flags &= ~Flag.");
296 return res
.asString
; // it is safe to cast here
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;
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 // ////////////////////////////////////////////////////////////////////////// //
312 public align(1) struct LayLineStyle
{
315 enum Justify
: ubyte {
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
;
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
);
339 // this generates getter and setter for each `Justify` mode
341 auto res
= CTFECharBuffer
!false(1024); // currently it is ~900
342 foreach (immutable string s
; __traits(allMembers
, Justify
)) {
344 res
.put("@property bool ");
345 res
.putStrLoCasedFirst(s
);
346 res
.put(" () const pure nothrow @safe @nogc { pragma(inline, true); return (mode == Justify.");
349 // setter (in the form of `setLeft`, etc.)
350 res
.put("ref LayLineStyle set");
352 res
.put(" () pure nothrow @safe @nogc { mode = Justify.");
354 res
.put("; return this; }\n");
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
{
378 static align(1) struct Props
{
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...
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
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; ///
464 public final class LayTextImpl(TBT
=char) if (isAnyCharType
!TBT
) {
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
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
;
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");
523 import core
.stdc
.string
: memset
;
524 memset(np
+used
, 0, newsz
-cursz
);
527 alloced
= newsz
/cast(uint)T
.sizeof
;
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
;
538 buf
.ptr
[0] = cast(char)(ch
&0xff);
539 return buf
.ptr
[0..1];
542 buf
.ptr
[0] = cast(char)(0xC0|
(ch
>>6));
543 buf
.ptr
[1] = cast(char)(0x80|
(ch
&0x3F));
544 return buf
.ptr
[0..2];
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];
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");
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
);
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
);
601 CharType
* dp
= ltext
+charsUsed
;
602 foreach (XCT xch
; str) {
603 static if (is(CharType
== wchar)) {
604 *dp
++ = cast(CharType
)(xch
> wchar.max ?
'?' : xch
);
606 *dp
++ = cast(CharType
)xch
;
610 charsUsed
+= cast(uint)str.length
;
615 uint wordsUsed
, wordsAllocated
;
617 LayWord
* allocWord(bool clear
) () nothrow @nogc {
618 ensurePool
!(16, true)(1, words
, wordsUsed
, wordsAllocated
);
619 auto res
= words
+wordsUsed
;
621 import core
.stdc
.string
: memset
;
622 memset(res
, 0, (*res
).sizeof
);
624 res
.wordNum
= wordsUsed
++;
625 //res.userTag = wordTag;
630 uint linesUsed
, linesAllocated
;
632 LayLine
* allocLine(bool clear
=false) () nothrow @nogc {
633 ensurePool
!(16, true)(1, lines
, linesUsed
, linesAllocated
);
635 import core
.stdc
.string
: memset
;
636 auto res
= lines
+(linesUsed
++);
637 memset(res
, 0, (*res
).sizeof
);
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
{
655 StyleStackItem
* styleStack
;
656 uint ststackUsed
, ststackAllocated
;
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
); }
666 // current attributes
667 LayLineStyle just
; // for current paragraph
669 // user can change this alot, so don't apply that immediately
670 LayFontStyle newStyle
;
671 LayLineStyle newJust
;
676 bool lastWasSoftHypen
;
677 int maxWidth
; // maximum text width
681 int mTextHeight
= 0; // total text height
682 int mTextWidth
= 0; // maximum text width
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;
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
;
700 return (cmpfn(lines
+i
) == 0 ? i
: -1);
704 LayObject
[] mObjects
; /// all known object; DON'T MODIFY!
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;
713 if (newStyle
.fontface
== -1 && alaf
.fixFontDG
!is null) alaf
.fixFontDG(alaf
, newStyle
);
714 if (newStyle
.fontsize
== 0) newStyle
.fontsize
= 16;
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;
739 mTextHeight
= mTextWidth
= 0;
740 if (mObjects
.length
) {
741 if (killObjects
) foreach (ref obj
; mObjects
) delete obj
;
743 mObjects
.assumeSafeAppend
;
745 if (newStyle
.fontface
== -1 && laf
.fixFontDG
!is null) laf
.fixFontDG(laf
, newStyle
);
748 firstParaLine
= true;
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
++);
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
);
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;
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;
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;
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;
817 int mid
= i
-(i
-bot
)/2;
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
{
855 int wordsLeft
; // not including current
856 nothrow @trusted @nogc:
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;
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
) {
877 if (w
is null || hi
<= lo || lo
> wordsLeft
) return null;
878 if (hi
> wordsLeft
+1) hi
= wordsLeft
+1;
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 {
905 int fid
= laf
.fontFaceId(name
);
906 if (fid
>= 0) return fid
;
908 static if (fail
) assert(0, "unknown font face"); // '"~name.idup~"'");
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);
916 void endLine () nothrow @trusted @nogc => put(EndLineCh
);
918 /// end current paragraph
919 void endPara () nothrow @trusted @nogc => put(EndParaCh
);
922 /// can be called to ensure that last word is put into text
923 void endWord (bool spaced
=true) nothrow @trusted @nogc {
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
);
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
;
943 if (!dec.complete
) { dec.reset
; put(' '); }
946 lastWasSoftHypen
= false;
947 if (obj
is null) return;
948 if (mObjects
.length
>= int.max
/2) throw new Exception("too many mObjects");
950 // create special word
951 auto w
= allocWord
!true();
952 w
.wstart
= cast(uint)mObjects
.length
; // store object index
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");
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 {
976 if (!dec.complete
) { dec.reset
; put(' '); }
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
;
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
1015 static if (is(T
== char)) {
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;
1029 void skipCh () @trusted {
1030 if (stpos
< str.length
) {
1031 curCh
= str.ptr
[stpos
++];
1032 if (curCh
> dchar.max
) curCh
= '?';
1040 if (!dec.complete
) curCh
= '?'; else 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
);
1052 if (wordsUsed
== 0 || words
[wordsUsed
-1].propsOrig
.someend
) just
= newJust
;
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
);
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();
1079 } else if (ch
== SoftHyphenCh
) {
1081 if (!lastWasSoftHypen
&& hasWordChars
) {
1083 lastWasSoftHypen
= true; // word flusher is using this flag
1086 lastWasSoftHypen
= true;
1088 } else if (ch
<= ' ' ||
isWhite(ch
)) {
1091 auto lw
= words
+wordsUsed
-1;
1092 lw
.propsOrig
.canbreak
= true;
1093 lw
.propsOrig
.spaced
= true;
1095 if (newStyle
.fontface
== -1 && laf
.fixFontDG
!is null) laf
.fixFontDG(laf
, newStyle
);
1098 lastWasSoftHypen
= false;
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();
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 {
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
) {
1122 import core
.stdc
.string
: memmove
;
1123 memmove(words
+idx
, words
+idx
+count
, (wordsUsed
-idx
-count
)*words
[0].sizeof
);
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 {
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
;
1144 while (ststackUsed
> odepth
) popStyles();
1146 if (newStyle
.fontface
== -1 && laf
.fixFontDG
!is null) laf
.fixFontDG(laf
, newStyle
);
1147 maxWidth
= newWidth
;
1149 if (linesAllocated
> 0) {
1150 import core
.stdc
.string
: memset
;
1151 memset(lines
, 0, linesAllocated
*lines
[0].sizeof
);
1154 uint wu
= wordsUsed
;
1157 firstParaLine
= true;
1158 scope(exit
) firstWordNotFlushed
= 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
);
1168 firstParaLine
= words
[widx
-1].propsOrig
.paraend
;
1175 void save (VFile fl) {
1176 fl.rawWriteExact("XLL0");
1177 fl.rawWriteExact(ltext[0..charsUsed]);
1178 fl.rawWriteExact(words[0..wordsUsed]);
1184 debug(xlayouter_dump
) void dump (VFile fl
) const {
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
);
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();
1207 w
.props
= w
.propsOrig
;
1208 // set word dimensions
1209 if (w
.style
.fontface
< 0) assert(0, "invalid font face in word style");
1211 w
.w
= w
.wsp
= w
.whyph
= 0;
1213 // calculate ascent, descent and height
1216 laf
.textMetrics(&a
, &d
, &h
);
1217 w
.asc
= cast(short)a
;
1218 w
.desc
= cast(short)d
;
1223 if (newStyle
.fontface
== -1 && laf
.fixFontDG
!is null) laf
.fixFontDG(laf
, newStyle
);
1228 void flushWord () nothrow @trusted @nogc {
1230 auto w
= allocWord
!true();
1231 w
.wstart
= lastWordStart
;
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)
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");
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
);
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; }
1256 w
.w
= w
.wsp
= w
.whyph
= 0;
1258 // calculate ascent, descent and height
1261 laf
.textMetrics(&a
, &d
, &h
);
1262 w
.asc
= cast(short)a
;
1263 w
.desc
= cast(short)d
;
1268 lastWordStart
= charsUsed
;
1270 if (newStyle
.fontface
== -1 && laf
.fixFontDG
!is null) laf
.fixFontDG(laf
, newStyle
);
1275 void flushLines (uint curw
, uint endw
) nothrow @trusted @nogc {
1277 debug(xlay_line_flush
) conwriteln("flushing ", endw
-curw
, " words");
1278 uint stline
= linesUsed
; // reformat from this
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;
1286 LayWord
*w
= words
+curw
;
1287 while (curw
< endw
) {
1288 debug(xlay_line_flush
) conwriteln(" ", endw
-curw
, " words left");
1290 // add line to work with
1292 ln
.wstart
= ln
.wend
= curw
;
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
;
1303 ind
= cast(short)laf
.spacesWidth(ln
.just
.paraIndent
);
1307 ln
.just
.leftpad
= ln
.just
.leftpad
+ind
;
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;
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); }
1338 ++w
; // advance to curw
1339 debug(xlay_line_flush
) conwriteln("curwdt=", curwdt
, "; maxWidth=", maxWidth
, "; wc=", ln
.wordCount
, "(", ln
.wend
-ln
.wstart
, ")");
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;
1353 // fix line width (word layouter will use that)
1354 ln
.w
+= ww
.whyph
-ww
.w
;
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
);
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
);
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
) {
1404 w
.w
= cast(short)(expanderWdt
+(expanderLeft
-- > 0 ?
1 : 0));
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
;
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
) {
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
) {
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
) {
1451 if (w
.props
.spaced
) {
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);
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
) {
1474 if (ln
.h
< 1) ln
.h
= 1;
1476 mTextWidth
= max(mTextWidth
, ln
.w
);
1477 mTextHeight
= ln
.y
+ln
.h
;
1478 debug(xlay_line_layout
) conwriteln("lidx=", lidx
, "; wc=", ln
.wordCount
);