egra: added some `foreach` iterators
[iv.d.git] / egra / gui / style.d
blob13dd566df80796880af8ed3327b83ca5af92664e
1 /*
2 * Simple Framebuffer GUI
4 * coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
5 * Understanding is not required. Only obedience.
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, version 3 of the License ONLY.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 module iv.egra.gui.style;
21 import iv.egra.gfx.base;
23 import iv.cmdcon;
24 import iv.dynstring;
25 import iv.strex;
26 import iv.xcolornames;
28 //version = egra_style_dynstr_debug;
29 //version = egra_style_debug_search;
32 // ////////////////////////////////////////////////////////////////////////// //
33 static immutable defaultStyleText = `
34 // inactive window
35 SubWindow {
36 frame: #ddd;
37 title-back: #ddd;
38 title-text: black;
39 shadow-color: rgba(0, 0, 0, 127);
40 shadow-size: 8;
41 shadow-dash: 0;
42 back: rgb(0, 0, 180);
44 drag-overlay-back: rgba(255, 127, 0, 79);
45 drag-overlay-dash: 0; /* boolean */
48 drag-overlay-back: #070;
49 drag-overlay-dash: 1; // boolean
50 //shadow-color: rgba(0, 0, 0, 42);
53 // for widgets
54 text: gray79;
55 hotline: gray79;
57 text-cursor-0: transparent;
58 text-cursor-1: transparent;
60 text-cursor-blink-time: 800; /* in milliseconds */
61 text-cursor-dot-time: 100; /* dot crawl time, in milliseconds */
64 SubWindow.minimised {
65 icon-size-x: 24;
66 icon-size-y: 24;
67 icon-margin-x: 4;
68 icon-margin-y: 4;
71 SubWindow:focused {
72 frame: white;
73 title-back: white;
75 // for widgets
76 text: white;
78 text-cursor-0: grey67;
79 text-cursor-1: grey67;
83 YesNoWindow {
84 frame: #ddd;
85 title-back: #ddd;
86 title-text: black;
88 back: rgb(0xbf, 0xcf, 0xef);
89 text: rgb(0x16, 0x2d, 0x59);
90 hotline: rgb(0x16, 0x2d, 0x59);
93 YesNoWindow:focused {
94 frame: rgb(0x40, 0x70, 0xcf);
95 title-back: rgb(0x73, 0x96, 0xdc);
96 title-text: rgb(0xff, 0xff, 0xff);
100 ProgressBarWidget {
101 rect: gray20;
102 back: rgb(17, 17, 0);
103 text: white;
104 stripe: rgb(52, 52, 38);
106 back-hishade: rgb(52, 52, 38);
107 stripe-hishade: rgb(82, 82, 70);
109 back-full: rgb(159, 71, 0);
110 stripe-full: rgb(173, 98, 38);
112 back-full-hishade: rgb(173, 98, 38);
113 stripe-full-hishade: rgb(203, 128, 70);
117 ButtonWidget {
118 back: grey67;
119 text: black;
122 ButtonWidget:focused {
123 back: grey98;
124 text: #003;
127 ButtonWidget:disabled {
128 back: grey55;
129 text: grey12;
130 hotline: transparent;
134 ButtonExWidget {
135 back: rgb(0x73, 0x96, 0xdc);
136 text: rgb(0xff, 0xff, 0xff);
137 rect: rgb(0x40, 0x70, 0xcf);
138 shadowline: rgb(0x83, 0xa6, 0xec);
141 ButtonExWidget:focused {
142 back: rgb(0x93, 0xb6, 0xfc);
143 shadowline: rgb(0xa3, 0xc6, 0xff);
147 CheckboxWidget {
148 //text: grey75;
149 mark: #0d0;
152 CheckboxWidget:focused {
153 //text: white;
154 back: #004;
155 mark: #0f0;
158 CheckboxWidget:disabled {
159 text: grey40;
160 back: transparent;
161 mark: grey40;
165 SimpleListBoxWidget {
166 back: #004;
167 text: #ff0;
168 cursor-back: #066;
169 cursor-text: white;
172 SimpleListBoxWidget:focused {
173 cursor-back: #044;
174 cursor-text: #ddd;
178 EditorWidget {
179 back: #007;
181 status-back: white;
182 status-color: black;
184 text: rgb(220, 220, 0);
185 quote0-text: rgb(128, 128, 0);
186 quote1-text: rgb(0, 128, 128);
187 wrap-mark-text: rgb(0, 90, 220);
189 attach-file-text: rgb(0x6e, 0x00, 0xff);
190 attach-bad-text: red;
192 // marked block
193 mark-back: rgb(0, 160, 160);
194 mark-text: white;
198 LineEditWidget {
199 back: black;
200 text: rgb(220, 220, 0);
205 // ////////////////////////////////////////////////////////////////////////// //
206 struct EgraCIString {
207 public:
208 static uint joaatHashPart (const(void)[] buf, uint hash=0) pure nothrow @trusted @nogc {
209 pragma(inline, true);
210 foreach (immutable ubyte b; cast(const(ubyte)[])buf) {
211 //hash += (uint8_t)locase1251(*s++);
212 hash += b|0x20; // this converts ASCII capitals to locase (and destroys other, but who cares)
213 hash += hash<<10;
214 hash ^= hash>>6;
216 return hash;
219 static uint joaatHashFinish (uint hash) pure nothrow @trusted @nogc {
220 pragma(inline, true);
221 // final mix
222 hash += hash<<3;
223 hash ^= hash>>11;
224 hash += hash<<15;
225 return hash;
228 // ascii only
229 static bool strEquCI (const(char)[] s0, const(char)[] s1) pure nothrow @trusted @nogc {
230 if (s0.length != s1.length) return false;
231 if (s0.ptr == s1.ptr) return true;
232 foreach (immutable idx, char c0; s0) {
233 // try the easiest case first
234 if (c0 == s1.ptr[idx]) continue;
235 c0 |= 0x20; // convert to ascii lowercase
236 if (c0 < 'a' || c0 > 'z') return false; // it wasn't a letter, no need to check the second char
237 // c0 is guaranteed to be a lowercase ascii here
238 if (c0 != (s1.ptr[idx]|0x20)) return false; // c1 will become a lowercase ascii only if it was uppercase/lowercase ascii
240 return true;
243 private:
244 uint hashCurr; // current hash
245 dynstring xstr;
247 nothrow @trusted @nogc:
248 public:
249 alias getData this;
251 public:
252 this() (in auto ref dynstring s) { pragma(inline, true); xstr = s; hashCurr = joaatHashPart(xstr.getData); }
253 this (const(char)[] s) { pragma(inline, true); xstr = s; hashCurr = joaatHashPart(xstr.getData); }
254 this (in char ch) { pragma(inline, true); xstr = ch; hashCurr = joaatHashPart(xstr.getData); }
256 ~this () { pragma(inline, true); xstr.clear(); hashCurr = 0; }
258 void clear () { pragma(inline, true); xstr.clear(); hashCurr = 0; }
260 @property dynstring str () const { pragma(inline, true); return dynstring(xstr); }
261 @property void str() (in auto ref dynstring s) { pragma(inline, true); xstr = s; hashCurr = joaatHashPart(xstr.getData); }
262 @property void str (const(char)[] s) { pragma(inline, true); xstr = s; hashCurr = joaatHashPart(xstr.getData); }
264 @property uint length () const pure { pragma(inline, true); return xstr.length; }
266 @property const(char)[] getData () const pure { pragma(inline, true); return xstr.getData; }
268 void opAssign() (const(char)[] s) { pragma(inline, true); xstr = s; hashCurr = joaatHashPart(xstr.getData); }
269 void opAssign() (in char ch) { pragma(inline, true); xstr = ch; hashCurr = joaatHashPart(xstr.getData); }
270 void opAssign() (in auto ref dynstring s) { pragma(inline, true); xstr = s.xstr; hashCurr = joaatHashPart(xstr.getData); }
271 void opAssign() (in auto ref EgraCIString s) { pragma(inline, true); xstr = s.xstr; hashCurr = s.hashCurr; }
273 void opOpAssign(string op:"~") (const(char)[] s) { pragma(inline, true); if (s.length) { xstr ~= s; hashCurr = joaatHashPart(xstr.getData); } }
274 void opOpAssign(string op:"~") (in char ch) { pragma(inline, true); if (s.length) { xstr ~= s; hashCurr = joaatHashPart(xstr.getData); } }
275 void opOpAssign(string op:"~") (in auto ref dynstring s) { pragma(inline, true); if (s.length) { xstr ~= s; hashCurr = joaatHashPart(xstr.getData); } }
276 void opOpAssign(string op:"~") (in auto ref EgraCIString s) { pragma(inline, true); if (s.xstr.length) { xstr ~= s.xstr; hashCurr = joaatHashPart(xstr.getData); } }
278 usize toHash () pure const @safe nothrow @nogc { pragma(inline, true); return joaatHashFinish(hashCurr); }
280 // case-insensitive
281 bool opEquals() (const(char)[] other) pure const @safe nothrow @nogc {
282 pragma(inline, true);
283 if (xstr.length != other.length) return false;
284 return strEquCI(xstr.getData, other);
287 // case-insensitive
288 bool opEquals() (in auto ref EgraCIString other) pure const @safe nothrow @nogc {
289 pragma(inline, true);
290 if (hashCurr != other.hashCurr) return false;
291 if (xstr.length != other.xstr.length) return false;
292 return strEquCI(xstr.getData, other.xstr.getData);
295 // case-insensitive
296 bool opEquals() (in auto ref dynstring other) pure const @safe nothrow @nogc {
297 pragma(inline, true);
298 if (xstr.length != other.length) return false;
299 return strEquCI(xstr.getData, other.getData);
304 // ////////////////////////////////////////////////////////////////////////// //
305 struct EgraSimpleParser {
306 private:
307 const(char)[] text;
308 const(char)[] str; // text left
310 public:
311 this (const(char)[] atext) nothrow @safe @nogc { pragma(inline, true); setText(atext); }
313 int getCurrentLine () pure const nothrow @safe @nogc {
314 int res = 0;
315 foreach (immutable char ch; text[0..$-str.length]) if (ch == '\n') ++res;
316 return res;
319 void error (string msg) const {
320 import std.conv : to;
321 version(none) { import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "===\n%.*s\n===\n", cast(uint)str.length, str.ptr); }
322 throw new Exception("parse error around line "~getCurrentLine.to!string~": "~msg);
325 void setText (const(char)[] atext) nothrow @safe @nogc { pragma(inline, true); text = atext; str = atext; }
327 bool isEOT () {
328 skipBlanks();
329 return (str.length == 0);
332 void skipBlanks () {
333 while (str.length) {
334 if (str[0] <= ' ') { str = str.xstripleft; continue; }
335 if (str.length < 2 || str[0] != '/') break;
336 // single-line comment?
337 if (str[1] == '/') {
338 str = str[2..$];
339 while (str.length && str[0] != '\n') str = str[1..$];
340 continue;
342 // multiline comment?
343 if (str[1] == '*') {
344 bool endFound = false;
345 auto svs = str;
346 str = str[2..$];
347 while (str.length) {
348 if (str.length > 1 && str[0] == '*' && str[1] == '/') {
349 endFound = true;
350 str = str[2..$];
351 break;
353 str = str[1..$];
355 if (!endFound) { str = svs; error("unfinished comment"); }
356 continue;
358 // multiline nested comment?
359 if (str[1] == '+') {
360 bool endFound = false;
361 auto svs = str;
362 int level = 0;
363 while (str.length) {
364 if (str.length > 1) {
365 if (str[0] == '/' && str[1] == '+') { str = str[2..$]; ++level; continue; }
366 if (str[0] == '+' && str[1] == '/') { str = str[2..$]; if (--level == 0) { endFound = true; break;} continue; }
368 str = str[1..$];
370 if (!endFound) { str = svs; error("unfinished comment"); }
371 continue;
373 break;
377 bool checkNoEat (const(char)[] tk) {
378 assert(tk.length);
379 skipBlanks();
380 return (str.length >= tk.length && str[0..tk.length] == tk);
383 bool checkDigitNoEat () {
384 skipBlanks();
385 return (str.length > 0 && isdigit(str[0]));
388 bool checkNoEat (in char ch) {
389 skipBlanks();
390 return (str.length > 0 && str[0] == ch);
393 bool check (const(char)[] tk) {
394 if (!checkNoEat(tk)) return false;
395 str = str[tk.length..$];
396 skipBlanks();
397 return true;
400 bool check (in char ch) {
401 if (!checkNoEat(ch)) return false;
402 str = str[1..$];
403 skipBlanks();
404 return true;
407 void expect (const(char)[] tk) {
408 skipBlanks();
409 auto svs = str;
410 if (!check(tk)) { str = svs; error("`"~tk.idup~"` expected"); }
413 void expect (in char ch) {
414 skipBlanks();
415 auto svs = str;
416 if (!check(ch)) { str = svs; error("`"~ch~"` expected"); }
419 const(char)[] expectId () {
420 skipBlanks();
421 if (str.length == 0) error("identifier expected");
422 if (!isalpha(str[0]) && str[0] != '_' && str[0] != '-') error("identifier expected");
423 usize pos = 1;
424 while (pos < str.length) {
425 if (!isalnum(str[pos]) && str[pos] != '_' && str[pos] != '-') break;
426 ++pos;
428 const(char)[] res = str[0..pos];
429 str = str[pos..$];
430 skipBlanks();
431 return res;
434 const(char)[] expectSelector () {
435 static bool isSelChar (in char ch) pure nothrow @safe @nogc {
436 pragma(inline, true);
437 return (isalnum(ch) || ch == '_' || ch == '-' || ch == '#' || ch == '.' || ch == ':' || ch == '>' || ch == '+');
439 skipBlanks();
440 if (str.length == 0) error("selector expected");
441 if (!isSelChar(str[0])) error("selector expected");
442 usize pos = 1;
443 while (pos < str.length && isSelChar(str[pos])) ++pos;
444 const(char)[] res = str[0..pos];
445 str = str[pos..$];
446 skipBlanks();
447 return res;
450 uint parseColor () {
451 skipBlanks();
452 if (str.length == 0) error("color expected");
454 // html-like color?
455 if (check('#')) return parseHtmlColor();
457 auto svs = str;
458 auto id = expectId();
460 if (id.strEquCI("transparent")) return gxTransparent;
462 // `rgb()` or `rgba()`?
463 bool allowAlpha;
464 if (id.strEquCI("rgba")) allowAlpha = true;
465 else if (id.strEquCI("rgb")) allowAlpha = false;
466 else {
467 auto xc = xFindColorByName(id);
468 if (xc is null) { str = svs; error("invalid color definition"); }
469 return gxrgb(xc.r, xc.g, xc.b);
472 skipBlanks();
473 if (!check('(')) { str = svs; error("invalid color definition"); }
474 immutable uint clr = parseColorRGB(allowAlpha);
475 if (!check(')')) { str = svs; error("invalid color definition"); }
476 return clr;
479 // open quote already eaten
480 dynstring parseString (in char qch) {
481 auto epos = str.indexOf('"');
482 if (epos < 0) error("invalid string");
483 auto svs = str;
484 dynstring res;
485 res.reserve(epos);
486 usize pos = 0;
487 while (pos < str.length) {
488 immutable char ch = str.ptr[pos++];
489 if (ch == 0) { str = svs; error("invalid string"); }
490 if (ch == qch) {
491 str = str[pos..$];
492 skipBlanks();
493 return res;
495 if (ch != '\\') {
496 if (ch == '\n') { str = svs; error("unterminated string"); }
497 res ~= ch;
498 continue;
500 if (pos >= str.length) { str = svs; error("unterminated string"); }
501 switch (str.ptr[pos++]) {
502 case 't': res ~= '\t'; break;
503 case 'n': res ~= '\n'; break;
504 case 'r': res ~= '\r'; break;
505 case '\n': break;
506 case '\r': if (pos < str.length && str.ptr[pos] == '\n') ++pos; break;
507 case '"': case '\'': case '\\': res ~= str.ptr[pos-1]; break;
508 default: str = svs; error("invalid string escape");
511 str = svs;
512 error("unterminated string");
513 assert(0);
516 int parseInt () {
517 skipBlanks();
518 if (str.length == 0) error("number expected");
519 auto svs = str;
520 bool neg = false;
521 if (check('+')) {}
522 else if (check('-')) neg = true;
523 if (str.length == 0 || !isdigit(str[0])) { str = svs; error("number expected"); }
524 int base = 0;
525 // check bases
526 if (str.length > 1 && str[0] == '0') {
527 switch (str[1]) {
528 case 'x': case 'X': base = 16; break;
529 case 'b': case 'B': base = 2; break;
530 case 'o': case 'O': base = 8; break;
531 case 'd': case 'D': base = 10; break;
532 default: break;
534 if (base) {
535 str = str[2..$];
536 while (str.length && str[0] == '_') str = str[1..$];
539 if (!base) base = 10;
540 else if (str.length == 0 || digitInBase(str[0], base) < 0) { str = svs; error("number expected"); }
541 immutable long vmax = (neg ? -cast(long)int.min : cast(long)int.max);
542 long n = 0;
543 while (str.length) {
544 if (str[0] != '_') {
545 immutable int dg = digitInBase(str[0], base);
546 if (dg < 0) break;
547 n = n*base+dg;
548 if (n > vmax) { str = svs; error("integer overflow"); }
550 str = str[1..$];
552 if (str.length && isalpha(str[0])) { str = svs; error("number expected"); }
553 skipBlanks();
554 return cast(int)n;
557 private:
558 // '#' skipped
559 uint parseHtmlColor () {
560 auto svs = str;
561 skipBlanks();
562 ubyte[3] rgb = 0;
563 // first 3 digits
564 foreach (immutable n; 0..3) {
565 while (str.length && str[0] == '_') str = str[1..$];
566 if (str.length == 0) { str = svs; error("invalid color"); }
567 immutable int dg = digitInBase(str[0], 16);
568 if (dg < 0) { str = svs; error("invalid color"); }
569 rgb[n] = cast(ubyte)dg;
570 str = str[1..$];
572 while (str.length && str[0] == '_') str = str[1..$];
573 // second 3 digits?
574 if (str.length && digitInBase(str[0], 16) >= 0) {
575 foreach (immutable n; 0..3) {
576 while (str.length && str[0] == '_') str = str[1..$];
577 if (str.length == 0) { str = svs; error("invalid color"); }
578 immutable int dg = digitInBase(str[0], 16);
579 if (dg < 0) { str = svs; error("invalid color"); }
580 rgb[n] = cast(ubyte)(rgb[n]*16+dg);
581 str = str[1..$];
583 while (str.length && str[0] == '_') str = str[1..$];
584 } else {
585 foreach (immutable n; 0..3) rgb[n] = cast(ubyte)(rgb[n]*16+rgb[n]);
587 skipBlanks();
588 return gxrgb(rgb[0], rgb[1], rgb[2]);
591 // "(" skipped
592 uint parseColorRGB (bool allowAlpha) {
593 auto svs = str;
594 ubyte[4] rgba = 0;
595 foreach (immutable n; 0..3+(allowAlpha ? 1 : 0)) {
596 if (n && !check(',')) { str = svs; error("invalid color"); }
597 skipBlanks();
598 if (str.length == 0 || !isdigit(str[0])) { str = svs; error("invalid color"); }
599 uint val = 0;
600 uint base = 10;
601 if (str[0] == '0' && str.length >= 2 && (str[1] == 'x' || str[1] == 'X')) {
602 str = str[2..$];
603 if (str.length == 0 || digitInBase(str[0], 16) < 0) { str = svs; error("invalid color"); }
604 base = 16;
606 while (str.length) {
607 if (str[0] != '_') {
608 immutable int dg = digitInBase(str[0], cast(int)base);
609 if (dg < 0) break;
610 val = val*base+cast(uint)dg;
611 if (val > 255) { str = svs; error("invalid color"); }
613 str = str[1..$];
615 while (str.length && str[0] == '_') str = str[1..$];
616 rgba[n] = cast(ubyte)val;
618 skipBlanks();
619 if (allowAlpha) return gxrgba(rgba[0], rgba[1], rgba[2], rgba[3]);
620 return gxrgb(rgba[0], rgba[1], rgba[2]);
625 // ////////////////////////////////////////////////////////////////////////// //
627 style store is a simple list of selectors and properties.
628 it also holds cache of path:prop, to avoid slow lookups.
630 style searching is working like this:
632 loop over all styles from the last one, check if the last path
633 element of each style is for us, or for one of our superclasses.
634 if it matches, and distance from us to superclass is lower than
635 the current one, remember this value. if the distance is zero
636 (exact match), stop searching, and use found value.
638 there are some modifiers:
639 :focused -- for focused widgets
640 :disabled -- for disabled widgets
642 if we asked to find something with one of modifiers, and there is
643 only "unmodified" style available, use the unmodified one. technically
644 it is implemented by two searches: with, and without a modifier.
646 special class for subwindows:
647 .minimised
649 class WidgetStyle {
650 public:
651 static struct Value {
652 enum Type {
653 Empty,
654 Str,
655 Int,
656 Color,
658 Type type = Type.Empty;
659 dynstring sval;
660 union {
661 uint color;
662 int ival;
665 this (const(char)[] str) @safe nothrow @nogc { pragma(inline, true); sval = str; type = Type.Str; }
666 this() (in auto ref dynstring str) @safe nothrow @nogc { pragma(inline, true); sval = str; type = Type.Str; }
667 this (in int val) @safe nothrow @nogc { pragma(inline, true); ival = val; type = Type.Int; }
668 this (in uint val) @safe nothrow @nogc { pragma(inline, true); color = val; type = Type.Color; }
669 this() (in auto ref Value v) @safe nothrow @nogc { pragma(inline, true); type = v.type; sval = v.sval; color = v.color; }
671 ~this () nothrow @safe @nogc { pragma(inline, true); sval.clear(); color = 0; type = Type.Empty; }
672 void clear () nothrow @safe @nogc { pragma(inline, true); sval.clear(); color = 0; type = Type.Empty; }
674 @property bool isEmpty () pure const @safe nothrow { pragma(inline, true); return (type == Type.Empty); }
675 @property bool isString () pure const @safe nothrow { pragma(inline, true); return (type == Type.Str); }
676 @property bool isColor () pure const @safe nothrow { pragma(inline, true); return (type == Type.Color); }
677 @property bool isInteger () pure const @safe nothrow { pragma(inline, true); return (type == Type.Int); }
679 static Value Empty () @safe nothrow @nogc { pragma(inline, true); return Value(); }
680 static Value String (const(char)[] str) @safe nothrow @nogc { pragma(inline, true); return Value(str); }
681 static Value String() (in auto ref dynstring str) @safe nothrow @nogc { pragma(inline, true); return Value(str); }
682 static Value Color (in uint clr) @safe nothrow @nogc { pragma(inline, true); return Value(clr); }
683 static Value Integer (in int val) @safe nothrow @nogc { pragma(inline, true); return Value(val); }
685 void opAssign() (in auto ref Value v) { pragma(inline, true); type = v.type; sval = v.sval; color = v.color; }
688 static struct Item {
689 dynstring sel; // selector
690 EgraCIString prop; // property name
691 Value value; // property value
693 this() (in auto ref Item it) @trusted nothrow @nogc {
694 pragma(inline, true);
695 sel = it.sel; prop = it.prop; value = it.value;
696 version(egra_style_dynstr_debug) {
697 import core.stdc.stdio : stderr, fprintf;
698 fprintf(stderr, "Item:0x%08x: constructed from 0x%08x! sel=[%.*s]; prop=[%.*s]\n",
699 cast(uint)cast(void*)&this, cast(uint)cast(void*)&it,
700 cast(uint)sel.length, sel.getData.ptr,
701 cast(uint)prop.length, prop.getData.ptr);
705 version(egra_style_dynstr_debug) {
706 this (this) nothrow @trusted @nogc {
707 import core.stdc.stdio : stderr, fprintf;
708 fprintf(stderr, "Item:0x%08x: copied! sel=[%.*s]; prop=[%.*s]\n", cast(uint)cast(void*)&this,
709 cast(uint)sel.length, sel.getData.ptr,
710 cast(uint)prop.length, prop.getData.ptr);
714 ~this () nothrow @trusted @nogc {
715 pragma(inline, true);
716 version(egra_style_dynstr_debug) {
717 import core.stdc.stdio : stderr, fprintf;
718 fprintf(stderr, "Item:0x%08x: DESTROYING! sel=[%.*s]; prop=[%.*s]\n", cast(uint)cast(void*)&this,
719 cast(uint)sel.length, sel.getData.ptr,
720 cast(uint)prop.length, prop.getData.ptr);
722 sel.clear();
723 prop.clear();
724 value.clear();
727 void clear () nothrow @safe @nogc { pragma(inline, true); sel.clear(); prop.clear(); value.clear(); }
729 void opAssign() (in auto ref Item it) nothrow @trusted @nofc { pragma(inline, true); sel = it.sel; prop = it.prop; value = it.value; }
732 protected:
733 Item[] style;
735 // "path" here is the path to a styled class, not a selector
736 // modifier is appended to the path
737 // prop name is appended last
738 Value[EgraCIString] styleCache;
740 protected:
741 final void clearStyleCache () @trusted nothrow {
742 pragma(inline, true);
743 if (styleCache.length) {
744 foreach (ref kv; styleCache.byKeyValue) { kv.key.clear(); kv.value.clear(); }
745 styleCache.clear();
746 //styleCache = null;
750 final const(Value)* findCachedValue (EgraStyledClass obj, const(char)[] prop) @trusted nothrow {
751 pragma(inline, true);
752 if (obj is null) {
753 return null;
754 } else {
755 EgraCIString pp = obj.getFullPath();
756 auto mod = obj.getCurrentMod();
757 if (mod.length) { pp ~= "\x00"; pp ~= mod; }
758 if (prop.length) { pp ~= "\x00"; pp ~= prop; }
759 return (pp in styleCache);
763 final void cacheValue (in ref Value val, EgraStyledClass obj, const(char)[] prop) @trusted nothrow {
764 pragma(inline, true);
765 if (obj is null) return;
766 EgraCIString pp = obj.getFullPath();
767 auto mod = obj.getCurrentMod();
768 if (mod.length) { pp ~= "\x00"; pp ~= mod; }
769 if (prop.length) { pp ~= "\x00"; pp ~= prop; }
770 styleCache[pp] = Value(val);
773 protected:
774 final void addColorItem() (in auto ref Item ci) @trusted nothrow {
775 if (ci.sel.length && ci.sel.length) {
776 clearStyleCache();
777 version(egra_style_dynstr_debug) {
778 import core.stdc.stdio : stderr, fprintf;
779 fprintf(stderr, "addColorItem:000: style.length=%u; style.capacity=%u; stype.ptr=0x%08x\n",
780 cast(uint)style.length, cast(uint)style.capacity, cast(uint)style.ptr);
781 conwriteln("ADDING: ci.sel:", ci.sel.getData, "; ci.prop:", ci.prop.getData);
783 style ~= Item(ci);
784 version(egra_style_dynstr_debug) {
785 import core.stdc.stdio : stderr, fprintf;
786 fprintf(stderr, "addColorItem:001: style.length=%u; style.capacity=%u; stype.ptr=0x%08x\n",
787 cast(uint)style.length, cast(uint)style.capacity, cast(uint)style.ptr);
788 conwriteln("ADDED(", style.length, "): ci.sel:", style[$-1].sel.getData, "; ci.prop:", style[$-1].prop.getData);
793 public:
794 void parseStyle (const(char)[] str) {
795 auto par = EgraSimpleParser(str);
796 while (!par.isEOT) {
797 if (par.check('!')) {
798 auto cmd = par.expectId();
799 if (cmd.strEquCI("clear-style")) {
800 clear();
801 } else {
802 par.error("invalid command: '"~cmd.idup~"'");
804 par.expect(';');
805 continue;
807 auto sel = par.expectSelector();
808 par.expect("{");
809 while (!par.check("}")) {
810 auto prop = par.expectId();
811 par.expect(":");
812 Item ci;
813 ci.sel = sel;
814 ci.prop = prop;
815 if (par.check('"')) {
816 // string
817 ci.value = Value.String(par.parseString('"'));
818 } else if (par.check('\'')) {
819 // string
820 ci.value = Value.String(par.parseString('\''));
821 } else if (par.checkDigitNoEat() || par.checkNoEat('+') || par.checkNoEat('-')) {
822 // number
823 ci.value = Value.Integer(par.parseInt());
824 } else {
825 // color
826 ci.value = Value.Color(par.parseColor());
828 par.expect(';');
829 addColorItem(ci);
833 version(none) {
834 conwriteln("items: ", style.length);
835 conwriteln("0:ADDED: ci.sel:", style[0].sel.getData, "; ci.prop:", style[0].prop.getData);
836 conwriteln("$-1:ADDED: ci.sel:", style[$-1].sel.getData, "; ci.prop:", style[$-1].prop.getData);
837 foreach (const ref Item it; style; reversed) {
838 conwriteln("*** it.sel:", it.sel.getData, "; it.prop:", it.prop.getData);
843 public:
844 this () {}
846 void cloneFrom (WidgetStyle st) {
847 if (st is null || st is this) return;
848 //Item[] style;
849 clearStyleCache();
850 style.length -= style.length;
851 style.reserve(st.style.length);
852 foreach (const ref Item it; st.style) addColorItem(it);
855 void clear () {
856 clearStyleCache();
857 style.length -= style.length;
860 protected static struct BaseInfo {
861 TypeInfo_Class defaultParent = void;
862 TypeInfo_Class ctsrc = void;
865 protected const(Value)* findValueIntr (EgraStyledClass obj, const(char)[] prop) @trusted nothrow {
866 if (obj is null || style.length == 0 || prop.length == 0) return null;
868 version(egra_style_debug_search) conwriteln("*** SEARCHING:", typeid(obj).name, "; prop:", prop, "****");
869 TypeInfo_Class cioverride = typeid(obj);
870 const(Value)* resval = null;
871 while (cioverride !is null) {
872 const(Value)* resmod = null;
873 const(Value)* resnomod = null;
874 foreach (const ref Item it; style; reversed) {
875 if (it.prop != prop) continue;
876 version(egra_style_debug_search) conwriteln(" OBJ:", typeid(obj).name, "; ci:", classShortName(cioverride), "; prop:", prop, "; it.sel:", it.sel.getData, "; it.prop:", it.prop.getData);
877 bool modhit;
878 if (obj.isMySelector(it.sel, classShortName(cioverride), &modhit, asQuery:false)) {
879 if (modhit) { resmod = &it.value; break; }
880 else if (resnomod is null) resnomod = &it.value;
883 if (resmod !is null) {
884 version(egra_style_debug_search) conwriteln(" FOUND MOD!");
885 resval = resmod;
886 break;
888 if (resnomod !is null) {
889 version(egra_style_debug_search) conwriteln(" FOUND NOMOD!");
890 resval = resnomod;
891 break;
893 cioverride = cioverride.base;
894 if (cioverride is typeid(EgraStyledClass)) {
895 EgraStyledClass tl = obj.getTopLevel();
896 if (tl is null || tl is obj) break;
897 obj = tl;
898 cioverride = typeid(obj);
902 return resval;
905 final const(Value)* findValue (EgraStyledClass obj, const(char)[] prop) @trusted nothrow {
906 if (obj is null || style.length == 0 || prop.length == 0) return null;
908 if (auto fv = findCachedValue(obj, prop)) return (fv.isEmpty ? null : fv);
910 if (auto fv = findValueIntr(obj, prop)) {
911 cacheValue(*fv, obj, prop);
912 return fv;
915 Value val = Value.Empty();
916 cacheValue(val, obj, prop);
917 return null;
920 final uint findColor (EgraStyledClass obj, const(char)[] prop, bool* foundp=null) @trusted nothrow {
921 if (auto val = findValue(obj, prop)) {
922 if (val.isColor) {
923 if (foundp) *foundp = true;
924 return val.color;
928 if (foundp) *foundp = false;
929 return gxUnknown;
932 // returns `null` if not found
933 final dynstring findString (EgraStyledClass obj, const(char)[] prop, bool* foundp=null) @trusted nothrow {
934 if (auto val = findValue(obj, prop)) {
935 if (val.isString) {
936 if (foundp) *foundp = true;
937 return val.sval;
941 if (foundp) *foundp = false;
942 return dynstring();
945 // returns 0 if not found
946 final int findInt (EgraStyledClass obj, const(char)[] prop, in int defval=0, bool* foundp=null) @trusted nothrow {
947 if (auto val = findValue(obj, prop)) {
948 if (val.isInteger) {
949 if (foundp) *foundp = true;
950 return val.ival;
954 if (foundp) *foundp = false;
955 return defval;
958 static:
959 static string classShortName (in TypeInfo_Class ct) pure nothrow @trusted @nogc {
960 pragma(inline, true);
961 if (ct is null) return null;
962 string name = ct.name;
963 auto dpos = name.lastIndexOf('.');
964 return (dpos < 0 ? name : name[dpos+1..$]);
969 abstract class EgraStyledClass {
970 protected:
971 // cached path to this object, w/o property name
972 EgraCIString mCachedPath;
973 WidgetStyle mStyleSheet;
974 // for styles
975 dynstring mId;
976 dynstring mStyleClass;
977 bool mStyleCloned;
979 // call when parent was changed
980 final void invalidatePathCache () nothrow @trusted @nogc { pragma(inline, true); mCachedPath.clear(); }
982 public:
983 bool isMyId (const(char)[] str) nothrow @trusted @nogc { return (str.length == 0 || str.strEquCI(mId.getData)); }
984 bool isMyStyleClass (const(char)[] str) nothrow @trusted @nogc { return (str.length == 0 || str.strEquCI(mStyleClass.getData)); }
985 bool isMyModifier (const(char)[] str) nothrow @trusted @nogc { return (str.length == 0 || str.strEquCI(getCurrentMod)); }
987 EgraStyledClass getParent () nothrow @trusted @nogc { return null; }
988 EgraStyledClass getFirstChild () nothrow @trusted @nogc { return null; }
989 EgraStyledClass getNextSibling () nothrow @trusted @nogc { return null; }
991 EgraStyledClass getTopLevel () nothrow @trusted @nogc {
992 EgraStyledClass w = getParent();
993 if (w is null) return null; // we are the top
994 for (;;) {
995 EgraStyledClass p = w.getParent();
996 if (p is null) return w;
997 w = p;
1001 // empty `str` should return `true`
1002 bool isMyClassName (const(char)[] str) nothrow @trusted @nogc {
1003 return (str.length == 0 || str.strEquCI(classShortName(typeid(this))));
1006 // for styling
1007 EgraCIString getFullPath () nothrow @trusted @nogc {
1008 if (mCachedPath.length == 0) {
1009 mCachedPath.clear(); // just in case
1010 for (EgraStyledClass w = this; w !is null; w = w.getParent()) {
1011 mCachedPath ~= typeid(w).name;
1012 mCachedPath ~= "\x00"; // delimiter
1013 mCachedPath ~= mId;
1014 mCachedPath ~= "\x00"; // delimiter
1015 mCachedPath ~= mStyleClass;
1016 mCachedPath ~= "\x00"; // delimiter
1019 return mCachedPath;
1022 string getCurrentMod () nothrow @trusted @nogc { return ""; }
1024 final WidgetStyle getStyle () nothrow @trusted @nogc {
1025 for (EgraStyledClass w = this; w !is null; w = w.getParent()) {
1026 if (w.mStyleSheet !is null) return w.mStyleSheet;
1028 return defaultColorStyle;
1031 final @property dynstring id () const nothrow @trusted @nogc { pragma(inline, true); return dynstring(mId); }
1032 @property void id (const(char)[] v) nothrow @trusted @nogc {
1033 if (v.length != mId.length || !strEquCI(v, mId.getData)) invalidatePathCache();
1034 if (v != mId.getData) mId = v;
1037 final @property dynstring styleClass () const nothrow @trusted @nogc { pragma(inline, true); return dynstring(mStyleClass); }
1038 @property void styleClass (const(char)[] v) nothrow @trusted @nogc {
1039 if (v.length != mStyleClass.length || !strEquCI(v, mStyleClass.getData)) invalidatePathCache();
1040 if (v != mStyleClass.getData) mStyleClass = v;
1043 public:
1044 void widgetChanged () nothrow {}
1046 void setStyle (WidgetStyle stl) {
1047 if (stl !is mStyleSheet) {
1048 mStyleSheet = stl;
1049 mStyleCloned = false;
1050 widgetChanged();
1054 // this clones the style
1055 void appendStyle (const(char)[] str) {
1056 str = str.xstrip;
1057 if (str.length == 0) return;
1058 if (mStyleSheet is null) {
1059 mStyleSheet = new WidgetStyle;
1060 mStyleSheet.cloneFrom(defaultColorStyle);
1061 mStyleCloned = true;
1062 } else if (!mStyleCloned) {
1063 WidgetStyle ws = new WidgetStyle;
1064 ws.cloneFrom(mStyleSheet);
1065 mStyleSheet = ws;
1066 mStyleCloned = true;
1068 mStyleSheet.parseStyle(str);
1069 widgetChanged();
1072 public:
1073 //this () {}
1074 ~this () nothrow @trusted @nogc { pragma(inline, true); mCachedPath.clear(); }
1076 public:
1077 static template isGoodSelectorDelegate(DG) {
1078 import std.traits;
1079 enum isGoodSelectorDelegate =
1080 (is(ReturnType!DG == void) || is(ReturnType!DG == EgraStyledClass) || is(ReturnType!DG == bool)) &&
1081 is(typeof((inout int=0) { DG dg = void; EgraStyledClass w; dg(w); }));
1084 final EgraStyledClass forEachSelector(DG) (const(char)[] sel, scope DG dg) if (isGoodSelectorDelegate!DG) {
1085 import std.traits;
1086 for (EgraStyledClass w = getFirstChild(); w !is null; w = w.getNextSibling()) {
1087 if (EgraStyledClass res = w.forEachSelector(sel, dg)) return res;
1089 bool modhit;
1090 if (!isMySelector(sel, &modhit)) return null;
1091 if (!modhit) return null;
1092 static if (is(ReturnType!DG == void)) {
1093 dg(this);
1094 return null;
1095 } else static if (is(ReturnType!DG == bool)) {
1096 if (dg(this)) return this;
1097 return null;
1098 } else static if (is(ReturnType!DG == EgraStyledClass)) {
1099 if (EgraStyledClass res = dg(this)) return res;
1100 return null;
1101 } else {
1102 static assert(0, "wtf?!");
1106 final EgraStyledClass querySelector (const(char)[] sel) {
1107 import std.traits;
1108 for (EgraStyledClass w = getFirstChild(); w !is null; w = w.getNextSibling()) {
1109 if (EgraStyledClass res = w.querySelector(sel)) return res;
1111 bool modhit;
1112 if (isMySelector(sel, &modhit)) {
1113 if (modhit) return this;
1115 return null;
1118 static template isGoodIteratorDelegate(DG) {
1119 import std.traits;
1120 enum isGoodIteratorDelegate =
1121 (is(ReturnType!DG == int)) &&
1122 is(typeof((inout int=0) { DG dg = void; EgraStyledClass w; int res = dg(w); }));
1125 static struct Iter {
1126 EgraStyledClass c;
1127 dynstring sel;
1129 this (EgraStyledClass cc, const(char)[] asel) nothrow @safe @nogc {
1130 pragma(inline, true);
1131 asel = asel.xstrip;
1132 if (asel.length) { c = cc; sel = asel; }
1135 int opApply(DG) (scope DG dg) if (isGoodIteratorDelegate!DG) {
1136 int res = 0;
1137 if (c is null || dg is null) return 0;
1138 c.forEachSelector(sel.getData, (EgraStyledClass w) {
1139 res = dg(w);
1140 return (res != 0);
1142 return res;
1146 final Iter querySelectorAll (const(char)[] sel) nothrow @safe @nogc { pragma(inline, true); return Iter(this, sel); }
1148 public:
1149 static string classShortName (in TypeInfo_Class ct) pure nothrow @trusted @nogc {
1150 pragma(inline, true);
1151 if (ct is null) return null;
1152 string name = ct.name;
1153 auto dpos = name.lastIndexOf('.');
1154 return (dpos < 0 ? name : name[dpos+1..$]);
1157 // `from`, or any superclass
1158 static bool isChildOf (in TypeInfo_Class from, const(char)[] cls) pure nothrow @trusted @nogc {
1159 cls = cls.xstrip;
1160 if (cls.length == 0) return false;
1161 // sorry for this cast
1162 for (TypeInfo_Class ti = cast(TypeInfo_Class)from; ti !is null; ti = ti.base) {
1163 if (cls.strEquCI(classShortName(ti))) {
1164 version(none) { import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "%.*s: isGoodMyClass: cls=<%.*s> TRUE\n",
1165 cast(uint)ti.name.length, ti.name.ptr, cast(uint)cls.length, cls.ptr); }
1166 return true;
1168 version(none) { import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "%.*s: isGoodMyClass: cls=<%.*s> FALSE\n",
1169 cast(uint)ti.name.length, ti.name.ptr, cast(uint)cls.length, cls.ptr); }
1171 return false;
1174 static bool isIdChar (in char ch) pure nothrow @trusted @nogc {
1175 pragma(inline, true);
1176 return
1177 (ch >= '0' && ch <= '9') ||
1178 (ch >= 'A' && ch <= 'Z') ||
1179 (ch >= 'a' && ch <= 'z') ||
1180 ch == '_' || ch == '-';
1183 // leading and trailing spaces should be stripped
1184 // also, there should be no spaces inside the string
1185 final bool checkOneSelector (const(char)[] sel, const(char)[] cnoverride, bool* modhit, in bool asQuery) nothrow @trusted @nogc {
1186 if (modhit) *modhit = true;
1187 //sel = sel.xstrip;
1188 version(none) { import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "%.*s: checkOneSelector: s=<%.*s>\n",
1189 cast(uint)typeid(this).name.length, typeid(this).name.ptr, cast(uint)sel.length, sel.ptr); }
1190 if (sel.length == 0) return false;
1191 usize epos = 0;
1192 while (epos < sel.length && isIdChar(sel.ptr[epos])) ++epos;
1193 const(char)[] cls = sel[0..epos];
1194 if (cls.length != 0) {
1195 if (cnoverride.length) {
1196 if (!cnoverride.strEquCI(cls)) return false;
1197 } else {
1198 if (!isMyClassName(cls)) return false;
1201 bool wasMod = false;
1202 sel = sel[epos..$];
1203 // check id and style class
1204 while (sel.length) {
1205 immutable char ch = sel.ptr[0];
1206 if (ch != '.' && ch != '#' && ch != ':') return false;
1207 epos = 1;
1208 while (epos < sel.length && isIdChar(sel.ptr[epos])) ++epos;
1209 const(char)[] nm = sel[1..epos];
1210 sel = sel[epos..$];
1211 final switch (ch) {
1212 case '.': if (!isMyStyleClass(nm)) return false; break;
1213 case '#': if (!isMyId(nm)) return false; break;
1214 case ':': wasMod = true; if (modhit) { if (!isMyModifier(nm)) *modhit = false; } break;
1217 if (modhit !is null && !wasMod && !asQuery && getCurrentMod().length) *modhit = false;
1218 return true;
1221 final bool isMySelector (const(char)[] sel, const(char)[] clnameoverride, bool* modhit=null, in bool asQuery=true) nothrow @trusted @nogc {
1222 if (modhit) *modhit = false;
1223 sel = sel.xstrip;
1224 if (sel.length == 0) return false;
1225 // check object class name
1226 usize epos = sel.length;
1227 while (epos > 0) {
1228 immutable char ch = sel[epos-1];
1229 if (ch <= ' ' || ch == '>') break;
1230 --epos;
1232 if (!checkOneSelector(sel[epos..$], clnameoverride, modhit, asQuery)) {
1233 if (modhit) *modhit = false;
1234 return false;
1236 sel = sel[0..epos].xstripright;
1237 if (sel.length == 0) return true;
1238 immutable bool oneParent = (sel[$-1] == '>');
1239 if (oneParent) {
1240 sel = sel[0..$-1].xstripright;
1241 if (sel.length == 0) return true;
1242 if (sel[$-1] == '>') return false;
1244 version(none) { import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "%.*s: isMySelector: oneParent=%d\n",
1245 cast(uint)typeid(this).name.length, typeid(this).name.ptr, cast(int)oneParent); }
1246 // sorry for this cast
1247 for (EgraStyledClass w = (cast(EgraStyledClass)this).getParent(); w !is null; w = w.getParent()) {
1248 version(none) { import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "%.*s: isMySelector: parent=<%.*s>; oneParent=%d\n",
1249 cast(uint)typeid(this).name.length, typeid(this).name.ptr, cast(uint)typeid(w).name.length, typeid(w).name.ptr, cast(int)oneParent); }
1250 if (w.isMySelector(sel)) return true;
1251 if (oneParent) return false;
1253 return false;
1256 final bool isMySelector (const(char)[] sel, bool* modhit=null) nothrow @trusted @nogc {
1257 pragma(inline, true);
1258 return isMySelector(sel, null, modhit);
1261 public:
1262 // returns gxUnknown if not found
1263 final uint getColor (const(char)[] prop, bool* foundp=null) @trusted nothrow {
1264 pragma(inline, true);
1265 return getStyle().findColor(this, prop, foundp);
1268 // returns empty string if not found
1269 final dynstring getString (const(char)[] prop, bool* foundp=null) @trusted nothrow {
1270 pragma(inline, true);
1271 return getStyle().findString(this, prop, foundp);
1274 final int getInt (const(char)[] prop, in int defval=0, bool* foundp=null) @trusted nothrow {
1275 pragma(inline, true);
1276 return getStyle().findInt(this, prop, defval, foundp);
1281 __gshared WidgetStyle defaultColorStyle;
1283 shared static this () {
1284 defaultColorStyle = new WidgetStyle;
1285 defaultColorStyle.parseStyle(defaultStyleText);