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;
59 text-cursor-dot: transparent;
61 text-cursor-blink-time: 800; /* in milliseconds */
62 text-cursor-dot-time: 100; /* dot crawl time, in milliseconds */
79 text-cursor-0: grey67;
81 text-cursor-dot: transparent;
90 back: rgb(0xbf, 0xcf, 0xef);
91 text: rgb(0x16, 0x2d, 0x59);
92 hotline: rgb(0x16, 0x2d, 0x59);
96 frame: rgb(0x40, 0x70, 0xcf);
97 title-back: rgb(0x73, 0x96, 0xdc);
98 title-text: rgb(0xff, 0xff, 0xff);
104 back: rgb(17, 17, 0);
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);
127 grad-midp: 30; /* percents */
130 ButtonWidget:focused {
138 //grad-midp: 20; /* percents */
141 ButtonWidget:disabled {
144 hotline: transparent;
149 //grad-midp: 20; /* percents */
154 back: rgb(0x73, 0x96, 0xdc);
157 rect: rgb(0x40, 0x70, 0xcf);
158 shadowline: rgb(0x83, 0xa6, 0xec);
161 ButtonExWidget:focused {
162 back: rgb(0x93, 0xb6, 0xfc);
165 shadowline: rgb(0xa3, 0xc6, 0xff);
175 CheckboxWidget:focused {
181 CheckboxWidget:disabled {
188 SimpleListBoxWidget {
195 SimpleListBoxWidget:focused {
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;
216 mark-back: rgb(0, 160, 160);
223 text: rgb(220, 220, 0);
228 // ////////////////////////////////////////////////////////////////////////// //
229 struct EgraCIString
{
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)
242 static uint joaatHashFinish (uint hash
) pure nothrow @trusted @nogc {
243 pragma(inline
, true);
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
267 uint hashCurr
; // current hash
270 nothrow @trusted @nogc:
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
); }
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
);
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
);
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
{
331 const(char)[] str; // text left
334 this (const(char)[] atext
) nothrow @safe @nogc { pragma(inline
, true); setText(atext
); }
336 int getCurrentLine () pure const nothrow @safe @nogc {
338 foreach (immutable char ch
; text
[0..$-str.length
]) if (ch
== '\n') ++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
; }
352 return (str.length
== 0);
357 if (str[0] <= ' ') { str = str.xstripleft
; continue; }
358 if (str.length
< 2 ||
str[0] != '/') break;
359 // single-line comment?
362 while (str.length
&& str[0] != '\n') str = str[1..$];
365 // multiline comment?
367 bool endFound
= false;
371 if (str.length
> 1 && str[0] == '*' && str[1] == '/') {
378 if (!endFound
) { str = svs
; error("unfinished comment"); }
381 // multiline nested comment?
383 bool endFound
= false;
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; }
393 if (!endFound
) { str = svs
; error("unfinished comment"); }
400 bool checkNoEat (const(char)[] tk
) {
403 return (str.length
>= tk
.length
&& str[0..tk
.length
] == tk
);
406 bool checkDigitNoEat () {
408 return (str.length
> 0 && isdigit(str[0]));
411 bool checkNoEat (in char ch
) {
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
..$];
423 bool check (in char ch
) {
424 if (!checkNoEat(ch
)) return false;
430 void expect (const(char)[] tk
) {
433 if (!check(tk
)) { str = svs
; error("`"~tk
.idup
~"` expected"); }
436 void expect (in char ch
) {
439 if (!check(ch
)) { str = svs
; error("`"~ch
~"` expected"); }
442 const(char)[] expectId () {
444 if (str.length
== 0) error("identifier expected");
445 if (!isalpha(str[0]) && str[0] != '_' && str[0] != '-') error("identifier expected");
447 while (pos
< str.length
) {
448 if (!isalnum(str[pos
]) && str[pos
] != '_' && str[pos
] != '-') break;
451 const(char)[] res
= str[0..pos
];
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
== '+');
463 if (str.length
== 0) error("selector expected");
464 if (!isSelChar(str[0])) error("selector expected");
466 while (pos
< str.length
&& isSelChar(str[pos
])) ++pos
;
467 const(char)[] res
= str[0..pos
];
475 if (str.length
== 0) error("color expected");
478 if (check('#')) return parseHtmlColor();
481 auto id
= expectId();
483 if (id
.strEquCI("transparent")) return gxTransparent
;
485 // `rgb()` or `rgba()`?
487 if (id
.strEquCI("rgba")) allowAlpha
= true;
488 else if (id
.strEquCI("rgb")) allowAlpha
= false;
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
);
496 if (!check('(')) { str = svs
; error("invalid color definition"); }
497 immutable uint clr
= parseColorRGB(allowAlpha
);
498 if (!check(')')) { str = svs
; error("invalid color definition"); }
502 // open quote already eaten
503 dynstring
parseString (in char qch
) {
504 auto epos
= str.indexOf('"');
505 if (epos
< 0) error("invalid string");
510 while (pos
< str.length
) {
511 immutable char ch
= str.ptr
[pos
++];
512 if (ch
== 0) { str = svs
; error("invalid string"); }
519 if (ch
== '\n') { str = svs
; error("unterminated string"); }
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;
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");
535 error("unterminated string");
541 if (str.length
== 0) error("number expected");
545 else if (check('-')) neg = true;
546 if (str.length
== 0 ||
!isdigit(str[0])) { str = svs
; error("number expected"); }
549 if (str.length
> 1 && str[0] == '0') {
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;
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
);
568 immutable int dg
= digitInBase(str[0], base
);
571 if (n
> vmax
) { str = svs
; error("integer overflow"); }
575 if (str.length
&& isalpha(str[0])) { str = svs
; error("number expected"); }
582 uint parseHtmlColor () {
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
;
595 while (str.length
&& str[0] == '_') str = str[1..$];
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
);
606 while (str.length
&& str[0] == '_') str = str[1..$];
608 foreach (immutable n
; 0..3) rgb
[n
] = cast(ubyte)(rgb
[n
]*16+rgb
[n
]);
611 return gxrgb(rgb
[0], rgb
[1], rgb
[2]);
615 uint parseColorRGB (bool allowAlpha
) {
618 foreach (immutable n
; 0..3+(allowAlpha ?
1 : 0)) {
619 if (n
&& !check(',')) { str = svs
; error("invalid color"); }
621 if (str.length
== 0 ||
!isdigit(str[0])) { str = svs
; error("invalid color"); }
624 if (str[0] == '0' && str.length
>= 2 && (str[1] == 'x' ||
str[1] == 'X')) {
626 if (str.length
== 0 ||
digitInBase(str[0], 16) < 0) { str = svs
; error("invalid color"); }
631 immutable int dg
= digitInBase(str[0], cast(int)base
);
633 val
= val
*base
+cast(uint)dg
;
634 if (val
> 255) { str = svs
; error("invalid color"); }
638 while (str.length
&& str[0] == '_') str = str[1..$];
639 rgba
[n
] = cast(ubyte)val
;
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:
674 static struct Value
{
681 Type type
= Type
.Empty
;
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
; }
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
);
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
; }
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
;
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(); }
773 final const(Value
)* findCachedValue (EgraStyledClass obj
, const(char)[] prop
) @trusted nothrow {
774 pragma(inline
, true);
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
);
797 final void addColorItem() (in auto ref Item ci
) @trusted nothrow {
798 if (ci
.sel
.length
&& ci
.sel
.length
) {
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
);
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
);
817 void parseStyle (const(char)[] str) {
818 auto par
= EgraSimpleParser(str);
820 if (par
.check('!')) {
821 auto cmd
= par
.expectId();
822 if (cmd
.strEquCI("clear-style")) {
825 par
.error("invalid command: '"~cmd
.idup
~"'");
830 auto sel
= par
.expectSelector();
832 while (!par
.check("}")) {
833 auto prop
= par
.expectId();
838 if (par
.check('"')) {
840 ci
.value
= Value
.String(par
.parseString('"'));
841 } else if (par
.check('\'')) {
843 ci
.value
= Value
.String(par
.parseString('\''));
844 } else if (par
.checkDigitNoEat() || par
.checkNoEat('+') || par
.checkNoEat('-')) {
846 ci
.value
= Value
.Integer(par
.parseInt());
849 ci
.value
= Value
.Color(par
.parseColor());
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
);
869 void cloneFrom (WidgetStyle st
) {
870 if (st
is null || st
is this) return;
873 style
.length
-= style
.length
;
874 style
.reserve(st
.style
.length
);
875 foreach (const ref Item it
; st
.style
) addColorItem(it
);
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)) {
903 // last selector had mod
904 if (!modhit || obj
.getCurrentMod().length
== 0) continue; // object has no mod, cannot apply
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!");
919 if (resnomod
!is null) {
920 version(egra_style_debug_search
) conwriteln(" FOUND NOMOD!");
924 cioverride
= cioverride
.base
;
925 if (cioverride
is typeid(EgraStyledClass
)) {
926 EgraStyledClass tl
= obj
.getTopLevel();
927 if (tl
is null || tl
is obj
) break;
929 cioverride
= typeid(obj
);
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
);
946 Value val
= Value
.Empty();
947 cacheValue(val
, obj
, prop
);
951 final uint findColor (EgraStyledClass obj
, const(char)[] prop
, bool* foundp
=null) @trusted nothrow {
952 if (auto val
= findValue(obj
, prop
)) {
954 if (foundp
) *foundp
= true;
959 if (foundp
) *foundp
= false;
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
)) {
967 if (foundp
) *foundp
= true;
972 if (foundp
) *foundp
= false;
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
)) {
980 if (foundp
) *foundp
= true;
985 if (foundp
) *foundp
= false;
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
{
1002 // cached path to this object, w/o property name
1003 EgraCIString mCachedPath
;
1004 WidgetStyle mStyleSheet
;
1007 dynstring mStyleClass
;
1010 // call when parent was changed
1011 final void invalidatePathCache () nothrow @trusted @nogc { pragma(inline
, true); mCachedPath
.clear(); }
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
1026 EgraStyledClass p
= w
.getParent();
1027 if (p
is null) return w
;
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))));
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
1045 mCachedPath
~= "\x00"; // delimiter
1046 mCachedPath
~= mStyleClass
;
1047 mCachedPath
~= "\x00"; // delimiter
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
;
1075 void widgetChanged () nothrow {}
1077 void setStyle (WidgetStyle stl
) {
1078 if (stl
!is mStyleSheet
) {
1080 mStyleCloned
= false;
1085 // this clones the style
1086 void appendStyle (const(char)[] str) {
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
);
1097 mStyleCloned
= true;
1099 mStyleSheet
.parseStyle(str);
1105 ~this () nothrow @trusted @nogc { pragma(inline
, true); mCachedPath
.clear(); }
1108 static template isGoodSelectorDelegate(DG
) {
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
) {
1117 for (EgraStyledClass w
= getFirstChild(); w
!is null; w
= w
.getNextSibling()) {
1118 if (EgraStyledClass res
= w
.forEachSelector(sel
, dg
)) return res
;
1121 if (!isMySelector(sel
, &modhit
)) return null;
1122 if (!modhit
) return null;
1123 static if (is(ReturnType
!DG
== void)) {
1126 } else static if (is(ReturnType
!DG
== bool)) {
1127 if (dg(this)) return this;
1129 } else static if (is(ReturnType
!DG
== EgraStyledClass
)) {
1130 if (EgraStyledClass res
= dg(this)) return res
;
1133 static assert(0, "wtf?!");
1137 final T
querySelectorInternal(T
:EgraStyledClass
) (const(char)[] sel
) {
1139 for (EgraStyledClass w
= getFirstChild(); w
!is null; w
= w
.getNextSibling()) {
1140 if (EgraStyledClass res
= w
.querySelector
!T(sel
)) return cast(T
)res
;
1143 if (isMySelector(sel
, &modhit
)) {
1144 if (modhit
) return cast(T
)this;
1149 final T
querySelector(T
:EgraStyledClass
=EgraStyledClass
) (const(char)[] sel
) { pragma(inline
, true); return querySelectorInternal
!T(sel
); }
1151 static template isGoodIteratorDelegate(DG
, T
) {
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
) {
1162 this (EgraStyledClass cc
, const(char)[] asel
) nothrow @safe @nogc {
1163 pragma(inline
, true);
1165 if (asel
.length
) { c
= cc
; sel
= asel
; }
1168 int opApply(DG
) (scope DG dg
) if (isGoodIteratorDelegate
!(DG
, T
)) {
1170 if (c
is null || dg
is null) return 0;
1171 c
.forEachSelector(sel
.getData
, (EgraStyledClass w
) {
1172 if (auto cw
= cast(T
)w
) {
1181 final auto querySelectorAll(T
:EgraStyledClass
=EgraStyledClass
) (const(char)[] sel
) nothrow @safe @nogc { pragma(inline
, true); return Iter
!T(this, sel
); }
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 {
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
); }
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
); }
1209 static bool isIdChar (in char ch
) pure nothrow @trusted @nogc {
1210 pragma(inline
, true);
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 {
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;
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;
1234 if (!isMyClassName(cls
)) return false;
1238 // check id and style class
1239 while (sel
.length
) {
1240 immutable char ch
= sel
.ptr
[0];
1241 if (ch
!= '.' && ch
!= '#' && ch
!= ':') return false;
1243 while (epos
< sel
.length
&& isIdChar(sel
.ptr
[epos
])) ++epos
;
1244 const(char)[] nm
= sel
[1..epos
];
1247 case '.': if (!isMyStyleClass(nm
)) return false; break;
1248 case '#': if (!isMyId(nm
)) return false; break;
1249 case ':': // all modifiers must match
1251 if (!isMyModifier(nm
)) {
1252 debug conwriteln("CHECKONE(", typeid(this).name
, "): mod=<", nm
, ">: NOT MATCHED! (", getCurrentMod
, ")");
1255 debug conwriteln("CHECKONE(", typeid(this).name
, "): mod=<", nm
, ">: MATCHED! (", getCurrentMod
, ")");
1260 if (!modseen
&& !asQuery
&& getCurrentMod().length
) modhit
= false;
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
1269 if (modhit
) *modhit
= false;
1270 if (modseen
) *modseen
= false;
1272 if (sel
.length
== 0) return false;
1273 // check object class name
1274 usize epos
= sel
.length
;
1276 immutable char ch
= sel
[epos
-1];
1277 if (ch
<= ' ' || ch
== '>') break;
1280 if (!checkOneSelector(sel
[epos
..$], clnameoverride
, tmpmh
, tmpms
, asQuery
)) {
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] == '>');
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;
1305 final bool isMySelector (const(char)[] sel
, bool* modhit
=null) nothrow @trusted @nogc {
1306 pragma(inline
, true);
1307 return isMySelector(sel
, null, modhit
);
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
);