erga: use `widgetChanged()` instead of direct screen rebuild posting; fixes to editor...
[iv.d.git] / egra / gui / style.d
blob68b2b7d0a7501cd8f08e162cfaaf5b0d30fc137d
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;
125 ButtonWidget:focused {
126 back: grey98;
127 text: #006;
128 hotline: #006;
131 ButtonWidget:disabled {
132 back: grey55;
133 text: grey12;
134 hotline: transparent;
138 ButtonExWidget {
139 back: rgb(0x73, 0x96, 0xdc);
140 text: grey94;
141 hotline: grey94;
142 rect: rgb(0x40, 0x70, 0xcf);
143 shadowline: rgb(0x83, 0xa6, 0xec);
146 ButtonExWidget:focused {
147 back: rgb(0x93, 0xb6, 0xfc);
148 text: white;
149 hotline: white;
150 shadowline: rgb(0xa3, 0xc6, 0xff);
154 CheckboxWidget {
155 //text: grey75;
156 back: transparent;
157 mark: #0d0;
160 CheckboxWidget:focused {
161 //text: white;
162 back: #004;
163 mark: #0f0;
166 CheckboxWidget:disabled {
167 text: grey40;
168 back: transparent;
169 mark: grey40;
173 SimpleListBoxWidget {
174 back: #004;
175 text: #ff0;
176 cursor-back: #066;
177 cursor-text: white;
180 SimpleListBoxWidget:focused {
181 cursor-back: #044;
182 cursor-text: #ddd;
186 EditorWidget {
187 back: #007;
189 status-back: white;
190 status-color: black;
192 text: rgb(220, 220, 0);
193 quote0-text: rgb(128, 128, 0);
194 quote1-text: rgb(0, 128, 128);
195 wrap-mark-text: rgb(0, 90, 220);
197 attach-file-text: rgb(0x6e, 0x00, 0xff);
198 attach-bad-text: red;
200 // marked block
201 mark-back: rgb(0, 160, 160);
202 mark-text: white;
206 LineEditWidget {
207 back: black;
208 text: rgb(220, 220, 0);
213 // ////////////////////////////////////////////////////////////////////////// //
214 struct EgraCIString {
215 public:
216 static uint joaatHashPart (const(void)[] buf, uint hash=0) pure nothrow @trusted @nogc {
217 pragma(inline, true);
218 foreach (immutable ubyte b; cast(const(ubyte)[])buf) {
219 //hash += (uint8_t)locase1251(*s++);
220 hash += b|0x20; // this converts ASCII capitals to locase (and destroys other, but who cares)
221 hash += hash<<10;
222 hash ^= hash>>6;
224 return hash;
227 static uint joaatHashFinish (uint hash) pure nothrow @trusted @nogc {
228 pragma(inline, true);
229 // final mix
230 hash += hash<<3;
231 hash ^= hash>>11;
232 hash += hash<<15;
233 return hash;
236 // ascii only
237 static bool strEquCI (const(char)[] s0, const(char)[] s1) pure nothrow @trusted @nogc {
238 if (s0.length != s1.length) return false;
239 if (s0.ptr == s1.ptr) return true;
240 foreach (immutable idx, char c0; s0) {
241 // try the easiest case first
242 if (c0 == s1.ptr[idx]) continue;
243 c0 |= 0x20; // convert to ascii lowercase
244 if (c0 < 'a' || c0 > 'z') return false; // it wasn't a letter, no need to check the second char
245 // c0 is guaranteed to be a lowercase ascii here
246 if (c0 != (s1.ptr[idx]|0x20)) return false; // c1 will become a lowercase ascii only if it was uppercase/lowercase ascii
248 return true;
251 private:
252 uint hashCurr; // current hash
253 dynstring xstr;
255 nothrow @trusted @nogc:
256 public:
257 alias getData this;
259 public:
260 this() (in auto ref dynstring s) { pragma(inline, true); xstr = s; hashCurr = joaatHashPart(xstr.getData); }
261 this (const(char)[] s) { pragma(inline, true); xstr = s; hashCurr = joaatHashPart(xstr.getData); }
262 this (in char ch) { pragma(inline, true); xstr = ch; hashCurr = joaatHashPart(xstr.getData); }
264 ~this () { pragma(inline, true); xstr.clear(); hashCurr = 0; }
266 void clear () { pragma(inline, true); xstr.clear(); hashCurr = 0; }
268 @property dynstring str () const { pragma(inline, true); return dynstring(xstr); }
269 @property void str() (in auto ref dynstring s) { pragma(inline, true); xstr = s; hashCurr = joaatHashPart(xstr.getData); }
270 @property void str (const(char)[] s) { pragma(inline, true); xstr = s; hashCurr = joaatHashPart(xstr.getData); }
272 @property uint length () const pure { pragma(inline, true); return xstr.length; }
274 @property const(char)[] getData () const pure { pragma(inline, true); return xstr.getData; }
276 void opAssign() (const(char)[] s) { pragma(inline, true); xstr = s; hashCurr = joaatHashPart(xstr.getData); }
277 void opAssign() (in char ch) { pragma(inline, true); xstr = ch; hashCurr = joaatHashPart(xstr.getData); }
278 void opAssign() (in auto ref dynstring s) { pragma(inline, true); xstr = s.xstr; hashCurr = joaatHashPart(xstr.getData); }
279 void opAssign() (in auto ref EgraCIString s) { pragma(inline, true); xstr = s.xstr; hashCurr = s.hashCurr; }
281 void opOpAssign(string op:"~") (const(char)[] s) { pragma(inline, true); if (s.length) { xstr ~= s; hashCurr = joaatHashPart(xstr.getData); } }
282 void opOpAssign(string op:"~") (in char ch) { pragma(inline, true); if (s.length) { xstr ~= s; hashCurr = joaatHashPart(xstr.getData); } }
283 void opOpAssign(string op:"~") (in auto ref dynstring s) { pragma(inline, true); if (s.length) { xstr ~= s; hashCurr = joaatHashPart(xstr.getData); } }
284 void opOpAssign(string op:"~") (in auto ref EgraCIString s) { pragma(inline, true); if (s.xstr.length) { xstr ~= s.xstr; hashCurr = joaatHashPart(xstr.getData); } }
286 usize toHash () pure const @safe nothrow @nogc { pragma(inline, true); return joaatHashFinish(hashCurr); }
288 // case-insensitive
289 bool opEquals() (const(char)[] other) pure const @safe nothrow @nogc {
290 pragma(inline, true);
291 if (xstr.length != other.length) return false;
292 return strEquCI(xstr.getData, other);
295 // case-insensitive
296 bool opEquals() (in auto ref EgraCIString other) pure const @safe nothrow @nogc {
297 pragma(inline, true);
298 if (hashCurr != other.hashCurr) return false;
299 if (xstr.length != other.xstr.length) return false;
300 return strEquCI(xstr.getData, other.xstr.getData);
303 // case-insensitive
304 bool opEquals() (in auto ref dynstring other) pure const @safe nothrow @nogc {
305 pragma(inline, true);
306 if (xstr.length != other.length) return false;
307 return strEquCI(xstr.getData, other.getData);
312 // ////////////////////////////////////////////////////////////////////////// //
313 struct EgraSimpleParser {
314 private:
315 const(char)[] text;
316 const(char)[] str; // text left
318 public:
319 this (const(char)[] atext) nothrow @safe @nogc { pragma(inline, true); setText(atext); }
321 int getCurrentLine () pure const nothrow @safe @nogc {
322 int res = 0;
323 foreach (immutable char ch; text[0..$-str.length]) if (ch == '\n') ++res;
324 return res;
327 void error (string msg) const {
328 import std.conv : to;
329 version(none) { import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "===\n%.*s\n===\n", cast(uint)str.length, str.ptr); }
330 throw new Exception("parse error around line "~getCurrentLine.to!string~": "~msg);
333 void setText (const(char)[] atext) nothrow @safe @nogc { pragma(inline, true); text = atext; str = atext; }
335 bool isEOT () {
336 skipBlanks();
337 return (str.length == 0);
340 void skipBlanks () {
341 while (str.length) {
342 if (str[0] <= ' ') { str = str.xstripleft; continue; }
343 if (str.length < 2 || str[0] != '/') break;
344 // single-line comment?
345 if (str[1] == '/') {
346 str = str[2..$];
347 while (str.length && str[0] != '\n') str = str[1..$];
348 continue;
350 // multiline comment?
351 if (str[1] == '*') {
352 bool endFound = false;
353 auto svs = str;
354 str = str[2..$];
355 while (str.length) {
356 if (str.length > 1 && str[0] == '*' && str[1] == '/') {
357 endFound = true;
358 str = str[2..$];
359 break;
361 str = str[1..$];
363 if (!endFound) { str = svs; error("unfinished comment"); }
364 continue;
366 // multiline nested comment?
367 if (str[1] == '+') {
368 bool endFound = false;
369 auto svs = str;
370 int level = 0;
371 while (str.length) {
372 if (str.length > 1) {
373 if (str[0] == '/' && str[1] == '+') { str = str[2..$]; ++level; continue; }
374 if (str[0] == '+' && str[1] == '/') { str = str[2..$]; if (--level == 0) { endFound = true; break;} continue; }
376 str = str[1..$];
378 if (!endFound) { str = svs; error("unfinished comment"); }
379 continue;
381 break;
385 bool checkNoEat (const(char)[] tk) {
386 assert(tk.length);
387 skipBlanks();
388 return (str.length >= tk.length && str[0..tk.length] == tk);
391 bool checkDigitNoEat () {
392 skipBlanks();
393 return (str.length > 0 && isdigit(str[0]));
396 bool checkNoEat (in char ch) {
397 skipBlanks();
398 return (str.length > 0 && str[0] == ch);
401 bool check (const(char)[] tk) {
402 if (!checkNoEat(tk)) return false;
403 str = str[tk.length..$];
404 skipBlanks();
405 return true;
408 bool check (in char ch) {
409 if (!checkNoEat(ch)) return false;
410 str = str[1..$];
411 skipBlanks();
412 return true;
415 void expect (const(char)[] tk) {
416 skipBlanks();
417 auto svs = str;
418 if (!check(tk)) { str = svs; error("`"~tk.idup~"` expected"); }
421 void expect (in char ch) {
422 skipBlanks();
423 auto svs = str;
424 if (!check(ch)) { str = svs; error("`"~ch~"` expected"); }
427 const(char)[] expectId () {
428 skipBlanks();
429 if (str.length == 0) error("identifier expected");
430 if (!isalpha(str[0]) && str[0] != '_' && str[0] != '-') error("identifier expected");
431 usize pos = 1;
432 while (pos < str.length) {
433 if (!isalnum(str[pos]) && str[pos] != '_' && str[pos] != '-') break;
434 ++pos;
436 const(char)[] res = str[0..pos];
437 str = str[pos..$];
438 skipBlanks();
439 return res;
442 const(char)[] expectSelector () {
443 static bool isSelChar (in char ch) pure nothrow @safe @nogc {
444 pragma(inline, true);
445 return (isalnum(ch) || ch == '_' || ch == '-' || ch == '#' || ch == '.' || ch == ':' || ch == '>' || ch == '+');
447 skipBlanks();
448 if (str.length == 0) error("selector expected");
449 if (!isSelChar(str[0])) error("selector expected");
450 usize pos = 1;
451 while (pos < str.length && isSelChar(str[pos])) ++pos;
452 const(char)[] res = str[0..pos];
453 str = str[pos..$];
454 skipBlanks();
455 return res;
458 uint parseColor () {
459 skipBlanks();
460 if (str.length == 0) error("color expected");
462 // html-like color?
463 if (check('#')) return parseHtmlColor();
465 auto svs = str;
466 auto id = expectId();
468 if (id.strEquCI("transparent")) return gxTransparent;
470 // `rgb()` or `rgba()`?
471 bool allowAlpha;
472 if (id.strEquCI("rgba")) allowAlpha = true;
473 else if (id.strEquCI("rgb")) allowAlpha = false;
474 else {
475 auto xc = xFindColorByName(id);
476 if (xc is null) { str = svs; error("invalid color definition"); }
477 return gxrgb(xc.r, xc.g, xc.b);
480 skipBlanks();
481 if (!check('(')) { str = svs; error("invalid color definition"); }
482 immutable uint clr = parseColorRGB(allowAlpha);
483 if (!check(')')) { str = svs; error("invalid color definition"); }
484 return clr;
487 // open quote already eaten
488 dynstring parseString (in char qch) {
489 auto epos = str.indexOf('"');
490 if (epos < 0) error("invalid string");
491 auto svs = str;
492 dynstring res;
493 res.reserve(epos);
494 usize pos = 0;
495 while (pos < str.length) {
496 immutable char ch = str.ptr[pos++];
497 if (ch == 0) { str = svs; error("invalid string"); }
498 if (ch == qch) {
499 str = str[pos..$];
500 skipBlanks();
501 return res;
503 if (ch != '\\') {
504 if (ch == '\n') { str = svs; error("unterminated string"); }
505 res ~= ch;
506 continue;
508 if (pos >= str.length) { str = svs; error("unterminated string"); }
509 switch (str.ptr[pos++]) {
510 case 't': res ~= '\t'; break;
511 case 'n': res ~= '\n'; break;
512 case 'r': res ~= '\r'; break;
513 case '\n': break;
514 case '\r': if (pos < str.length && str.ptr[pos] == '\n') ++pos; break;
515 case '"': case '\'': case '\\': res ~= str.ptr[pos-1]; break;
516 default: str = svs; error("invalid string escape");
519 str = svs;
520 error("unterminated string");
521 assert(0);
524 int parseInt () {
525 skipBlanks();
526 if (str.length == 0) error("number expected");
527 auto svs = str;
528 bool neg = false;
529 if (check('+')) {}
530 else if (check('-')) neg = true;
531 if (str.length == 0 || !isdigit(str[0])) { str = svs; error("number expected"); }
532 int base = 0;
533 // check bases
534 if (str.length > 1 && str[0] == '0') {
535 switch (str[1]) {
536 case 'x': case 'X': base = 16; break;
537 case 'b': case 'B': base = 2; break;
538 case 'o': case 'O': base = 8; break;
539 case 'd': case 'D': base = 10; break;
540 default: break;
542 if (base) {
543 str = str[2..$];
544 while (str.length && str[0] == '_') str = str[1..$];
547 if (!base) base = 10;
548 else if (str.length == 0 || digitInBase(str[0], base) < 0) { str = svs; error("number expected"); }
549 immutable long vmax = (neg ? -cast(long)int.min : cast(long)int.max);
550 long n = 0;
551 while (str.length) {
552 if (str[0] != '_') {
553 immutable int dg = digitInBase(str[0], base);
554 if (dg < 0) break;
555 n = n*base+dg;
556 if (n > vmax) { str = svs; error("integer overflow"); }
558 str = str[1..$];
560 if (str.length && isalpha(str[0])) { str = svs; error("number expected"); }
561 skipBlanks();
562 return cast(int)n;
565 private:
566 // '#' skipped
567 uint parseHtmlColor () {
568 auto svs = str;
569 skipBlanks();
570 ubyte[3] rgb = 0;
571 // first 3 digits
572 foreach (immutable n; 0..3) {
573 while (str.length && str[0] == '_') str = str[1..$];
574 if (str.length == 0) { str = svs; error("invalid color"); }
575 immutable int dg = digitInBase(str[0], 16);
576 if (dg < 0) { str = svs; error("invalid color"); }
577 rgb[n] = cast(ubyte)dg;
578 str = str[1..$];
580 while (str.length && str[0] == '_') str = str[1..$];
581 // second 3 digits?
582 if (str.length && digitInBase(str[0], 16) >= 0) {
583 foreach (immutable n; 0..3) {
584 while (str.length && str[0] == '_') str = str[1..$];
585 if (str.length == 0) { str = svs; error("invalid color"); }
586 immutable int dg = digitInBase(str[0], 16);
587 if (dg < 0) { str = svs; error("invalid color"); }
588 rgb[n] = cast(ubyte)(rgb[n]*16+dg);
589 str = str[1..$];
591 while (str.length && str[0] == '_') str = str[1..$];
592 } else {
593 foreach (immutable n; 0..3) rgb[n] = cast(ubyte)(rgb[n]*16+rgb[n]);
595 skipBlanks();
596 return gxrgb(rgb[0], rgb[1], rgb[2]);
599 // "(" skipped
600 uint parseColorRGB (bool allowAlpha) {
601 auto svs = str;
602 ubyte[4] rgba = 0;
603 foreach (immutable n; 0..3+(allowAlpha ? 1 : 0)) {
604 if (n && !check(',')) { str = svs; error("invalid color"); }
605 skipBlanks();
606 if (str.length == 0 || !isdigit(str[0])) { str = svs; error("invalid color"); }
607 uint val = 0;
608 uint base = 10;
609 if (str[0] == '0' && str.length >= 2 && (str[1] == 'x' || str[1] == 'X')) {
610 str = str[2..$];
611 if (str.length == 0 || digitInBase(str[0], 16) < 0) { str = svs; error("invalid color"); }
612 base = 16;
614 while (str.length) {
615 if (str[0] != '_') {
616 immutable int dg = digitInBase(str[0], cast(int)base);
617 if (dg < 0) break;
618 val = val*base+cast(uint)dg;
619 if (val > 255) { str = svs; error("invalid color"); }
621 str = str[1..$];
623 while (str.length && str[0] == '_') str = str[1..$];
624 rgba[n] = cast(ubyte)val;
626 skipBlanks();
627 if (allowAlpha) return gxrgba(rgba[0], rgba[1], rgba[2], rgba[3]);
628 return gxrgb(rgba[0], rgba[1], rgba[2]);
633 // ////////////////////////////////////////////////////////////////////////// //
635 style store is a simple list of selectors and properties.
636 it also holds cache of path:prop, to avoid slow lookups.
638 style searching is working like this:
640 loop over all styles from the last one, check if the last path
641 element of each style is for us, or for one of our superclasses.
642 if it matches, and distance from us to superclass is lower than
643 the current one, remember this value. if the distance is zero
644 (exact match), stop searching, and use found value.
646 there are some modifiers:
647 :focused -- for focused widgets
648 :disabled -- for disabled widgets
650 if we asked to find something with one of modifiers, and there is
651 only "unmodified" style available, use the unmodified one. technically
652 it is implemented by two searches: with, and without a modifier.
654 special class for subwindows:
655 .minimised
657 class WidgetStyle {
658 public:
659 static struct Value {
660 enum Type {
661 Empty,
662 Str,
663 Int,
664 Color,
666 Type type = Type.Empty;
667 dynstring sval;
668 union {
669 uint color;
670 int ival;
673 this (const(char)[] str) @safe nothrow @nogc { pragma(inline, true); sval = str; type = Type.Str; }
674 this() (in auto ref dynstring str) @safe nothrow @nogc { pragma(inline, true); sval = str; type = Type.Str; }
675 this (in int val) @safe nothrow @nogc { pragma(inline, true); ival = val; type = Type.Int; }
676 this (in uint val) @safe nothrow @nogc { pragma(inline, true); color = val; type = Type.Color; }
677 this() (in auto ref Value v) @safe nothrow @nogc { pragma(inline, true); type = v.type; sval = v.sval; color = v.color; }
679 ~this () nothrow @safe @nogc { pragma(inline, true); sval.clear(); color = 0; type = Type.Empty; }
680 void clear () nothrow @safe @nogc { pragma(inline, true); sval.clear(); color = 0; type = Type.Empty; }
682 @property bool isEmpty () pure const @safe nothrow { pragma(inline, true); return (type == Type.Empty); }
683 @property bool isString () pure const @safe nothrow { pragma(inline, true); return (type == Type.Str); }
684 @property bool isColor () pure const @safe nothrow { pragma(inline, true); return (type == Type.Color); }
685 @property bool isInteger () pure const @safe nothrow { pragma(inline, true); return (type == Type.Int); }
687 static Value Empty () @safe nothrow @nogc { pragma(inline, true); return Value(); }
688 static Value String (const(char)[] str) @safe nothrow @nogc { pragma(inline, true); return Value(str); }
689 static Value String() (in auto ref dynstring str) @safe nothrow @nogc { pragma(inline, true); return Value(str); }
690 static Value Color (in uint clr) @safe nothrow @nogc { pragma(inline, true); return Value(clr); }
691 static Value Integer (in int val) @safe nothrow @nogc { pragma(inline, true); return Value(val); }
693 void opAssign() (in auto ref Value v) { pragma(inline, true); type = v.type; sval = v.sval; color = v.color; }
696 static struct Item {
697 dynstring sel; // selector
698 EgraCIString prop; // property name
699 Value value; // property value
701 this() (in auto ref Item it) @trusted nothrow @nogc {
702 pragma(inline, true);
703 sel = it.sel; prop = it.prop; value = it.value;
704 version(egra_style_dynstr_debug) {
705 import core.stdc.stdio : stderr, fprintf;
706 fprintf(stderr, "Item:0x%08x: constructed from 0x%08x! sel=[%.*s]; prop=[%.*s]\n",
707 cast(uint)cast(void*)&this, cast(uint)cast(void*)&it,
708 cast(uint)sel.length, sel.getData.ptr,
709 cast(uint)prop.length, prop.getData.ptr);
713 version(egra_style_dynstr_debug) {
714 this (this) nothrow @trusted @nogc {
715 import core.stdc.stdio : stderr, fprintf;
716 fprintf(stderr, "Item:0x%08x: copied! sel=[%.*s]; prop=[%.*s]\n", cast(uint)cast(void*)&this,
717 cast(uint)sel.length, sel.getData.ptr,
718 cast(uint)prop.length, prop.getData.ptr);
722 ~this () nothrow @trusted @nogc {
723 pragma(inline, true);
724 version(egra_style_dynstr_debug) {
725 import core.stdc.stdio : stderr, fprintf;
726 fprintf(stderr, "Item:0x%08x: DESTROYING! sel=[%.*s]; prop=[%.*s]\n", cast(uint)cast(void*)&this,
727 cast(uint)sel.length, sel.getData.ptr,
728 cast(uint)prop.length, prop.getData.ptr);
730 sel.clear();
731 prop.clear();
732 value.clear();
735 void clear () nothrow @safe @nogc { pragma(inline, true); sel.clear(); prop.clear(); value.clear(); }
737 void opAssign() (in auto ref Item it) nothrow @trusted @nofc { pragma(inline, true); sel = it.sel; prop = it.prop; value = it.value; }
740 protected:
741 Item[] style;
743 // "path" here is the path to a styled class, not a selector
744 // modifier is appended to the path
745 // prop name is appended last
746 Value[EgraCIString] styleCache;
748 protected:
749 final void clearStyleCache () @trusted nothrow {
750 pragma(inline, true);
751 if (styleCache.length) {
752 foreach (ref kv; styleCache.byKeyValue) { kv.key.clear(); kv.value.clear(); }
753 styleCache.clear();
754 //styleCache = null;
758 final const(Value)* findCachedValue (EgraStyledClass obj, const(char)[] prop) @trusted nothrow {
759 pragma(inline, true);
760 if (obj is null) {
761 return null;
762 } else {
763 EgraCIString pp = obj.getFullPath();
764 auto mod = obj.getCurrentMod();
765 if (mod.length) { pp ~= "\x00"; pp ~= mod; }
766 if (prop.length) { pp ~= "\x00"; pp ~= prop; }
767 return (pp in styleCache);
771 final void cacheValue (in ref Value val, EgraStyledClass obj, const(char)[] prop) @trusted nothrow {
772 pragma(inline, true);
773 if (obj is null) return;
774 EgraCIString pp = obj.getFullPath();
775 auto mod = obj.getCurrentMod();
776 if (mod.length) { pp ~= "\x00"; pp ~= mod; }
777 if (prop.length) { pp ~= "\x00"; pp ~= prop; }
778 styleCache[pp] = Value(val);
781 protected:
782 final void addColorItem() (in auto ref Item ci) @trusted nothrow {
783 if (ci.sel.length && ci.sel.length) {
784 clearStyleCache();
785 version(egra_style_dynstr_debug) {
786 import core.stdc.stdio : stderr, fprintf;
787 fprintf(stderr, "addColorItem:000: style.length=%u; style.capacity=%u; stype.ptr=0x%08x\n",
788 cast(uint)style.length, cast(uint)style.capacity, cast(uint)style.ptr);
789 conwriteln("ADDING: ci.sel:", ci.sel.getData, "; ci.prop:", ci.prop.getData);
791 style ~= Item(ci);
792 version(egra_style_dynstr_debug) {
793 import core.stdc.stdio : stderr, fprintf;
794 fprintf(stderr, "addColorItem:001: style.length=%u; style.capacity=%u; stype.ptr=0x%08x\n",
795 cast(uint)style.length, cast(uint)style.capacity, cast(uint)style.ptr);
796 conwriteln("ADDED(", style.length, "): ci.sel:", style[$-1].sel.getData, "; ci.prop:", style[$-1].prop.getData);
801 public:
802 void parseStyle (const(char)[] str) {
803 auto par = EgraSimpleParser(str);
804 while (!par.isEOT) {
805 if (par.check('!')) {
806 auto cmd = par.expectId();
807 if (cmd.strEquCI("clear-style")) {
808 clear();
809 } else {
810 par.error("invalid command: '"~cmd.idup~"'");
812 par.expect(';');
813 continue;
815 auto sel = par.expectSelector();
816 par.expect("{");
817 while (!par.check("}")) {
818 auto prop = par.expectId();
819 par.expect(":");
820 Item ci;
821 ci.sel = sel;
822 ci.prop = prop;
823 if (par.check('"')) {
824 // string
825 ci.value = Value.String(par.parseString('"'));
826 } else if (par.check('\'')) {
827 // string
828 ci.value = Value.String(par.parseString('\''));
829 } else if (par.checkDigitNoEat() || par.checkNoEat('+') || par.checkNoEat('-')) {
830 // number
831 ci.value = Value.Integer(par.parseInt());
832 } else {
833 // color
834 ci.value = Value.Color(par.parseColor());
836 par.expect(';');
837 addColorItem(ci);
841 version(none) {
842 conwriteln("items: ", style.length);
843 conwriteln("0:ADDED: ci.sel:", style[0].sel.getData, "; ci.prop:", style[0].prop.getData);
844 conwriteln("$-1:ADDED: ci.sel:", style[$-1].sel.getData, "; ci.prop:", style[$-1].prop.getData);
845 foreach (const ref Item it; style; reversed) {
846 conwriteln("*** it.sel:", it.sel.getData, "; it.prop:", it.prop.getData);
851 public:
852 this () {}
854 void cloneFrom (WidgetStyle st) {
855 if (st is null || st is this) return;
856 //Item[] style;
857 clearStyleCache();
858 style.length -= style.length;
859 style.reserve(st.style.length);
860 foreach (const ref Item it; st.style) addColorItem(it);
863 void clear () {
864 clearStyleCache();
865 style.length -= style.length;
868 protected static struct BaseInfo {
869 TypeInfo_Class defaultParent = void;
870 TypeInfo_Class ctsrc = void;
873 protected const(Value)* findValueIntr (EgraStyledClass obj, const(char)[] prop) @trusted nothrow {
874 if (obj is null || style.length == 0 || prop.length == 0) return null;
876 version(egra_style_debug_search) conwriteln("*** SEARCHING:", typeid(obj).name, "; mod=", obj.getCurrentMod(), "; prop:", prop, "****");
877 TypeInfo_Class cioverride = typeid(obj);
878 const(Value)* resval = null;
879 while (cioverride !is null) {
880 const(Value)* resmod = null;
881 const(Value)* resnomod = null;
882 foreach (const ref Item it; style; reversed) {
883 if (it.prop != prop) continue;
884 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);
885 bool modhit, modseen;
886 if (obj.isMySelector(it.sel, classShortName(cioverride), &modhit, &modseen, asQuery:false)) {
887 if (modseen) {
888 // last selector had mod
889 if (!modhit || obj.getCurrentMod().length == 0) continue; // object has no mod, cannot apply
890 } else {
891 // last selector had no mod
892 //if (modhit && obj.getCurrentMod().length != 0) modhit = false;
894 debug conwriteln(" FOUND! modseen=", modseen, "; modhit=", modhit, "; objmod=", obj.getCurrentMod, "; sel=", it.sel);
895 if (modhit) { resmod = &it.value; break; }
896 else if (resnomod is null) resnomod = &it.value;
899 if (resmod !is null) {
900 version(egra_style_debug_search) conwriteln(" FOUND MOD!");
901 resval = resmod;
902 break;
904 if (resnomod !is null) {
905 version(egra_style_debug_search) conwriteln(" FOUND NOMOD!");
906 resval = resnomod;
907 break;
909 cioverride = cioverride.base;
910 if (cioverride is typeid(EgraStyledClass)) {
911 EgraStyledClass tl = obj.getTopLevel();
912 if (tl is null || tl is obj) break;
913 obj = tl;
914 cioverride = typeid(obj);
918 return resval;
921 final const(Value)* findValue (EgraStyledClass obj, const(char)[] prop) @trusted nothrow {
922 if (obj is null || style.length == 0 || prop.length == 0) return null;
924 if (auto fv = findCachedValue(obj, prop)) return (fv.isEmpty ? null : fv);
926 if (auto fv = findValueIntr(obj, prop)) {
927 cacheValue(*fv, obj, prop);
928 return fv;
931 Value val = Value.Empty();
932 cacheValue(val, obj, prop);
933 return null;
936 final uint findColor (EgraStyledClass obj, const(char)[] prop, bool* foundp=null) @trusted nothrow {
937 if (auto val = findValue(obj, prop)) {
938 if (val.isColor) {
939 if (foundp) *foundp = true;
940 return val.color;
944 if (foundp) *foundp = false;
945 return gxUnknown;
948 // returns `null` if not found
949 final dynstring findString (EgraStyledClass obj, const(char)[] prop, bool* foundp=null) @trusted nothrow {
950 if (auto val = findValue(obj, prop)) {
951 if (val.isString) {
952 if (foundp) *foundp = true;
953 return val.sval;
957 if (foundp) *foundp = false;
958 return dynstring();
961 // returns 0 if not found
962 final int findInt (EgraStyledClass obj, const(char)[] prop, in int defval=0, bool* foundp=null) @trusted nothrow {
963 if (auto val = findValue(obj, prop)) {
964 if (val.isInteger) {
965 if (foundp) *foundp = true;
966 return val.ival;
970 if (foundp) *foundp = false;
971 return defval;
974 static:
975 static string classShortName (in TypeInfo_Class ct) pure nothrow @trusted @nogc {
976 pragma(inline, true);
977 if (ct is null) return null;
978 string name = ct.name;
979 auto dpos = name.lastIndexOf('.');
980 return (dpos < 0 ? name : name[dpos+1..$]);
985 abstract class EgraStyledClass {
986 protected:
987 // cached path to this object, w/o property name
988 EgraCIString mCachedPath;
989 WidgetStyle mStyleSheet;
990 // for styles
991 dynstring mId;
992 dynstring mStyleClass;
993 bool mStyleCloned;
995 // call when parent was changed
996 final void invalidatePathCache () nothrow @trusted @nogc { pragma(inline, true); mCachedPath.clear(); }
998 public:
999 bool isMyId (const(char)[] str) nothrow @trusted @nogc { return (str.length == 0 || str.strEquCI(mId.getData)); }
1000 bool isMyStyleClass (const(char)[] str) nothrow @trusted @nogc { return (str.length == 0 || str.strEquCI(mStyleClass.getData)); }
1001 bool isMyModifier (const(char)[] str) nothrow @trusted @nogc { return (str.length == 0 || str.strEquCI(getCurrentMod)); }
1003 EgraStyledClass getParent () nothrow @trusted @nogc { return null; }
1004 EgraStyledClass getFirstChild () nothrow @trusted @nogc { return null; }
1005 EgraStyledClass getNextSibling () nothrow @trusted @nogc { return null; }
1007 EgraStyledClass getTopLevel () nothrow @trusted @nogc {
1008 EgraStyledClass w = getParent();
1009 if (w is null) return null; // we are the top
1010 for (;;) {
1011 EgraStyledClass p = w.getParent();
1012 if (p is null) return w;
1013 w = p;
1017 // empty `str` should return `true`
1018 bool isMyClassName (const(char)[] str) nothrow @trusted @nogc {
1019 return (str.length == 0 || str.strEquCI(classShortName(typeid(this))));
1022 // for styling
1023 EgraCIString getFullPath () nothrow @trusted @nogc {
1024 if (mCachedPath.length == 0) {
1025 mCachedPath.clear(); // just in case
1026 for (EgraStyledClass w = this; w !is null; w = w.getParent()) {
1027 mCachedPath ~= typeid(w).name;
1028 mCachedPath ~= "\x00"; // delimiter
1029 mCachedPath ~= mId;
1030 mCachedPath ~= "\x00"; // delimiter
1031 mCachedPath ~= mStyleClass;
1032 mCachedPath ~= "\x00"; // delimiter
1035 return mCachedPath;
1038 string getCurrentMod () nothrow @trusted @nogc { return ""; }
1040 final WidgetStyle getStyle () nothrow @trusted @nogc {
1041 for (EgraStyledClass w = this; w !is null; w = w.getParent()) {
1042 if (w.mStyleSheet !is null) return w.mStyleSheet;
1044 return defaultColorStyle;
1047 final @property dynstring id () const nothrow @trusted @nogc { pragma(inline, true); return dynstring(mId); }
1048 @property void id (const(char)[] v) nothrow @trusted @nogc {
1049 if (v.length != mId.length || !strEquCI(v, mId.getData)) invalidatePathCache();
1050 if (v != mId.getData) mId = v;
1053 final @property dynstring styleClass () const nothrow @trusted @nogc { pragma(inline, true); return dynstring(mStyleClass); }
1054 @property void styleClass (const(char)[] v) nothrow @trusted @nogc {
1055 if (v.length != mStyleClass.length || !strEquCI(v, mStyleClass.getData)) invalidatePathCache();
1056 if (v != mStyleClass.getData) mStyleClass = v;
1059 public:
1060 void widgetChanged () nothrow {}
1062 void setStyle (WidgetStyle stl) {
1063 if (stl !is mStyleSheet) {
1064 mStyleSheet = stl;
1065 mStyleCloned = false;
1066 widgetChanged();
1070 // this clones the style
1071 void appendStyle (const(char)[] str) {
1072 str = str.xstrip;
1073 if (str.length == 0) return;
1074 if (mStyleSheet is null) {
1075 mStyleSheet = new WidgetStyle;
1076 mStyleSheet.cloneFrom(defaultColorStyle);
1077 mStyleCloned = true;
1078 } else if (!mStyleCloned) {
1079 WidgetStyle ws = new WidgetStyle;
1080 ws.cloneFrom(mStyleSheet);
1081 mStyleSheet = ws;
1082 mStyleCloned = true;
1084 mStyleSheet.parseStyle(str);
1085 widgetChanged();
1088 public:
1089 //this () {}
1090 ~this () nothrow @trusted @nogc { pragma(inline, true); mCachedPath.clear(); }
1092 public:
1093 static template isGoodSelectorDelegate(DG) {
1094 import std.traits;
1095 enum isGoodSelectorDelegate =
1096 (is(ReturnType!DG == void) || is(ReturnType!DG == EgraStyledClass) || is(ReturnType!DG == bool)) &&
1097 is(typeof((inout int=0) { DG dg = void; EgraStyledClass w; dg(w); }));
1100 final EgraStyledClass forEachSelector(DG) (const(char)[] sel, scope DG dg) if (isGoodSelectorDelegate!DG) {
1101 import std.traits;
1102 for (EgraStyledClass w = getFirstChild(); w !is null; w = w.getNextSibling()) {
1103 if (EgraStyledClass res = w.forEachSelector(sel, dg)) return res;
1105 bool modhit;
1106 if (!isMySelector(sel, &modhit)) return null;
1107 if (!modhit) return null;
1108 static if (is(ReturnType!DG == void)) {
1109 dg(this);
1110 return null;
1111 } else static if (is(ReturnType!DG == bool)) {
1112 if (dg(this)) return this;
1113 return null;
1114 } else static if (is(ReturnType!DG == EgraStyledClass)) {
1115 if (EgraStyledClass res = dg(this)) return res;
1116 return null;
1117 } else {
1118 static assert(0, "wtf?!");
1122 final EgraStyledClass querySelector (const(char)[] sel) {
1123 import std.traits;
1124 for (EgraStyledClass w = getFirstChild(); w !is null; w = w.getNextSibling()) {
1125 if (EgraStyledClass res = w.querySelector(sel)) return res;
1127 bool modhit;
1128 if (isMySelector(sel, &modhit)) {
1129 if (modhit) return this;
1131 return null;
1134 static template isGoodIteratorDelegate(DG) {
1135 import std.traits;
1136 enum isGoodIteratorDelegate =
1137 (is(ReturnType!DG == int)) &&
1138 is(typeof((inout int=0) { DG dg = void; EgraStyledClass w; int res = dg(w); }));
1141 static struct Iter {
1142 EgraStyledClass c;
1143 dynstring sel;
1145 this (EgraStyledClass cc, const(char)[] asel) nothrow @safe @nogc {
1146 pragma(inline, true);
1147 asel = asel.xstrip;
1148 if (asel.length) { c = cc; sel = asel; }
1151 int opApply(DG) (scope DG dg) if (isGoodIteratorDelegate!DG) {
1152 int res = 0;
1153 if (c is null || dg is null) return 0;
1154 c.forEachSelector(sel.getData, (EgraStyledClass w) {
1155 res = dg(w);
1156 return (res != 0);
1158 return res;
1162 final Iter querySelectorAll (const(char)[] sel) nothrow @safe @nogc { pragma(inline, true); return Iter(this, sel); }
1164 public:
1165 static string classShortName (in TypeInfo_Class ct) pure nothrow @trusted @nogc {
1166 pragma(inline, true);
1167 if (ct is null) return null;
1168 string name = ct.name;
1169 auto dpos = name.lastIndexOf('.');
1170 return (dpos < 0 ? name : name[dpos+1..$]);
1173 // `from`, or any superclass
1174 static bool isChildOf (in TypeInfo_Class from, const(char)[] cls) pure nothrow @trusted @nogc {
1175 cls = cls.xstrip;
1176 if (cls.length == 0) return false;
1177 // sorry for this cast
1178 for (TypeInfo_Class ti = cast(TypeInfo_Class)from; ti !is null; ti = ti.base) {
1179 if (cls.strEquCI(classShortName(ti))) {
1180 version(none) { import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "%.*s: isGoodMyClass: cls=<%.*s> TRUE\n",
1181 cast(uint)ti.name.length, ti.name.ptr, cast(uint)cls.length, cls.ptr); }
1182 return true;
1184 version(none) { import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "%.*s: isGoodMyClass: cls=<%.*s> FALSE\n",
1185 cast(uint)ti.name.length, ti.name.ptr, cast(uint)cls.length, cls.ptr); }
1187 return false;
1190 static bool isIdChar (in char ch) pure nothrow @trusted @nogc {
1191 pragma(inline, true);
1192 return
1193 (ch >= '0' && ch <= '9') ||
1194 (ch >= 'A' && ch <= 'Z') ||
1195 (ch >= 'a' && ch <= 'z') ||
1196 ch == '_' || ch == '-';
1199 // leading and trailing spaces should be stripped
1200 // also, there should be no spaces inside the string
1201 final bool checkOneSelector (const(char)[] sel, const(char)[] cnoverride, out bool modhit, out bool modseen, in bool asQuery) nothrow @trusted @nogc {
1202 modhit = true;
1203 modseen = false;
1204 //sel = sel.xstrip;
1205 version(none) { import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "%.*s: checkOneSelector: s=<%.*s>\n",
1206 cast(uint)typeid(this).name.length, typeid(this).name.ptr, cast(uint)sel.length, sel.ptr); }
1207 if (sel.length == 0) return false;
1208 usize epos = 0;
1209 while (epos < sel.length && isIdChar(sel.ptr[epos])) ++epos;
1210 const(char)[] cls = sel[0..epos];
1211 if (cls.length != 0) {
1212 if (cnoverride.length) {
1213 if (!cnoverride.strEquCI(cls)) return false;
1214 } else {
1215 if (!isMyClassName(cls)) return false;
1218 sel = sel[epos..$];
1219 // check id and style class
1220 while (sel.length) {
1221 immutable char ch = sel.ptr[0];
1222 if (ch != '.' && ch != '#' && ch != ':') return false;
1223 epos = 1;
1224 while (epos < sel.length && isIdChar(sel.ptr[epos])) ++epos;
1225 const(char)[] nm = sel[1..epos];
1226 sel = sel[epos..$];
1227 final switch (ch) {
1228 case '.': if (!isMyStyleClass(nm)) return false; break;
1229 case '#': if (!isMyId(nm)) return false; break;
1230 case ':': // all modifiers must match
1231 modseen = true;
1232 if (!isMyModifier(nm)) {
1233 debug conwriteln("CHECKONE(", typeid(this).name, "): mod=<", nm, ">: NOT MATCHED! (", getCurrentMod, ")");
1234 modhit = false;
1235 } else {
1236 debug conwriteln("CHECKONE(", typeid(this).name, "): mod=<", nm, ">: MATCHED! (", getCurrentMod, ")");
1238 break;
1241 if (!modseen && !asQuery && getCurrentMod().length) modhit = false;
1242 return true;
1245 final bool isMySelector (const(char)[] sel, const(char)[] clnameoverride,
1246 bool* modhit=null, bool* modseen=null,
1247 in bool asQuery=true) nothrow @trusted @nogc
1249 bool tmpmh, tmpms;
1250 if (modhit) *modhit = false;
1251 if (modseen) *modseen = false;
1252 sel = sel.xstrip;
1253 if (sel.length == 0) return false;
1254 // check object class name
1255 usize epos = sel.length;
1256 while (epos > 0) {
1257 immutable char ch = sel[epos-1];
1258 if (ch <= ' ' || ch == '>') break;
1259 --epos;
1261 if (!checkOneSelector(sel[epos..$], clnameoverride, tmpmh, tmpms, asQuery)) {
1262 return false;
1264 if (modhit) *modhit = tmpmh;
1265 if (modseen) *modseen = tmpms;
1266 sel = sel[0..epos].xstripright;
1267 if (sel.length == 0) return true;
1268 immutable bool oneParent = (sel[$-1] == '>');
1269 if (oneParent) {
1270 sel = sel[0..$-1].xstripright;
1271 if (sel.length == 0) return true;
1272 if (sel[$-1] == '>') return false;
1274 version(none) { import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "%.*s: isMySelector: oneParent=%d\n",
1275 cast(uint)typeid(this).name.length, typeid(this).name.ptr, cast(int)oneParent); }
1276 // sorry for this cast
1277 for (EgraStyledClass w = (cast(EgraStyledClass)this).getParent(); w !is null; w = w.getParent()) {
1278 version(none) { import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "%.*s: isMySelector: parent=<%.*s>; oneParent=%d\n",
1279 cast(uint)typeid(this).name.length, typeid(this).name.ptr, cast(uint)typeid(w).name.length, typeid(w).name.ptr, cast(int)oneParent); }
1280 if (w.isMySelector(sel, null, null, null, asQuery:asQuery)) return true;
1281 if (oneParent) return false;
1283 return false;
1286 final bool isMySelector (const(char)[] sel, bool* modhit=null) nothrow @trusted @nogc {
1287 pragma(inline, true);
1288 return isMySelector(sel, null, modhit);
1291 public:
1292 // returns gxUnknown if not found
1293 final uint getColor (const(char)[] prop, bool* foundp=null) @trusted nothrow {
1294 pragma(inline, true);
1295 return getStyle().findColor(this, prop, foundp);
1298 // returns empty string if not found
1299 final dynstring getString (const(char)[] prop, bool* foundp=null) @trusted nothrow {
1300 pragma(inline, true);
1301 return getStyle().findString(this, prop, foundp);
1304 final int getInt (const(char)[] prop, in int defval=0, bool* foundp=null) @trusted nothrow {
1305 pragma(inline, true);
1306 return getStyle().findInt(this, prop, defval, foundp);
1311 __gshared WidgetStyle defaultColorStyle;
1313 shared static this () {
1314 defaultColorStyle = new WidgetStyle;
1315 defaultColorStyle.parseStyle(defaultStyleText);