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);
125 ButtonWidget:focused {
131 ButtonWidget:disabled {
134 hotline: transparent;
139 back: rgb(0x73, 0x96, 0xdc);
142 rect: rgb(0x40, 0x70, 0xcf);
143 shadowline: rgb(0x83, 0xa6, 0xec);
146 ButtonExWidget:focused {
147 back: rgb(0x93, 0xb6, 0xfc);
150 shadowline: rgb(0xa3, 0xc6, 0xff);
160 CheckboxWidget:focused {
166 CheckboxWidget:disabled {
173 SimpleListBoxWidget {
180 SimpleListBoxWidget:focused {
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;
201 mark-back: rgb(0, 160, 160);
208 text: rgb(220, 220, 0);
213 // ////////////////////////////////////////////////////////////////////////// //
214 struct EgraCIString
{
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)
227 static uint joaatHashFinish (uint hash
) pure nothrow @trusted @nogc {
228 pragma(inline
, true);
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
252 uint hashCurr
; // current hash
255 nothrow @trusted @nogc:
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
); }
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
);
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
);
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
{
316 const(char)[] str; // text left
319 this (const(char)[] atext
) nothrow @safe @nogc { pragma(inline
, true); setText(atext
); }
321 int getCurrentLine () pure const nothrow @safe @nogc {
323 foreach (immutable char ch
; text
[0..$-str.length
]) if (ch
== '\n') ++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
; }
337 return (str.length
== 0);
342 if (str[0] <= ' ') { str = str.xstripleft
; continue; }
343 if (str.length
< 2 ||
str[0] != '/') break;
344 // single-line comment?
347 while (str.length
&& str[0] != '\n') str = str[1..$];
350 // multiline comment?
352 bool endFound
= false;
356 if (str.length
> 1 && str[0] == '*' && str[1] == '/') {
363 if (!endFound
) { str = svs
; error("unfinished comment"); }
366 // multiline nested comment?
368 bool endFound
= false;
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; }
378 if (!endFound
) { str = svs
; error("unfinished comment"); }
385 bool checkNoEat (const(char)[] tk
) {
388 return (str.length
>= tk
.length
&& str[0..tk
.length
] == tk
);
391 bool checkDigitNoEat () {
393 return (str.length
> 0 && isdigit(str[0]));
396 bool checkNoEat (in char ch
) {
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
..$];
408 bool check (in char ch
) {
409 if (!checkNoEat(ch
)) return false;
415 void expect (const(char)[] tk
) {
418 if (!check(tk
)) { str = svs
; error("`"~tk
.idup
~"` expected"); }
421 void expect (in char ch
) {
424 if (!check(ch
)) { str = svs
; error("`"~ch
~"` expected"); }
427 const(char)[] expectId () {
429 if (str.length
== 0) error("identifier expected");
430 if (!isalpha(str[0]) && str[0] != '_' && str[0] != '-') error("identifier expected");
432 while (pos
< str.length
) {
433 if (!isalnum(str[pos
]) && str[pos
] != '_' && str[pos
] != '-') break;
436 const(char)[] res
= str[0..pos
];
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
== '+');
448 if (str.length
== 0) error("selector expected");
449 if (!isSelChar(str[0])) error("selector expected");
451 while (pos
< str.length
&& isSelChar(str[pos
])) ++pos
;
452 const(char)[] res
= str[0..pos
];
460 if (str.length
== 0) error("color expected");
463 if (check('#')) return parseHtmlColor();
466 auto id
= expectId();
468 if (id
.strEquCI("transparent")) return gxTransparent
;
470 // `rgb()` or `rgba()`?
472 if (id
.strEquCI("rgba")) allowAlpha
= true;
473 else if (id
.strEquCI("rgb")) allowAlpha
= false;
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
);
481 if (!check('(')) { str = svs
; error("invalid color definition"); }
482 immutable uint clr
= parseColorRGB(allowAlpha
);
483 if (!check(')')) { str = svs
; error("invalid color definition"); }
487 // open quote already eaten
488 dynstring
parseString (in char qch
) {
489 auto epos
= str.indexOf('"');
490 if (epos
< 0) error("invalid string");
495 while (pos
< str.length
) {
496 immutable char ch
= str.ptr
[pos
++];
497 if (ch
== 0) { str = svs
; error("invalid string"); }
504 if (ch
== '\n') { str = svs
; error("unterminated string"); }
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;
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");
520 error("unterminated string");
526 if (str.length
== 0) error("number expected");
530 else if (check('-')) neg = true;
531 if (str.length
== 0 ||
!isdigit(str[0])) { str = svs
; error("number expected"); }
534 if (str.length
> 1 && str[0] == '0') {
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;
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
);
553 immutable int dg
= digitInBase(str[0], base
);
556 if (n
> vmax
) { str = svs
; error("integer overflow"); }
560 if (str.length
&& isalpha(str[0])) { str = svs
; error("number expected"); }
567 uint parseHtmlColor () {
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
;
580 while (str.length
&& str[0] == '_') str = str[1..$];
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
);
591 while (str.length
&& str[0] == '_') str = str[1..$];
593 foreach (immutable n
; 0..3) rgb
[n
] = cast(ubyte)(rgb
[n
]*16+rgb
[n
]);
596 return gxrgb(rgb
[0], rgb
[1], rgb
[2]);
600 uint parseColorRGB (bool allowAlpha
) {
603 foreach (immutable n
; 0..3+(allowAlpha ?
1 : 0)) {
604 if (n
&& !check(',')) { str = svs
; error("invalid color"); }
606 if (str.length
== 0 ||
!isdigit(str[0])) { str = svs
; error("invalid color"); }
609 if (str[0] == '0' && str.length
>= 2 && (str[1] == 'x' ||
str[1] == 'X')) {
611 if (str.length
== 0 ||
digitInBase(str[0], 16) < 0) { str = svs
; error("invalid color"); }
616 immutable int dg
= digitInBase(str[0], cast(int)base
);
618 val
= val
*base
+cast(uint)dg
;
619 if (val
> 255) { str = svs
; error("invalid color"); }
623 while (str.length
&& str[0] == '_') str = str[1..$];
624 rgba
[n
] = cast(ubyte)val
;
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:
659 static struct Value
{
666 Type type
= Type
.Empty
;
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
; }
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
);
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
; }
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
;
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(); }
758 final const(Value
)* findCachedValue (EgraStyledClass obj
, const(char)[] prop
) @trusted nothrow {
759 pragma(inline
, true);
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
);
782 final void addColorItem() (in auto ref Item ci
) @trusted nothrow {
783 if (ci
.sel
.length
&& ci
.sel
.length
) {
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
);
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
);
802 void parseStyle (const(char)[] str) {
803 auto par
= EgraSimpleParser(str);
805 if (par
.check('!')) {
806 auto cmd
= par
.expectId();
807 if (cmd
.strEquCI("clear-style")) {
810 par
.error("invalid command: '"~cmd
.idup
~"'");
815 auto sel
= par
.expectSelector();
817 while (!par
.check("}")) {
818 auto prop
= par
.expectId();
823 if (par
.check('"')) {
825 ci
.value
= Value
.String(par
.parseString('"'));
826 } else if (par
.check('\'')) {
828 ci
.value
= Value
.String(par
.parseString('\''));
829 } else if (par
.checkDigitNoEat() || par
.checkNoEat('+') || par
.checkNoEat('-')) {
831 ci
.value
= Value
.Integer(par
.parseInt());
834 ci
.value
= Value
.Color(par
.parseColor());
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
);
854 void cloneFrom (WidgetStyle st
) {
855 if (st
is null || st
is this) return;
858 style
.length
-= style
.length
;
859 style
.reserve(st
.style
.length
);
860 foreach (const ref Item it
; st
.style
) addColorItem(it
);
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)) {
888 // last selector had mod
889 if (!modhit || obj
.getCurrentMod().length
== 0) continue; // object has no mod, cannot apply
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!");
904 if (resnomod
!is null) {
905 version(egra_style_debug_search
) conwriteln(" FOUND NOMOD!");
909 cioverride
= cioverride
.base
;
910 if (cioverride
is typeid(EgraStyledClass
)) {
911 EgraStyledClass tl
= obj
.getTopLevel();
912 if (tl
is null || tl
is obj
) break;
914 cioverride
= typeid(obj
);
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
);
931 Value val
= Value
.Empty();
932 cacheValue(val
, obj
, prop
);
936 final uint findColor (EgraStyledClass obj
, const(char)[] prop
, bool* foundp
=null) @trusted nothrow {
937 if (auto val
= findValue(obj
, prop
)) {
939 if (foundp
) *foundp
= true;
944 if (foundp
) *foundp
= false;
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
)) {
952 if (foundp
) *foundp
= true;
957 if (foundp
) *foundp
= false;
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
)) {
965 if (foundp
) *foundp
= true;
970 if (foundp
) *foundp
= false;
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
{
987 // cached path to this object, w/o property name
988 EgraCIString mCachedPath
;
989 WidgetStyle mStyleSheet
;
992 dynstring mStyleClass
;
995 // call when parent was changed
996 final void invalidatePathCache () nothrow @trusted @nogc { pragma(inline
, true); mCachedPath
.clear(); }
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
1011 EgraStyledClass p
= w
.getParent();
1012 if (p
is null) return w
;
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))));
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
1030 mCachedPath
~= "\x00"; // delimiter
1031 mCachedPath
~= mStyleClass
;
1032 mCachedPath
~= "\x00"; // delimiter
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
;
1060 void widgetChanged () nothrow {}
1062 void setStyle (WidgetStyle stl
) {
1063 if (stl
!is mStyleSheet
) {
1065 mStyleCloned
= false;
1070 // this clones the style
1071 void appendStyle (const(char)[] str) {
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
);
1082 mStyleCloned
= true;
1084 mStyleSheet
.parseStyle(str);
1090 ~this () nothrow @trusted @nogc { pragma(inline
, true); mCachedPath
.clear(); }
1093 static template isGoodSelectorDelegate(DG
) {
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
) {
1102 for (EgraStyledClass w
= getFirstChild(); w
!is null; w
= w
.getNextSibling()) {
1103 if (EgraStyledClass res
= w
.forEachSelector(sel
, dg
)) return res
;
1106 if (!isMySelector(sel
, &modhit
)) return null;
1107 if (!modhit
) return null;
1108 static if (is(ReturnType
!DG
== void)) {
1111 } else static if (is(ReturnType
!DG
== bool)) {
1112 if (dg(this)) return this;
1114 } else static if (is(ReturnType
!DG
== EgraStyledClass
)) {
1115 if (EgraStyledClass res
= dg(this)) return res
;
1118 static assert(0, "wtf?!");
1122 final EgraStyledClass
querySelector (const(char)[] sel
) {
1124 for (EgraStyledClass w
= getFirstChild(); w
!is null; w
= w
.getNextSibling()) {
1125 if (EgraStyledClass res
= w
.querySelector(sel
)) return res
;
1128 if (isMySelector(sel
, &modhit
)) {
1129 if (modhit
) return this;
1134 static template isGoodIteratorDelegate(DG
) {
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
{
1145 this (EgraStyledClass cc
, const(char)[] asel
) nothrow @safe @nogc {
1146 pragma(inline
, true);
1148 if (asel
.length
) { c
= cc
; sel
= asel
; }
1151 int opApply(DG
) (scope DG dg
) if (isGoodIteratorDelegate
!DG
) {
1153 if (c
is null || dg
is null) return 0;
1154 c
.forEachSelector(sel
.getData
, (EgraStyledClass w
) {
1162 final Iter
querySelectorAll (const(char)[] sel
) nothrow @safe @nogc { pragma(inline
, true); return Iter(this, sel
); }
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 {
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
); }
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
); }
1190 static bool isIdChar (in char ch
) pure nothrow @trusted @nogc {
1191 pragma(inline
, true);
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 {
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;
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;
1215 if (!isMyClassName(cls
)) return false;
1219 // check id and style class
1220 while (sel
.length
) {
1221 immutable char ch
= sel
.ptr
[0];
1222 if (ch
!= '.' && ch
!= '#' && ch
!= ':') return false;
1224 while (epos
< sel
.length
&& isIdChar(sel
.ptr
[epos
])) ++epos
;
1225 const(char)[] nm
= sel
[1..epos
];
1228 case '.': if (!isMyStyleClass(nm
)) return false; break;
1229 case '#': if (!isMyId(nm
)) return false; break;
1230 case ':': // all modifiers must match
1232 if (!isMyModifier(nm
)) {
1233 debug conwriteln("CHECKONE(", typeid(this).name
, "): mod=<", nm
, ">: NOT MATCHED! (", getCurrentMod
, ")");
1236 debug conwriteln("CHECKONE(", typeid(this).name
, "): mod=<", nm
, ">: MATCHED! (", getCurrentMod
, ")");
1241 if (!modseen
&& !asQuery
&& getCurrentMod().length
) modhit
= false;
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
1250 if (modhit
) *modhit
= false;
1251 if (modseen
) *modseen
= false;
1253 if (sel
.length
== 0) return false;
1254 // check object class name
1255 usize epos
= sel
.length
;
1257 immutable char ch
= sel
[epos
-1];
1258 if (ch
<= ' ' || ch
== '>') break;
1261 if (!checkOneSelector(sel
[epos
..$], clnameoverride
, tmpmh
, tmpms
, asQuery
)) {
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] == '>');
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;
1286 final bool isMySelector (const(char)[] sel
, bool* modhit
=null) nothrow @trusted @nogc {
1287 pragma(inline
, true);
1288 return isMySelector(sel
, null, modhit
);
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
);