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
;
26 import iv
.xcolornames
;
28 //version = egra_style_dynstr_debug;
29 //version = egra_style_debug_search;
32 // ////////////////////////////////////////////////////////////////////////// //
33 static immutable defaultStyleText
= `
39 shadow-color: rgba(0, 0, 0, 127);
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);
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 */
78 text-cursor-0: grey67;
79 text-cursor-1: grey67;
88 back: rgb(0xbf, 0xcf, 0xef);
89 text: rgb(0x16, 0x2d, 0x59);
90 hotline: rgb(0x16, 0x2d, 0x59);
94 frame: rgb(0x40, 0x70, 0xcf);
95 title-back: rgb(0x73, 0x96, 0xdc);
96 title-text: rgb(0xff, 0xff, 0xff);
102 back: rgb(17, 17, 0);
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);
122 ButtonWidget:focused {
127 ButtonWidget:disabled {
130 hotline: transparent;
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);
152 CheckboxWidget:focused {
158 CheckboxWidget:disabled {
165 SimpleListBoxWidget {
172 SimpleListBoxWidget:focused {
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;
193 mark-back: rgb(0, 160, 160);
200 text: rgb(220, 220, 0);
205 // ////////////////////////////////////////////////////////////////////////// //
206 struct EgraCIString
{
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)
219 static uint joaatHashFinish (uint hash
) pure nothrow @trusted @nogc {
220 pragma(inline
, true);
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
244 uint hashCurr
; // current hash
247 nothrow @trusted @nogc:
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
); }
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
);
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
);
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
{
308 const(char)[] str; // text left
311 this (const(char)[] atext
) nothrow @safe @nogc { pragma(inline
, true); setText(atext
); }
313 int getCurrentLine () pure const nothrow @safe @nogc {
315 foreach (immutable char ch
; text
[0..$-str.length
]) if (ch
== '\n') ++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
; }
329 return (str.length
== 0);
334 if (str[0] <= ' ') { str = str.xstripleft
; continue; }
335 if (str.length
< 2 ||
str[0] != '/') break;
336 // single-line comment?
339 while (str.length
&& str[0] != '\n') str = str[1..$];
342 // multiline comment?
344 bool endFound
= false;
348 if (str.length
> 1 && str[0] == '*' && str[1] == '/') {
355 if (!endFound
) { str = svs
; error("unfinished comment"); }
358 // multiline nested comment?
360 bool endFound
= false;
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; }
370 if (!endFound
) { str = svs
; error("unfinished comment"); }
377 bool checkNoEat (const(char)[] tk
) {
380 return (str.length
>= tk
.length
&& str[0..tk
.length
] == tk
);
383 bool checkDigitNoEat () {
385 return (str.length
> 0 && isdigit(str[0]));
388 bool checkNoEat (in char ch
) {
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
..$];
400 bool check (in char ch
) {
401 if (!checkNoEat(ch
)) return false;
407 void expect (const(char)[] tk
) {
410 if (!check(tk
)) { str = svs
; error("`"~tk
.idup
~"` expected"); }
413 void expect (in char ch
) {
416 if (!check(ch
)) { str = svs
; error("`"~ch
~"` expected"); }
419 const(char)[] expectId () {
421 if (str.length
== 0) error("identifier expected");
422 if (!isalpha(str[0]) && str[0] != '_' && str[0] != '-') error("identifier expected");
424 while (pos
< str.length
) {
425 if (!isalnum(str[pos
]) && str[pos
] != '_' && str[pos
] != '-') break;
428 const(char)[] res
= str[0..pos
];
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
== '+');
440 if (str.length
== 0) error("selector expected");
441 if (!isSelChar(str[0])) error("selector expected");
443 while (pos
< str.length
&& isSelChar(str[pos
])) ++pos
;
444 const(char)[] res
= str[0..pos
];
452 if (str.length
== 0) error("color expected");
455 if (check('#')) return parseHtmlColor();
458 auto id
= expectId();
460 if (id
.strEquCI("transparent")) return gxTransparent
;
462 // `rgb()` or `rgba()`?
464 if (id
.strEquCI("rgba")) allowAlpha
= true;
465 else if (id
.strEquCI("rgb")) allowAlpha
= false;
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
);
473 if (!check('(')) { str = svs
; error("invalid color definition"); }
474 immutable uint clr
= parseColorRGB(allowAlpha
);
475 if (!check(')')) { str = svs
; error("invalid color definition"); }
479 // open quote already eaten
480 dynstring
parseString (in char qch
) {
481 auto epos
= str.indexOf('"');
482 if (epos
< 0) error("invalid string");
487 while (pos
< str.length
) {
488 immutable char ch
= str.ptr
[pos
++];
489 if (ch
== 0) { str = svs
; error("invalid string"); }
496 if (ch
== '\n') { str = svs
; error("unterminated string"); }
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;
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");
512 error("unterminated string");
518 if (str.length
== 0) error("number expected");
522 else if (check('-')) neg = true;
523 if (str.length
== 0 ||
!isdigit(str[0])) { str = svs
; error("number expected"); }
526 if (str.length
> 1 && str[0] == '0') {
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;
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
);
545 immutable int dg
= digitInBase(str[0], base
);
548 if (n
> vmax
) { str = svs
; error("integer overflow"); }
552 if (str.length
&& isalpha(str[0])) { str = svs
; error("number expected"); }
559 uint parseHtmlColor () {
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
;
572 while (str.length
&& str[0] == '_') str = str[1..$];
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
);
583 while (str.length
&& str[0] == '_') str = str[1..$];
585 foreach (immutable n
; 0..3) rgb
[n
] = cast(ubyte)(rgb
[n
]*16+rgb
[n
]);
588 return gxrgb(rgb
[0], rgb
[1], rgb
[2]);
592 uint parseColorRGB (bool allowAlpha
) {
595 foreach (immutable n
; 0..3+(allowAlpha ?
1 : 0)) {
596 if (n
&& !check(',')) { str = svs
; error("invalid color"); }
598 if (str.length
== 0 ||
!isdigit(str[0])) { str = svs
; error("invalid color"); }
601 if (str[0] == '0' && str.length
>= 2 && (str[1] == 'x' ||
str[1] == 'X')) {
603 if (str.length
== 0 ||
digitInBase(str[0], 16) < 0) { str = svs
; error("invalid color"); }
608 immutable int dg
= digitInBase(str[0], cast(int)base
);
610 val
= val
*base
+cast(uint)dg
;
611 if (val
> 255) { str = svs
; error("invalid color"); }
615 while (str.length
&& str[0] == '_') str = str[1..$];
616 rgba
[n
] = cast(ubyte)val
;
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:
651 static struct Value
{
658 Type type
= Type
.Empty
;
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
; }
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
);
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
; }
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
;
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(); }
750 final const(Value
)* findCachedValue (EgraStyledClass obj
, const(char)[] prop
) @trusted nothrow {
751 pragma(inline
, true);
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
);
774 final void addColorItem() (in auto ref Item ci
) @trusted nothrow {
775 if (ci
.sel
.length
&& ci
.sel
.length
) {
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
);
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
);
794 void parseStyle (const(char)[] str) {
795 auto par
= EgraSimpleParser(str);
797 if (par
.check('!')) {
798 auto cmd
= par
.expectId();
799 if (cmd
.strEquCI("clear-style")) {
802 par
.error("invalid command: '"~cmd
.idup
~"'");
807 auto sel
= par
.expectSelector();
809 while (!par
.check("}")) {
810 auto prop
= par
.expectId();
815 if (par
.check('"')) {
817 ci
.value
= Value
.String(par
.parseString('"'));
818 } else if (par
.check('\'')) {
820 ci
.value
= Value
.String(par
.parseString('\''));
821 } else if (par
.checkDigitNoEat() || par
.checkNoEat('+') || par
.checkNoEat('-')) {
823 ci
.value
= Value
.Integer(par
.parseInt());
826 ci
.value
= Value
.Color(par
.parseColor());
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
);
846 void cloneFrom (WidgetStyle st
) {
847 if (st
is null || st
is this) return;
850 style
.length
-= style
.length
;
851 style
.reserve(st
.style
.length
);
852 foreach (const ref Item it
; st
.style
) addColorItem(it
);
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
);
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!");
888 if (resnomod
!is null) {
889 version(egra_style_debug_search
) conwriteln(" FOUND NOMOD!");
893 cioverride
= cioverride
.base
;
894 if (cioverride
is typeid(EgraStyledClass
)) {
895 EgraStyledClass tl
= obj
.getTopLevel();
896 if (tl
is null || tl
is obj
) break;
898 cioverride
= typeid(obj
);
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
);
915 Value val
= Value
.Empty();
916 cacheValue(val
, obj
, prop
);
920 final uint findColor (EgraStyledClass obj
, const(char)[] prop
, bool* foundp
=null) @trusted nothrow {
921 if (auto val
= findValue(obj
, prop
)) {
923 if (foundp
) *foundp
= true;
928 if (foundp
) *foundp
= false;
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
)) {
936 if (foundp
) *foundp
= true;
941 if (foundp
) *foundp
= false;
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
)) {
949 if (foundp
) *foundp
= true;
954 if (foundp
) *foundp
= false;
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
{
971 // cached path to this object, w/o property name
972 EgraCIString mCachedPath
;
973 WidgetStyle mStyleSheet
;
976 dynstring mStyleClass
;
979 // call when parent was changed
980 final void invalidatePathCache () nothrow @trusted @nogc { pragma(inline
, true); mCachedPath
.clear(); }
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
995 EgraStyledClass p
= w
.getParent();
996 if (p
is null) return w
;
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))));
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
1014 mCachedPath
~= "\x00"; // delimiter
1015 mCachedPath
~= mStyleClass
;
1016 mCachedPath
~= "\x00"; // delimiter
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
;
1044 void widgetChanged () nothrow {}
1046 void setStyle (WidgetStyle stl
) {
1047 if (stl
!is mStyleSheet
) {
1049 mStyleCloned
= false;
1054 // this clones the style
1055 void appendStyle (const(char)[] str) {
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
);
1066 mStyleCloned
= true;
1068 mStyleSheet
.parseStyle(str);
1074 ~this () nothrow @trusted @nogc { pragma(inline
, true); mCachedPath
.clear(); }
1077 static template isGoodSelectorDelegate(DG
) {
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
) {
1086 for (EgraStyledClass w
= getFirstChild(); w
!is null; w
= w
.getNextSibling()) {
1087 if (EgraStyledClass res
= w
.forEachSelector(sel
, dg
)) return res
;
1090 if (!isMySelector(sel
, &modhit
)) return null;
1091 if (!modhit
) return null;
1092 static if (is(ReturnType
!DG
== void)) {
1095 } else static if (is(ReturnType
!DG
== bool)) {
1096 if (dg(this)) return this;
1098 } else static if (is(ReturnType
!DG
== EgraStyledClass
)) {
1099 if (EgraStyledClass res
= dg(this)) return res
;
1102 static assert(0, "wtf?!");
1106 final EgraStyledClass
querySelector (const(char)[] sel
) {
1108 for (EgraStyledClass w
= getFirstChild(); w
!is null; w
= w
.getNextSibling()) {
1109 if (EgraStyledClass res
= w
.querySelector(sel
)) return res
;
1112 if (isMySelector(sel
, &modhit
)) {
1113 if (modhit
) return this;
1118 static template isGoodIteratorDelegate(DG
) {
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
{
1129 this (EgraStyledClass cc
, const(char)[] asel
) nothrow @safe @nogc {
1130 pragma(inline
, true);
1132 if (asel
.length
) { c
= cc
; sel
= asel
; }
1135 int opApply(DG
) (scope DG dg
) if (isGoodIteratorDelegate
!DG
) {
1137 if (c
is null || dg
is null) return 0;
1138 c
.forEachSelector(sel
.getData
, (EgraStyledClass w
) {
1146 final Iter
querySelectorAll (const(char)[] sel
) nothrow @safe @nogc { pragma(inline
, true); return Iter(this, sel
); }
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 {
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
); }
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
); }
1174 static bool isIdChar (in char ch
) pure nothrow @trusted @nogc {
1175 pragma(inline
, true);
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;
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;
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;
1198 if (!isMyClassName(cls
)) return false;
1201 bool wasMod
= false;
1203 // check id and style class
1204 while (sel
.length
) {
1205 immutable char ch
= sel
.ptr
[0];
1206 if (ch
!= '.' && ch
!= '#' && ch
!= ':') return false;
1208 while (epos
< sel
.length
&& isIdChar(sel
.ptr
[epos
])) ++epos
;
1209 const(char)[] nm
= sel
[1..epos
];
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;
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;
1224 if (sel
.length
== 0) return false;
1225 // check object class name
1226 usize epos
= sel
.length
;
1228 immutable char ch
= sel
[epos
-1];
1229 if (ch
<= ' ' || ch
== '>') break;
1232 if (!checkOneSelector(sel
[epos
..$], clnameoverride
, modhit
, asQuery
)) {
1233 if (modhit
) *modhit
= false;
1236 sel
= sel
[0..epos
].xstripright
;
1237 if (sel
.length
== 0) return true;
1238 immutable bool oneParent
= (sel
[$-1] == '>');
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;
1256 final bool isMySelector (const(char)[] sel
, bool* modhit
=null) nothrow @trusted @nogc {
1257 pragma(inline
, true);
1258 return isMySelector(sel
, null, modhit
);
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
);