sq3: show SQLite error messages on stderr by default
[iv.d.git] / egra / gui / style.d
blobb5c9c5ea272f50e3f5c8fc9ffc69dfc75629cdde
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;
59 text-cursor-dot: transparent;
61 text-cursor-blink-time: 800; /* in milliseconds */
62 text-cursor-dot-time: 100; /* dot crawl time, in milliseconds */
65 SubWindow.minimised {
66 icon-size-x: 24;
67 icon-size-y: 24;
68 icon-margin-x: 4;
69 icon-margin-y: 4;
72 SubWindow:focused {
73 frame: white;
74 title-back: white;
76 // for widgets
77 text: white;
79 text-cursor-0: grey67;
80 text-cursor-1: white;
81 text-cursor-dot: transparent;
85 YesNoWindow {
86 frame: #ddd;
87 title-back: #ddd;
88 title-text: black;
90 back: rgb(0xbf, 0xcf, 0xef);
91 text: rgb(0x16, 0x2d, 0x59);
92 hotline: rgb(0x16, 0x2d, 0x59);
95 YesNoWindow:focused {
96 frame: rgb(0x40, 0x70, 0xcf);
97 title-back: rgb(0x73, 0x96, 0xdc);
98 title-text: rgb(0xff, 0xff, 0xff);
102 ProgressBarWidget {
103 rect: gray20;
104 back: rgb(17, 17, 0);
105 text: white;
106 stripe: rgb(52, 52, 38);
108 back-hishade: rgb(52, 52, 38);
109 stripe-hishade: rgb(82, 82, 70);
111 back-full: rgb(159, 71, 0);
112 stripe-full: rgb(173, 98, 38);
114 back-full-hishade: rgb(173, 98, 38);
115 stripe-full-hishade: rgb(203, 128, 70);
119 ButtonWidget {
120 back: grey67;
121 text: black;
122 hotline: black;
123 // gradient button
124 grad0-back: grey50;
125 grad1-back: grey77;
126 grad2-back: grey56;
127 grad-midp: 30; /* percents */
130 ButtonWidget:focused {
131 back: grey98;
132 text: #006;
133 hotline: #006;
134 // gradient button
135 grad0-back: grey70;
136 grad1-back: white;
137 grad2-back: grey76;
138 //grad-midp: 20; /* percents */
141 ButtonWidget:disabled {
142 back: grey55;
143 text: grey12;
144 hotline: transparent;
145 // gradient button
146 grad0-back: grey32;
147 grad1-back: grey42;
148 grad2-back: grey36;
149 //grad-midp: 20; /* percents */
153 ButtonExWidget {
154 back: rgb(0x73, 0x96, 0xdc);
155 text: grey94;
156 hotline: grey94;
157 rect: rgb(0x40, 0x70, 0xcf);
158 shadowline: rgb(0x83, 0xa6, 0xec);
161 ButtonExWidget:focused {
162 back: rgb(0x93, 0xb6, 0xfc);
163 text: white;
164 hotline: white;
165 shadowline: rgb(0xa3, 0xc6, 0xff);
169 CheckboxWidget {
170 //text: grey75;
171 back: transparent;
172 mark: #0d0;
175 CheckboxWidget:focused {
176 //text: white;
177 back: #004;
178 mark: #0f0;
181 CheckboxWidget:disabled {
182 text: grey40;
183 back: transparent;
184 mark: grey40;
188 SimpleListBoxWidget {
189 back: #004;
190 text: #ff0;
191 cursor-back: #044;
192 cursor-text: #ddd;
195 SimpleListBoxWidget:focused {
196 cursor-back: #066;
197 cursor-text: white;
201 EditorWidget {
202 back: #007;
204 status-back: gray80;
205 status-text: black;
207 text: rgb(220, 220, 0);
208 quote0-text: rgb(128, 128, 0);
209 quote1-text: rgb(0, 128, 128);
210 wrap-mark-text: rgb(0, 90, 220);
212 attach-file-text: rgb(0x6e, 0x00, 0xff);
213 attach-bad-text: red;
215 // marked block
216 mark-back: rgb(0, 160, 160);
217 mark-text: white;
221 LineEditWidget {
222 back: black;
223 text: rgb(220, 220, 0);
228 // ////////////////////////////////////////////////////////////////////////// //
229 struct EgraCIString {
230 public:
231 static uint joaatHashPart (const(void)[] buf, uint hash=0) pure nothrow @trusted @nogc {
232 pragma(inline, true);
233 foreach (immutable ubyte b; cast(const(ubyte)[])buf) {
234 //hash += (uint8_t)locase1251(*s++);
235 hash += b|0x20; // this converts ASCII capitals to locase (and destroys other, but who cares)
236 hash += hash<<10;
237 hash ^= hash>>6;
239 return hash;
242 static uint joaatHashFinish (uint hash) pure nothrow @trusted @nogc {
243 pragma(inline, true);
244 // final mix
245 hash += hash<<3;
246 hash ^= hash>>11;
247 hash += hash<<15;
248 return hash;
251 // ascii only
252 static bool strEquCI (const(char)[] s0, const(char)[] s1) pure nothrow @trusted @nogc {
253 if (s0.length != s1.length) return false;
254 if (s0.ptr == s1.ptr) return true;
255 foreach (immutable idx, char c0; s0) {
256 // try the easiest case first
257 if (c0 == s1.ptr[idx]) continue;
258 c0 |= 0x20; // convert to ascii lowercase
259 if (c0 < 'a' || c0 > 'z') return false; // it wasn't a letter, no need to check the second char
260 // c0 is guaranteed to be a lowercase ascii here
261 if (c0 != (s1.ptr[idx]|0x20)) return false; // c1 will become a lowercase ascii only if it was uppercase/lowercase ascii
263 return true;
266 private:
267 uint hashCurr; // current hash
268 dynstring xstr;
270 nothrow @trusted @nogc:
271 public:
272 alias getData this;
274 public:
275 this() (in auto ref dynstring s) { pragma(inline, true); xstr = s; hashCurr = joaatHashPart(xstr.getData); }
276 this (const(char)[] s) { pragma(inline, true); xstr = s; hashCurr = joaatHashPart(xstr.getData); }
277 this (in char ch) { pragma(inline, true); xstr = ch; hashCurr = joaatHashPart(xstr.getData); }
279 ~this () { pragma(inline, true); xstr.clear(); hashCurr = 0; }
281 void clear () { pragma(inline, true); xstr.clear(); hashCurr = 0; }
283 @property dynstring str () const { pragma(inline, true); return dynstring(xstr); }
284 @property void str() (in auto ref dynstring s) { pragma(inline, true); xstr = s; hashCurr = joaatHashPart(xstr.getData); }
285 @property void str (const(char)[] s) { pragma(inline, true); xstr = s; hashCurr = joaatHashPart(xstr.getData); }
287 @property uint length () const pure { pragma(inline, true); return xstr.length; }
289 @property const(char)[] getData () const pure { pragma(inline, true); return xstr.getData; }
291 void opAssign() (const(char)[] s) { pragma(inline, true); xstr = s; hashCurr = joaatHashPart(xstr.getData); }
292 void opAssign() (in char ch) { pragma(inline, true); xstr = ch; hashCurr = joaatHashPart(xstr.getData); }
293 void opAssign() (in auto ref dynstring s) { pragma(inline, true); xstr = s.xstr; hashCurr = joaatHashPart(xstr.getData); }
294 void opAssign() (in auto ref EgraCIString s) { pragma(inline, true); xstr = s.xstr; hashCurr = s.hashCurr; }
296 void opOpAssign(string op:"~") (const(char)[] s) { pragma(inline, true); if (s.length) { xstr ~= s; hashCurr = joaatHashPart(xstr.getData); } }
297 void opOpAssign(string op:"~") (in char ch) { pragma(inline, true); if (s.length) { xstr ~= s; hashCurr = joaatHashPart(xstr.getData); } }
298 void opOpAssign(string op:"~") (in auto ref dynstring s) { pragma(inline, true); if (s.length) { xstr ~= s; hashCurr = joaatHashPart(xstr.getData); } }
299 void opOpAssign(string op:"~") (in auto ref EgraCIString s) { pragma(inline, true); if (s.xstr.length) { xstr ~= s.xstr; hashCurr = joaatHashPart(xstr.getData); } }
301 usize toHash () pure const @safe nothrow @nogc { pragma(inline, true); return joaatHashFinish(hashCurr); }
303 // case-insensitive
304 bool opEquals() (const(char)[] other) pure const @safe nothrow @nogc {
305 pragma(inline, true);
306 if (xstr.length != other.length) return false;
307 return strEquCI(xstr.getData, other);
310 // case-insensitive
311 bool opEquals() (in auto ref EgraCIString other) pure const @safe nothrow @nogc {
312 pragma(inline, true);
313 if (hashCurr != other.hashCurr) return false;
314 if (xstr.length != other.xstr.length) return false;
315 return strEquCI(xstr.getData, other.xstr.getData);
318 // case-insensitive
319 bool opEquals() (in auto ref dynstring other) pure const @safe nothrow @nogc {
320 pragma(inline, true);
321 if (xstr.length != other.length) return false;
322 return strEquCI(xstr.getData, other.getData);
327 // ////////////////////////////////////////////////////////////////////////// //
328 struct EgraSimpleParser {
329 private:
330 const(char)[] text;
331 const(char)[] str; // text left
333 public:
334 this (const(char)[] atext) nothrow @safe @nogc { pragma(inline, true); setText(atext); }
336 int getCurrentLine () pure const nothrow @safe @nogc {
337 int res = 0;
338 foreach (immutable char ch; text[0..$-str.length]) if (ch == '\n') ++res;
339 return res;
342 void error (string msg) const {
343 import std.conv : to;
344 version(none) { import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "===\n%.*s\n===\n", cast(uint)str.length, str.ptr); }
345 throw new Exception("parse error around line "~getCurrentLine.to!string~": "~msg);
348 void setText (const(char)[] atext) nothrow @safe @nogc { pragma(inline, true); text = atext; str = atext; }
350 bool isEOT () {
351 skipBlanks();
352 return (str.length == 0);
355 void skipBlanks () {
356 while (str.length) {
357 if (str[0] <= ' ') { str = str.xstripleft; continue; }
358 if (str.length < 2 || str[0] != '/') break;
359 // single-line comment?
360 if (str[1] == '/') {
361 str = str[2..$];
362 while (str.length && str[0] != '\n') str = str[1..$];
363 continue;
365 // multiline comment?
366 if (str[1] == '*') {
367 bool endFound = false;
368 auto svs = str;
369 str = str[2..$];
370 while (str.length) {
371 if (str.length > 1 && str[0] == '*' && str[1] == '/') {
372 endFound = true;
373 str = str[2..$];
374 break;
376 str = str[1..$];
378 if (!endFound) { str = svs; error("unfinished comment"); }
379 continue;
381 // multiline nested comment?
382 if (str[1] == '+') {
383 bool endFound = false;
384 auto svs = str;
385 int level = 0;
386 while (str.length) {
387 if (str.length > 1) {
388 if (str[0] == '/' && str[1] == '+') { str = str[2..$]; ++level; continue; }
389 if (str[0] == '+' && str[1] == '/') { str = str[2..$]; if (--level == 0) { endFound = true; break;} continue; }
391 str = str[1..$];
393 if (!endFound) { str = svs; error("unfinished comment"); }
394 continue;
396 break;
400 bool checkNoEat (const(char)[] tk) {
401 assert(tk.length);
402 skipBlanks();
403 return (str.length >= tk.length && str[0..tk.length] == tk);
406 bool checkDigitNoEat () {
407 skipBlanks();
408 return (str.length > 0 && isdigit(str[0]));
411 bool checkNoEat (in char ch) {
412 skipBlanks();
413 return (str.length > 0 && str[0] == ch);
416 bool check (const(char)[] tk) {
417 if (!checkNoEat(tk)) return false;
418 str = str[tk.length..$];
419 skipBlanks();
420 return true;
423 bool check (in char ch) {
424 if (!checkNoEat(ch)) return false;
425 str = str[1..$];
426 skipBlanks();
427 return true;
430 void expect (const(char)[] tk) {
431 skipBlanks();
432 auto svs = str;
433 if (!check(tk)) { str = svs; error("`"~tk.idup~"` expected"); }
436 void expect (in char ch) {
437 skipBlanks();
438 auto svs = str;
439 if (!check(ch)) { str = svs; error("`"~ch~"` expected"); }
442 const(char)[] expectId () {
443 skipBlanks();
444 if (str.length == 0) error("identifier expected");
445 if (!isalpha(str[0]) && str[0] != '_' && str[0] != '-') error("identifier expected");
446 usize pos = 1;
447 while (pos < str.length) {
448 if (!isalnum(str[pos]) && str[pos] != '_' && str[pos] != '-') break;
449 ++pos;
451 const(char)[] res = str[0..pos];
452 str = str[pos..$];
453 skipBlanks();
454 return res;
457 const(char)[] expectSelector () {
458 static bool isSelChar (in char ch) pure nothrow @safe @nogc {
459 pragma(inline, true);
460 return (isalnum(ch) || ch == '_' || ch == '-' || ch == '#' || ch == '.' || ch == ':' || ch == '>' || ch == '+');
462 skipBlanks();
463 if (str.length == 0) error("selector expected");
464 if (!isSelChar(str[0])) error("selector expected");
465 usize pos = 1;
466 while (pos < str.length && isSelChar(str[pos])) ++pos;
467 const(char)[] res = str[0..pos];
468 str = str[pos..$];
469 skipBlanks();
470 return res;
473 uint parseColor () {
474 skipBlanks();
475 if (str.length == 0) error("color expected");
477 // html-like color?
478 if (check('#')) return parseHtmlColor();
480 auto svs = str;
481 auto id = expectId();
483 if (id.strEquCI("transparent")) return gxTransparent;
485 // `rgb()` or `rgba()`?
486 bool allowAlpha;
487 if (id.strEquCI("rgba")) allowAlpha = true;
488 else if (id.strEquCI("rgb")) allowAlpha = false;
489 else {
490 auto xc = xFindColorByName(id);
491 if (xc is null) { str = svs; error("invalid color definition"); }
492 return gxrgb(xc.r, xc.g, xc.b);
495 skipBlanks();
496 if (!check('(')) { str = svs; error("invalid color definition"); }
497 immutable uint clr = parseColorRGB(allowAlpha);
498 if (!check(')')) { str = svs; error("invalid color definition"); }
499 return clr;
502 // open quote already eaten
503 dynstring parseString (in char qch) {
504 auto epos = str.indexOf('"');
505 if (epos < 0) error("invalid string");
506 auto svs = str;
507 dynstring res;
508 res.reserve(epos);
509 usize pos = 0;
510 while (pos < str.length) {
511 immutable char ch = str.ptr[pos++];
512 if (ch == 0) { str = svs; error("invalid string"); }
513 if (ch == qch) {
514 str = str[pos..$];
515 skipBlanks();
516 return res;
518 if (ch != '\\') {
519 if (ch == '\n') { str = svs; error("unterminated string"); }
520 res ~= ch;
521 continue;
523 if (pos >= str.length) { str = svs; error("unterminated string"); }
524 switch (str.ptr[pos++]) {
525 case 't': res ~= '\t'; break;
526 case 'n': res ~= '\n'; break;
527 case 'r': res ~= '\r'; break;
528 case '\n': break;
529 case '\r': if (pos < str.length && str.ptr[pos] == '\n') ++pos; break;
530 case '"': case '\'': case '\\': res ~= str.ptr[pos-1]; break;
531 default: str = svs; error("invalid string escape");
534 str = svs;
535 error("unterminated string");
536 assert(0);
539 int parseInt () {
540 skipBlanks();
541 if (str.length == 0) error("number expected");
542 auto svs = str;
543 bool neg = false;
544 if (check('+')) {}
545 else if (check('-')) neg = true;
546 if (str.length == 0 || !isdigit(str[0])) { str = svs; error("number expected"); }
547 int base = 0;
548 // check bases
549 if (str.length > 1 && str[0] == '0') {
550 switch (str[1]) {
551 case 'x': case 'X': base = 16; break;
552 case 'b': case 'B': base = 2; break;
553 case 'o': case 'O': base = 8; break;
554 case 'd': case 'D': base = 10; break;
555 default: break;
557 if (base) {
558 str = str[2..$];
559 while (str.length && str[0] == '_') str = str[1..$];
562 if (!base) base = 10;
563 else if (str.length == 0 || digitInBase(str[0], base) < 0) { str = svs; error("number expected"); }
564 immutable long vmax = (neg ? -cast(long)int.min : cast(long)int.max);
565 long n = 0;
566 while (str.length) {
567 if (str[0] != '_') {
568 immutable int dg = digitInBase(str[0], base);
569 if (dg < 0) break;
570 n = n*base+dg;
571 if (n > vmax) { str = svs; error("integer overflow"); }
573 str = str[1..$];
575 if (str.length && isalpha(str[0])) { str = svs; error("number expected"); }
576 skipBlanks();
577 return cast(int)n;
580 private:
581 // '#' skipped
582 uint parseHtmlColor () {
583 auto svs = str;
584 skipBlanks();
585 ubyte[3] rgb = 0;
586 // first 3 digits
587 foreach (immutable n; 0..3) {
588 while (str.length && str[0] == '_') str = str[1..$];
589 if (str.length == 0) { str = svs; error("invalid color"); }
590 immutable int dg = digitInBase(str[0], 16);
591 if (dg < 0) { str = svs; error("invalid color"); }
592 rgb[n] = cast(ubyte)dg;
593 str = str[1..$];
595 while (str.length && str[0] == '_') str = str[1..$];
596 // second 3 digits?
597 if (str.length && digitInBase(str[0], 16) >= 0) {
598 foreach (immutable n; 0..3) {
599 while (str.length && str[0] == '_') str = str[1..$];
600 if (str.length == 0) { str = svs; error("invalid color"); }
601 immutable int dg = digitInBase(str[0], 16);
602 if (dg < 0) { str = svs; error("invalid color"); }
603 rgb[n] = cast(ubyte)(rgb[n]*16+dg);
604 str = str[1..$];
606 while (str.length && str[0] == '_') str = str[1..$];
607 } else {
608 foreach (immutable n; 0..3) rgb[n] = cast(ubyte)(rgb[n]*16+rgb[n]);
610 skipBlanks();
611 return gxrgb(rgb[0], rgb[1], rgb[2]);
614 // "(" skipped
615 uint parseColorRGB (bool allowAlpha) {
616 auto svs = str;
617 ubyte[4] rgba = 0;
618 foreach (immutable n; 0..3+(allowAlpha ? 1 : 0)) {
619 if (n && !check(',')) { str = svs; error("invalid color"); }
620 skipBlanks();
621 if (str.length == 0 || !isdigit(str[0])) { str = svs; error("invalid color"); }
622 uint val = 0;
623 uint base = 10;
624 if (str[0] == '0' && str.length >= 2 && (str[1] == 'x' || str[1] == 'X')) {
625 str = str[2..$];
626 if (str.length == 0 || digitInBase(str[0], 16) < 0) { str = svs; error("invalid color"); }
627 base = 16;
629 while (str.length) {
630 if (str[0] != '_') {
631 immutable int dg = digitInBase(str[0], cast(int)base);
632 if (dg < 0) break;
633 val = val*base+cast(uint)dg;
634 if (val > 255) { str = svs; error("invalid color"); }
636 str = str[1..$];
638 while (str.length && str[0] == '_') str = str[1..$];
639 rgba[n] = cast(ubyte)val;
641 skipBlanks();
642 if (allowAlpha) return gxrgba(rgba[0], rgba[1], rgba[2], rgba[3]);
643 return gxrgb(rgba[0], rgba[1], rgba[2]);
648 // ////////////////////////////////////////////////////////////////////////// //
650 style store is a simple list of selectors and properties.
651 it also holds cache of path:prop, to avoid slow lookups.
653 style searching is working like this:
655 loop over all styles from the last one, check if the last path
656 element of each style is for us, or for one of our superclasses.
657 if it matches, and distance from us to superclass is lower than
658 the current one, remember this value. if the distance is zero
659 (exact match), stop searching, and use found value.
661 there are some modifiers:
662 :focused -- for focused widgets
663 :disabled -- for disabled widgets
665 if we asked to find something with one of modifiers, and there is
666 only "unmodified" style available, use the unmodified one. technically
667 it is implemented by two searches: with, and without a modifier.
669 special class for subwindows:
670 .minimised
672 class WidgetStyle {
673 public:
674 static struct Value {
675 enum Type {
676 Empty,
677 Str,
678 Int,
679 Color,
681 Type type = Type.Empty;
682 dynstring sval;
683 union {
684 uint color;
685 int ival;
688 this (const(char)[] str) @safe nothrow @nogc { pragma(inline, true); sval = str; type = Type.Str; }
689 this() (in auto ref dynstring str) @safe nothrow @nogc { pragma(inline, true); sval = str; type = Type.Str; }
690 this (in int val) @safe nothrow @nogc { pragma(inline, true); ival = val; type = Type.Int; }
691 this (in uint val) @safe nothrow @nogc { pragma(inline, true); color = val; type = Type.Color; }
692 this() (in auto ref Value v) @safe nothrow @nogc { pragma(inline, true); type = v.type; sval = v.sval; color = v.color; }
694 ~this () nothrow @safe @nogc { pragma(inline, true); sval.clear(); color = 0; type = Type.Empty; }
695 void clear () nothrow @safe @nogc { pragma(inline, true); sval.clear(); color = 0; type = Type.Empty; }
697 @property bool isEmpty () pure const @safe nothrow { pragma(inline, true); return (type == Type.Empty); }
698 @property bool isString () pure const @safe nothrow { pragma(inline, true); return (type == Type.Str); }
699 @property bool isColor () pure const @safe nothrow { pragma(inline, true); return (type == Type.Color); }
700 @property bool isInteger () pure const @safe nothrow { pragma(inline, true); return (type == Type.Int); }
702 static Value Empty () @safe nothrow @nogc { pragma(inline, true); return Value(); }
703 static Value String (const(char)[] str) @safe nothrow @nogc { pragma(inline, true); return Value(str); }
704 static Value String() (in auto ref dynstring str) @safe nothrow @nogc { pragma(inline, true); return Value(str); }
705 static Value Color (in uint clr) @safe nothrow @nogc { pragma(inline, true); return Value(clr); }
706 static Value Integer (in int val) @safe nothrow @nogc { pragma(inline, true); return Value(val); }
708 void opAssign() (in auto ref Value v) { pragma(inline, true); type = v.type; sval = v.sval; color = v.color; }
711 static struct Item {
712 dynstring sel; // selector
713 EgraCIString prop; // property name
714 Value value; // property value
716 this() (in auto ref Item it) @trusted nothrow @nogc {
717 pragma(inline, true);
718 sel = it.sel; prop = it.prop; value = it.value;
719 version(egra_style_dynstr_debug) {
720 import core.stdc.stdio : stderr, fprintf;
721 fprintf(stderr, "Item:0x%08x: constructed from 0x%08x! sel=[%.*s]; prop=[%.*s]\n",
722 cast(uint)cast(void*)&this, cast(uint)cast(void*)&it,
723 cast(uint)sel.length, sel.getData.ptr,
724 cast(uint)prop.length, prop.getData.ptr);
728 version(egra_style_dynstr_debug) {
729 this (this) nothrow @trusted @nogc {
730 import core.stdc.stdio : stderr, fprintf;
731 fprintf(stderr, "Item:0x%08x: copied! sel=[%.*s]; prop=[%.*s]\n", cast(uint)cast(void*)&this,
732 cast(uint)sel.length, sel.getData.ptr,
733 cast(uint)prop.length, prop.getData.ptr);
737 ~this () nothrow @trusted @nogc {
738 pragma(inline, true);
739 version(egra_style_dynstr_debug) {
740 import core.stdc.stdio : stderr, fprintf;
741 fprintf(stderr, "Item:0x%08x: DESTROYING! sel=[%.*s]; prop=[%.*s]\n", cast(uint)cast(void*)&this,
742 cast(uint)sel.length, sel.getData.ptr,
743 cast(uint)prop.length, prop.getData.ptr);
745 sel.clear();
746 prop.clear();
747 value.clear();
750 void clear () nothrow @safe @nogc { pragma(inline, true); sel.clear(); prop.clear(); value.clear(); }
752 void opAssign() (in auto ref Item it) nothrow @trusted @nofc { pragma(inline, true); sel = it.sel; prop = it.prop; value = it.value; }
755 protected:
756 Item[] style;
758 // "path" here is the path to a styled class, not a selector
759 // modifier is appended to the path
760 // prop name is appended last
761 Value[EgraCIString] styleCache;
763 protected:
764 final void clearStyleCache () @trusted nothrow {
765 pragma(inline, true);
766 if (styleCache.length) {
767 foreach (ref kv; styleCache.byKeyValue) { kv.key.clear(); kv.value.clear(); }
768 styleCache.clear();
769 //styleCache = null;
773 final const(Value)* findCachedValue (EgraStyledClass obj, const(char)[] prop) @trusted nothrow {
774 pragma(inline, true);
775 if (obj is null) {
776 return null;
777 } else {
778 EgraCIString pp = obj.getFullPath();
779 auto mod = obj.getCurrentMod();
780 if (mod.length) { pp ~= "\x00"; pp ~= mod; }
781 if (prop.length) { pp ~= "\x00"; pp ~= prop; }
782 return (pp in styleCache);
786 final void cacheValue (in ref Value val, EgraStyledClass obj, const(char)[] prop) @trusted nothrow {
787 pragma(inline, true);
788 if (obj is null) return;
789 EgraCIString pp = obj.getFullPath();
790 auto mod = obj.getCurrentMod();
791 if (mod.length) { pp ~= "\x00"; pp ~= mod; }
792 if (prop.length) { pp ~= "\x00"; pp ~= prop; }
793 styleCache[pp] = Value(val);
796 protected:
797 final void addColorItem() (in auto ref Item ci) @trusted nothrow {
798 if (ci.sel.length && ci.sel.length) {
799 clearStyleCache();
800 version(egra_style_dynstr_debug) {
801 import core.stdc.stdio : stderr, fprintf;
802 fprintf(stderr, "addColorItem:000: style.length=%u; style.capacity=%u; stype.ptr=0x%08x\n",
803 cast(uint)style.length, cast(uint)style.capacity, cast(uint)style.ptr);
804 conwriteln("ADDING: ci.sel:", ci.sel.getData, "; ci.prop:", ci.prop.getData);
806 style ~= Item(ci);
807 version(egra_style_dynstr_debug) {
808 import core.stdc.stdio : stderr, fprintf;
809 fprintf(stderr, "addColorItem:001: style.length=%u; style.capacity=%u; stype.ptr=0x%08x\n",
810 cast(uint)style.length, cast(uint)style.capacity, cast(uint)style.ptr);
811 conwriteln("ADDED(", style.length, "): ci.sel:", style[$-1].sel.getData, "; ci.prop:", style[$-1].prop.getData);
816 public:
817 void parseStyle (const(char)[] str) {
818 auto par = EgraSimpleParser(str);
819 while (!par.isEOT) {
820 if (par.check('!')) {
821 auto cmd = par.expectId();
822 if (cmd.strEquCI("clear-style")) {
823 clear();
824 } else {
825 par.error("invalid command: '"~cmd.idup~"'");
827 par.expect(';');
828 continue;
830 auto sel = par.expectSelector();
831 par.expect("{");
832 while (!par.check("}")) {
833 auto prop = par.expectId();
834 par.expect(":");
835 Item ci;
836 ci.sel = sel;
837 ci.prop = prop;
838 if (par.check('"')) {
839 // string
840 ci.value = Value.String(par.parseString('"'));
841 } else if (par.check('\'')) {
842 // string
843 ci.value = Value.String(par.parseString('\''));
844 } else if (par.checkDigitNoEat() || par.checkNoEat('+') || par.checkNoEat('-')) {
845 // number
846 ci.value = Value.Integer(par.parseInt());
847 } else {
848 // color
849 ci.value = Value.Color(par.parseColor());
851 par.expect(';');
852 addColorItem(ci);
856 version(none) {
857 conwriteln("items: ", style.length);
858 conwriteln("0:ADDED: ci.sel:", style[0].sel.getData, "; ci.prop:", style[0].prop.getData);
859 conwriteln("$-1:ADDED: ci.sel:", style[$-1].sel.getData, "; ci.prop:", style[$-1].prop.getData);
860 foreach (const ref Item it; style; reversed) {
861 conwriteln("*** it.sel:", it.sel.getData, "; it.prop:", it.prop.getData);
866 public:
867 this () {}
869 void cloneFrom (WidgetStyle st) {
870 if (st is null || st is this) return;
871 //Item[] style;
872 clearStyleCache();
873 style.length -= style.length;
874 style.reserve(st.style.length);
875 foreach (const ref Item it; st.style) addColorItem(it);
878 void clear () {
879 clearStyleCache();
880 style.length -= style.length;
883 protected static struct BaseInfo {
884 TypeInfo_Class defaultParent = void;
885 TypeInfo_Class ctsrc = void;
888 protected const(Value)* findValueIntr (EgraStyledClass obj, const(char)[] prop) @trusted nothrow {
889 if (obj is null || style.length == 0 || prop.length == 0) return null;
891 version(egra_style_debug_search) conwriteln("*** SEARCHING:", typeid(obj).name, "; mod=", obj.getCurrentMod(), "; prop:", prop, "****");
892 TypeInfo_Class cioverride = typeid(obj);
893 const(Value)* resval = null;
894 while (cioverride !is null) {
895 const(Value)* resmod = null;
896 const(Value)* resnomod = null;
897 foreach (const ref Item it; style; reversed) {
898 if (it.prop != prop) continue;
899 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);
900 bool modhit, modseen;
901 if (obj.isMySelector(it.sel, classShortName(cioverride), &modhit, &modseen, asQuery:false)) {
902 if (modseen) {
903 // last selector had mod
904 if (!modhit || obj.getCurrentMod().length == 0) continue; // object has no mod, cannot apply
905 } else {
906 // last selector had no mod
907 //if (modhit && obj.getCurrentMod().length != 0) modhit = false;
909 debug conwriteln(" FOUND! modseen=", modseen, "; modhit=", modhit, "; objmod=", obj.getCurrentMod, "; sel=", it.sel);
910 if (modhit) { resmod = &it.value; break; }
911 else if (resnomod is null) resnomod = &it.value;
914 if (resmod !is null) {
915 version(egra_style_debug_search) conwriteln(" FOUND MOD!");
916 resval = resmod;
917 break;
919 if (resnomod !is null) {
920 version(egra_style_debug_search) conwriteln(" FOUND NOMOD!");
921 resval = resnomod;
922 break;
924 cioverride = cioverride.base;
925 if (cioverride is typeid(EgraStyledClass)) {
926 EgraStyledClass tl = obj.getTopLevel();
927 if (tl is null || tl is obj) break;
928 obj = tl;
929 cioverride = typeid(obj);
933 return resval;
936 final const(Value)* findValue (EgraStyledClass obj, const(char)[] prop) @trusted nothrow {
937 if (obj is null || style.length == 0 || prop.length == 0) return null;
939 if (auto fv = findCachedValue(obj, prop)) return (fv.isEmpty ? null : fv);
941 if (auto fv = findValueIntr(obj, prop)) {
942 cacheValue(*fv, obj, prop);
943 return fv;
946 Value val = Value.Empty();
947 cacheValue(val, obj, prop);
948 return null;
951 final uint findColor (EgraStyledClass obj, const(char)[] prop, bool* foundp=null) @trusted nothrow {
952 if (auto val = findValue(obj, prop)) {
953 if (val.isColor) {
954 if (foundp) *foundp = true;
955 return val.color;
959 if (foundp) *foundp = false;
960 return gxUnknown;
963 // returns `null` if not found
964 final dynstring findString (EgraStyledClass obj, const(char)[] prop, bool* foundp=null) @trusted nothrow {
965 if (auto val = findValue(obj, prop)) {
966 if (val.isString) {
967 if (foundp) *foundp = true;
968 return val.sval;
972 if (foundp) *foundp = false;
973 return dynstring();
976 // returns 0 if not found
977 final int findInt (EgraStyledClass obj, const(char)[] prop, in int defval=0, bool* foundp=null) @trusted nothrow {
978 if (auto val = findValue(obj, prop)) {
979 if (val.isInteger) {
980 if (foundp) *foundp = true;
981 return val.ival;
985 if (foundp) *foundp = false;
986 return defval;
989 static:
990 static string classShortName (in TypeInfo_Class ct) pure nothrow @trusted @nogc {
991 pragma(inline, true);
992 if (ct is null) return null;
993 string name = ct.name;
994 auto dpos = name.lastIndexOf('.');
995 return (dpos < 0 ? name : name[dpos+1..$]);
1000 abstract class EgraStyledClass {
1001 protected:
1002 // cached path to this object, w/o property name
1003 EgraCIString mCachedPath;
1004 WidgetStyle mStyleSheet;
1005 // for styles
1006 dynstring mId;
1007 dynstring mStyleClass;
1008 bool mStyleCloned;
1010 // call when parent was changed
1011 final void invalidatePathCache () nothrow @trusted @nogc { pragma(inline, true); mCachedPath.clear(); }
1013 public:
1014 bool isMyId (const(char)[] str) nothrow @trusted @nogc { return (str.length == 0 || str.strEquCI(mId.getData)); }
1015 bool isMyStyleClass (const(char)[] str) nothrow @trusted @nogc { return (str.length == 0 || str.strEquCI(mStyleClass.getData)); }
1016 bool isMyModifier (const(char)[] str) nothrow @trusted @nogc { return (str.length == 0 || str.strEquCI(getCurrentMod)); }
1018 EgraStyledClass getParent () nothrow @trusted @nogc { return null; }
1019 EgraStyledClass getFirstChild () nothrow @trusted @nogc { return null; }
1020 EgraStyledClass getNextSibling () nothrow @trusted @nogc { return null; }
1022 EgraStyledClass getTopLevel () nothrow @trusted @nogc {
1023 EgraStyledClass w = getParent();
1024 if (w is null) return null; // we are the top
1025 for (;;) {
1026 EgraStyledClass p = w.getParent();
1027 if (p is null) return w;
1028 w = p;
1032 // empty `str` should return `true`
1033 bool isMyClassName (const(char)[] str) nothrow @trusted @nogc {
1034 return (str.length == 0 || str.strEquCI(classShortName(typeid(this))));
1037 // for styling
1038 EgraCIString getFullPath () nothrow @trusted @nogc {
1039 if (mCachedPath.length == 0) {
1040 mCachedPath.clear(); // just in case
1041 for (EgraStyledClass w = this; w !is null; w = w.getParent()) {
1042 mCachedPath ~= typeid(w).name;
1043 mCachedPath ~= "\x00"; // delimiter
1044 mCachedPath ~= mId;
1045 mCachedPath ~= "\x00"; // delimiter
1046 mCachedPath ~= mStyleClass;
1047 mCachedPath ~= "\x00"; // delimiter
1050 return mCachedPath;
1053 string getCurrentMod () nothrow @trusted @nogc { return ""; }
1055 final WidgetStyle getStyle () nothrow @trusted @nogc {
1056 for (EgraStyledClass w = this; w !is null; w = w.getParent()) {
1057 if (w.mStyleSheet !is null) return w.mStyleSheet;
1059 return defaultColorStyle;
1062 final @property dynstring id () const nothrow @trusted @nogc { pragma(inline, true); return dynstring(mId); }
1063 @property void id (const(char)[] v) nothrow @trusted @nogc {
1064 if (v.length != mId.length || !strEquCI(v, mId.getData)) invalidatePathCache();
1065 if (v != mId.getData) mId = v;
1068 final @property dynstring styleClass () const nothrow @trusted @nogc { pragma(inline, true); return dynstring(mStyleClass); }
1069 @property void styleClass (const(char)[] v) nothrow @trusted @nogc {
1070 if (v.length != mStyleClass.length || !strEquCI(v, mStyleClass.getData)) invalidatePathCache();
1071 if (v != mStyleClass.getData) mStyleClass = v;
1074 public:
1075 void widgetChanged () nothrow {}
1077 void setStyle (WidgetStyle stl) {
1078 if (stl !is mStyleSheet) {
1079 mStyleSheet = stl;
1080 mStyleCloned = false;
1081 widgetChanged();
1085 // this clones the style
1086 void appendStyle (const(char)[] str) {
1087 str = str.xstrip;
1088 if (str.length == 0) return;
1089 if (mStyleSheet is null) {
1090 mStyleSheet = new WidgetStyle;
1091 mStyleSheet.cloneFrom(defaultColorStyle);
1092 mStyleCloned = true;
1093 } else if (!mStyleCloned) {
1094 WidgetStyle ws = new WidgetStyle;
1095 ws.cloneFrom(mStyleSheet);
1096 mStyleSheet = ws;
1097 mStyleCloned = true;
1099 mStyleSheet.parseStyle(str);
1100 widgetChanged();
1103 public:
1104 //this () {}
1105 ~this () nothrow @trusted @nogc { pragma(inline, true); mCachedPath.clear(); }
1107 public:
1108 static template isGoodSelectorDelegate(DG) {
1109 import std.traits;
1110 enum isGoodSelectorDelegate =
1111 (is(ReturnType!DG == void) || is(ReturnType!DG == EgraStyledClass) || is(ReturnType!DG == bool)) &&
1112 is(typeof((inout int=0) { DG dg = void; EgraStyledClass w; dg(w); }));
1115 final EgraStyledClass forEachSelector(DG) (const(char)[] sel, scope DG dg) if (isGoodSelectorDelegate!DG) {
1116 import std.traits;
1117 for (EgraStyledClass w = getFirstChild(); w !is null; w = w.getNextSibling()) {
1118 if (EgraStyledClass res = w.forEachSelector(sel, dg)) return res;
1120 bool modhit;
1121 if (!isMySelector(sel, &modhit)) return null;
1122 if (!modhit) return null;
1123 static if (is(ReturnType!DG == void)) {
1124 dg(this);
1125 return null;
1126 } else static if (is(ReturnType!DG == bool)) {
1127 if (dg(this)) return this;
1128 return null;
1129 } else static if (is(ReturnType!DG == EgraStyledClass)) {
1130 if (EgraStyledClass res = dg(this)) return res;
1131 return null;
1132 } else {
1133 static assert(0, "wtf?!");
1137 final T querySelectorInternal(T:EgraStyledClass) (const(char)[] sel) {
1138 import std.traits;
1139 for (EgraStyledClass w = getFirstChild(); w !is null; w = w.getNextSibling()) {
1140 if (EgraStyledClass res = w.querySelector!T(sel)) return cast(T)res;
1142 bool modhit;
1143 if (isMySelector(sel, &modhit)) {
1144 if (modhit) return cast(T)this;
1146 return null;
1149 final T querySelector(T:EgraStyledClass=EgraStyledClass) (const(char)[] sel) { pragma(inline, true); return querySelectorInternal!T(sel); }
1151 static template isGoodIteratorDelegate(DG, T) {
1152 import std.traits;
1153 enum isGoodIteratorDelegate =
1154 (is(ReturnType!DG == int)) &&
1155 is(typeof((inout int=0) { DG dg = void; /*EgraStyledClass*/T w; int res = dg(w); }));
1158 static struct Iter(T) {
1159 EgraStyledClass c;
1160 dynstring sel;
1162 this (EgraStyledClass cc, const(char)[] asel) nothrow @safe @nogc {
1163 pragma(inline, true);
1164 asel = asel.xstrip;
1165 if (asel.length) { c = cc; sel = asel; }
1168 int opApply(DG) (scope DG dg) if (isGoodIteratorDelegate!(DG, T)) {
1169 int res = 0;
1170 if (c is null || dg is null) return 0;
1171 c.forEachSelector(sel.getData, (EgraStyledClass w) {
1172 if (auto cw = cast(T)w) {
1173 res = dg(cw);
1175 return (res != 0);
1177 return res;
1181 final auto querySelectorAll(T:EgraStyledClass=EgraStyledClass) (const(char)[] sel) nothrow @safe @nogc { pragma(inline, true); return Iter!T(this, sel); }
1183 public:
1184 static string classShortName (in TypeInfo_Class ct) pure nothrow @trusted @nogc {
1185 pragma(inline, true);
1186 if (ct is null) return null;
1187 string name = ct.name;
1188 auto dpos = name.lastIndexOf('.');
1189 return (dpos < 0 ? name : name[dpos+1..$]);
1192 // `from`, or any superclass
1193 static bool isChildOf (in TypeInfo_Class from, const(char)[] cls) pure nothrow @trusted @nogc {
1194 cls = cls.xstrip;
1195 if (cls.length == 0) return false;
1196 // sorry for this cast
1197 for (TypeInfo_Class ti = cast(TypeInfo_Class)from; ti !is null; ti = ti.base) {
1198 if (cls.strEquCI(classShortName(ti))) {
1199 version(none) { import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "%.*s: isGoodMyClass: cls=<%.*s> TRUE\n",
1200 cast(uint)ti.name.length, ti.name.ptr, cast(uint)cls.length, cls.ptr); }
1201 return true;
1203 version(none) { import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "%.*s: isGoodMyClass: cls=<%.*s> FALSE\n",
1204 cast(uint)ti.name.length, ti.name.ptr, cast(uint)cls.length, cls.ptr); }
1206 return false;
1209 static bool isIdChar (in char ch) pure nothrow @trusted @nogc {
1210 pragma(inline, true);
1211 return
1212 (ch >= '0' && ch <= '9') ||
1213 (ch >= 'A' && ch <= 'Z') ||
1214 (ch >= 'a' && ch <= 'z') ||
1215 ch == '_' || ch == '-';
1218 // leading and trailing spaces should be stripped
1219 // also, there should be no spaces inside the string
1220 final bool checkOneSelector (const(char)[] sel, const(char)[] cnoverride, out bool modhit, out bool modseen, in bool asQuery) nothrow @trusted @nogc {
1221 modhit = true;
1222 modseen = false;
1223 //sel = sel.xstrip;
1224 version(none) { import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "%.*s: checkOneSelector: s=<%.*s>\n",
1225 cast(uint)typeid(this).name.length, typeid(this).name.ptr, cast(uint)sel.length, sel.ptr); }
1226 if (sel.length == 0) return false;
1227 usize epos = 0;
1228 while (epos < sel.length && isIdChar(sel.ptr[epos])) ++epos;
1229 const(char)[] cls = sel[0..epos];
1230 if (cls.length != 0) {
1231 if (cnoverride.length) {
1232 if (!cnoverride.strEquCI(cls)) return false;
1233 } else {
1234 if (!isMyClassName(cls)) return false;
1237 sel = sel[epos..$];
1238 // check id and style class
1239 while (sel.length) {
1240 immutable char ch = sel.ptr[0];
1241 if (ch != '.' && ch != '#' && ch != ':') return false;
1242 epos = 1;
1243 while (epos < sel.length && isIdChar(sel.ptr[epos])) ++epos;
1244 const(char)[] nm = sel[1..epos];
1245 sel = sel[epos..$];
1246 final switch (ch) {
1247 case '.': if (!isMyStyleClass(nm)) return false; break;
1248 case '#': if (!isMyId(nm)) return false; break;
1249 case ':': // all modifiers must match
1250 modseen = true;
1251 if (!isMyModifier(nm)) {
1252 debug conwriteln("CHECKONE(", typeid(this).name, "): mod=<", nm, ">: NOT MATCHED! (", getCurrentMod, ")");
1253 modhit = false;
1254 } else {
1255 debug conwriteln("CHECKONE(", typeid(this).name, "): mod=<", nm, ">: MATCHED! (", getCurrentMod, ")");
1257 break;
1260 if (!modseen && !asQuery && getCurrentMod().length) modhit = false;
1261 return true;
1264 final bool isMySelector (const(char)[] sel, const(char)[] clnameoverride,
1265 bool* modhit=null, bool* modseen=null,
1266 in bool asQuery=true) nothrow @trusted @nogc
1268 bool tmpmh, tmpms;
1269 if (modhit) *modhit = false;
1270 if (modseen) *modseen = false;
1271 sel = sel.xstrip;
1272 if (sel.length == 0) return false;
1273 // check object class name
1274 usize epos = sel.length;
1275 while (epos > 0) {
1276 immutable char ch = sel[epos-1];
1277 if (ch <= ' ' || ch == '>') break;
1278 --epos;
1280 if (!checkOneSelector(sel[epos..$], clnameoverride, tmpmh, tmpms, asQuery)) {
1281 return false;
1283 if (modhit) *modhit = tmpmh;
1284 if (modseen) *modseen = tmpms;
1285 sel = sel[0..epos].xstripright;
1286 if (sel.length == 0) return true;
1287 immutable bool oneParent = (sel[$-1] == '>');
1288 if (oneParent) {
1289 sel = sel[0..$-1].xstripright;
1290 if (sel.length == 0) return true;
1291 if (sel[$-1] == '>') return false;
1293 version(none) { import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "%.*s: isMySelector: oneParent=%d\n",
1294 cast(uint)typeid(this).name.length, typeid(this).name.ptr, cast(int)oneParent); }
1295 // sorry for this cast
1296 for (EgraStyledClass w = (cast(EgraStyledClass)this).getParent(); w !is null; w = w.getParent()) {
1297 version(none) { import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "%.*s: isMySelector: parent=<%.*s>; oneParent=%d\n",
1298 cast(uint)typeid(this).name.length, typeid(this).name.ptr, cast(uint)typeid(w).name.length, typeid(w).name.ptr, cast(int)oneParent); }
1299 if (w.isMySelector(sel, null, null, null, asQuery:asQuery)) return true;
1300 if (oneParent) return false;
1302 return false;
1305 final bool isMySelector (const(char)[] sel, bool* modhit=null) nothrow @trusted @nogc {
1306 pragma(inline, true);
1307 return isMySelector(sel, null, modhit);
1310 public:
1311 // returns gxUnknown if not found
1312 final uint getColor (const(char)[] prop, bool* foundp=null) @trusted nothrow {
1313 pragma(inline, true);
1314 return getStyle().findColor(this, prop, foundp);
1317 // returns empty string if not found
1318 final dynstring getString (const(char)[] prop, bool* foundp=null) @trusted nothrow {
1319 pragma(inline, true);
1320 return getStyle().findString(this, prop, foundp);
1323 final int getInt (const(char)[] prop, in int defval=0, bool* foundp=null) @trusted nothrow {
1324 pragma(inline, true);
1325 return getStyle().findInt(this, prop, defval, foundp);
1330 __gshared WidgetStyle defaultColorStyle;
1332 shared static this () {
1333 defaultColorStyle = new WidgetStyle;
1334 defaultColorStyle.parseStyle(defaultStyleText);