more paints
[amper.git] / amperskin.d
blob098dccd7843ea110665325a06f7c254a6ef6d6ad
1 module amperskin is aliced;
3 import arsd.bmp;
4 import arsd.color;
5 import arsd.image;
6 import arsd.simpledisplay;
8 import iv.cmdcon;
9 import iv.cmdcongl;
10 import iv.strex;
11 import iv.vfs.io;
13 import egfx;
16 // ////////////////////////////////////////////////////////////////////////// //
17 class QuitEvent {}
20 void postQuitEvent () {
21 if (glconMainWindow is null) return;
22 if (!glconMainWindow.eventQueued!QuitEvent) glconMainWindow.postEvent(new QuitEvent());
26 // ////////////////////////////////////////////////////////////////////////// //
27 //__gshared string skinfile = "skins/WastedAMP1-0.zip";
28 __gshared string skinfile = "skins/winamp2.zip";
29 __gshared bool modeShuffle = false;
30 __gshared bool modeRepeat = false;
32 shared static this () {
33 conRegVar!modeShuffle("mode_shuffle", "shuffle playlist");
34 conRegVar!modeRepeat("mode_repeat", "repeat playlist");
38 // ////////////////////////////////////////////////////////////////////////// //
39 // width: 275; height: shaded: 14, full: 116
40 __gshared EImage skinImgMain;
41 __gshared EImage skinImgCButtons;
42 __gshared EImage skinImgShufRep;
43 __gshared EImage skinImgNums;
44 __gshared EImage skinImgPlaylist;
45 __gshared EImage skinImgTitle;
46 __gshared uint skinPListColNormal = gxRGB!(0, 255, 0);
47 __gshared uint skinPListColCurrent = gxRGB!(255, 255, 255);
48 __gshared uint skinPListColNormalBG = gxRGB!(0, 0, 0);
49 __gshared uint skinPListColCurrentBG = gxRGB!(0, 0, 255);
52 // ////////////////////////////////////////////////////////////////////////// //
53 __gshared AmpMainWindow ampMain;
54 __gshared AmpPListWindow ampPList;
57 // ////////////////////////////////////////////////////////////////////////// //
58 void loadSkin () {
59 static struct KeyValue {
60 ConString key;
61 ConString value;
62 nothrow @trusted @nogc:
63 this (ConString s) {
64 s = s.xstrip;
65 usize pos = 0;
66 while (pos < s.length && s.ptr[pos] != '=' && s.ptr[pos] != ';') ++pos;
67 key = s[0..pos].xstrip;
68 if (pos < s.length && s.ptr[pos] == '=') {
69 ++pos;
70 if (pos < s.length && s.ptr[pos] <= ' ') ++pos;
71 auto stpos = pos;
72 if (pos < s.length && s.ptr[pos] != ';') ++pos;
73 value = s[stpos..pos].xstrip;
77 uint asColor () const {
78 auto val = value.xstrip;
79 if (val.length != 4 && val.length != 7) return gxTransparent;
80 int[3] rgb;
81 if (val.length == 4) {
82 foreach (immutable idx, char ch; val[1..$]) {
83 int dg = digitInBase(ch, 16);
84 if (dg < 0) return gxTransparent;
85 dg = 255*dg/15;
86 rgb[idx] = dg;
88 } else {
89 foreach (immutable idx; 0..3) {
90 int d0 = digitInBase(val[1+idx*2], 16);
91 int d1 = digitInBase(val[2+idx*2], 16);
92 if (d0 < 0 || d1 < 0) return gxTransparent;
93 rgb[idx] = d0*16+d1;
96 return gxrgb(rgb[0], rgb[1], rgb[2]);
100 static void parsePlaylistConfig (VFile fl) {
101 bool inTextSection = false;
102 foreach (auto line; fl.byLine) {
103 line = line.xstrip;
104 if (line.length == 0 || line.ptr[0] == ';') continue;
105 if (line[0] == '[') {
106 inTextSection = line.strEquCI("[Text]");
107 continue;
109 if (inTextSection) {
110 auto kv = KeyValue(line);
111 auto clr = kv.asColor;
112 if (clr.gxIsTransparent) continue;
113 if (kv.key.strEquCI("Normal")) skinPListColNormal = clr;
114 else if (kv.key.strEquCI("Current")) skinPListColCurrent = clr;
115 else if (kv.key.strEquCI("NormalBG")) skinPListColNormalBG = clr;
116 else if (kv.key.strEquCI("CurrentBG")) skinPListColCurrentBG = clr;
121 static bool isGoodImageExtension (ConString fname) {
122 auto epos = fname.lastIndexOf('.');
123 if (epos < 0) return false;
124 fname = fname[epos..$];
125 return
126 fname.strEquCI(".bmp") ||
127 fname.strEquCI(".png") ||
128 fname.strEquCI(".jpg") ||
129 fname.strEquCI(".jpeg") ||
130 false;
133 void loadImage (EImage* img, string fname) {
134 auto epos = fname.lastIndexOf('.');
135 if (epos < 0) throw new Exception("invalid skin image '"~fname~"'");
136 auto ext = fname[epos..$];
138 auto fl = VFile(fname);
139 auto sz = fl.size;
140 if (sz < 1 || sz > 1024*1024*8) throw new Exception("invalid skin image '"~fname~"'");
142 auto data = new ubyte[](cast(uint)sz);
143 scope(exit) delete data;
144 fl.rawReadExact(data);
146 MemoryImage ximg;
147 scope(exit) delete ximg;
149 if (ext.strEquCI(".bmp")) ximg = readBmp(data);
150 else if (ext.strEquCI(".png")) ximg = imageFromPng(readPng(data));
151 else if (ext.strEquCI(".jpg") || ext.strEquCI(".jpeg")) ximg = readJpegFromMemory(data);
152 else throw new Exception("unknown image format for '"~fname~"'");
153 assert(ximg !is null);
155 *img = new EImage(ximg);
158 AmpPListWindow.SavedData pldata;
159 if (ampPList !is null) pldata = ampPList.saveData();
160 scope(exit) if (ampPList !is null) ampPList.restoreData(pldata);
162 ampMain = null;
163 ampPList = null;
165 skinPListColNormal = gxRGB!(0, 255, 0);
166 skinPListColCurrent = gxRGB!(255, 255, 255);
167 skinPListColNormalBG = gxRGB!(0, 0, 0);
168 skinPListColCurrentBG = gxRGB!(0, 0, 255);
170 EImage numsex;
172 static struct ImageInfo {
173 string name;
174 EImage* img;
175 bool optional;
177 ImageInfo[$] imglist = [
178 ImageInfo("main", &skinImgMain),
179 ImageInfo("cbuttons", &skinImgCButtons),
180 ImageInfo("shufrep", &skinImgShufRep),
181 ImageInfo("numbers", &numsex),
182 ImageInfo("nums_ex", &skinImgNums),
183 ImageInfo("pledit", &skinImgPlaylist),
184 ImageInfo("titlebar", &skinImgTitle, true),
187 foreach (ref ii; imglist) { *(ii.img) = null; }
189 auto vid = vfsAddPak(skinfile, "skin:");
190 scope(exit) vfsRemovePak(vid);
191 foreach (auto de; vfsFileList) {
192 if (!isGoodImageExtension(de.name)) continue;
194 auto fname = de.name;
195 auto xpos = fname.lastIndexOf('/');
196 if (xpos >= 0) fname = fname[xpos+1..$];
197 else if (fname.startsWith("skin:")) fname = fname[5..$];
198 xpos = fname.lastIndexOf('.');
199 if (xpos <= 0) continue;
200 auto iname = fname[0..xpos];
201 //conwriteln("[", de.name, "]:[", fname, "]:[", iname, "]");
203 if (fname.strEquCI("pledit.txt")) {
204 parsePlaylistConfig(VFile(de.name));
205 continue;
208 foreach (ref ii; imglist) {
209 if (*(ii.img) is null && iname.strEquCI(ii.name)) {
210 //conwriteln(de.name);
211 loadImage(ii.img, de.name);
212 break;
217 if (skinImgNums is null) {
218 foreach (ref ii; imglist) if (ii.name == "numbers" && *(ii.img) !is null) skinImgNums = *(ii.img);
221 foreach (ref ii; imglist) {
222 if (ii.name == "numbers" && *(ii.img) is null) *(ii.img) = skinImgNums;
223 if (ii.name == "nums_ex" && *(ii.img) is null) *(ii.img) = numsex;
224 if (*(ii.img) is null && !ii.optional) throw new Exception("invalid skin (missing '"~ii.name~"' image)");
227 ampMain = new AmpMainWindow();
228 ampPList = new AmpPListWindow();
232 // ////////////////////////////////////////////////////////////////////////// //
233 final class EImage {
234 int width;
235 int height;
236 uint[] data;
238 this (MemoryImage img) {
239 if (img is null) throw new Exception("can't create eimage from nothing");
240 width = img.width;
241 height = img.height;
242 data.length = width*height;
243 { import core.memory : GC; GC.setAttr(data.ptr, GC.BlkAttr.NO_INTERIOR|GC.BlkAttr.NO_SCAN); }
244 foreach (immutable int dy; 0..img.height) {
245 foreach (immutable int dx; 0..img.width) {
246 auto cc = img.getPixel(dx, dy);
247 data[dy*width+dx] = gxrgb(cc.r, cc.g, cc.b);
252 final void blitAt (int x, int y) nothrow @trusted @nogc {
254 if (x < 0 || y < 0 || x+width > VBufWidth || y+height > VBufHeight) return;
255 const(uint)* src = data.ptr;
256 uint* dst = vglTexBuf+y*VBufWidth+x;
257 immutable int w = width;
258 foreach (immutable dy; 0..height) {
259 dst[0..w] = src[0..w];
260 src += width;
261 dst += VBufWidth;
264 blitRect(x, y, GxRect(0, 0, width, height));
267 final void blitRect (int destx, int desty, GxRect srect) nothrow @trusted @nogc {
268 srect.intersect(GxRect(0, 0, width, height));
269 if (srect.empty) return;
270 assert(srect.x0 >= 0 && srect.x0 < width);
271 assert(srect.y0 >= 0 && srect.y0 < height);
272 assert(srect.x1 >= 0 && srect.x1 < width);
273 assert(srect.y1 >= 0 && srect.y1 < height);
274 int dx = destx;
275 int dy = desty;
276 int dw = srect.width;
277 int dh = srect.height;
278 int skipLeft, skipTop;
279 if (!gxClipRect.clipHVStripes(dx, dy, dw, dh, &skipLeft, &skipTop)) return;
280 if (!GxRect(0, 0, VBufWidth, VBufHeight).clipHVStripes(dx, dy, dw, dh, &skipLeft, &skipTop)) return;
281 assert(dw > 0 && dw <= width);
282 assert(dh > 0 && dh <= height);
283 assert(skipLeft+srect.x0+dw <= width);
284 assert(skipTop+srect.y0+dh <= height);
285 const(uint)* src = data.ptr+(skipTop+srect.y0)*width+(skipLeft+srect.x0);
286 uint* dst = vglTexBuf+dy*VBufWidth+dx;
287 while (dh-- > 0) {
288 assert(src+dw <= data.ptr+width*height);
289 assert(dst+dw <= vglTexBuf+VBufWidth*VBufHeight);
290 dst[0..dw] = src[0..dw];
291 src += width;
292 dst += VBufWidth;
298 // ////////////////////////////////////////////////////////////////////////// //
299 class AmpWindow {
300 bool active;
301 EImage img;
302 GxRect imgrc;
303 AmpWidget[] widgets;
304 int mActiveWidget = -1;
306 this (EImage aimg) {
307 if (aimg is null) throw new Exception("no image for window");
308 img = aimg;
309 imgrc = GxRect(0, 0, img.width, img.height);
312 AmpWidget addWidget (AmpWidget w) {
313 if (w is null) return null;
314 if (w.parent !is null) throw new Exception("widget already owned");
315 widgets ~= w;
316 w.parent = this;
317 return w;
320 void defocused () {
321 if (active) {
322 mActiveWidget = -1;
323 active = false;
324 glconPostScreenRebuild();
328 void focused () {
329 if (!active) {
330 active = true;
331 glconPostScreenRebuild();
335 final void focusChanged (bool focused) {
336 if (focused) this.focused(); else this.defocused();
339 final @property AmpWidget activeWidget () pure nothrow @safe @nogc { pragma(inline, true); return (mActiveWidget >= 0 && mActiveWidget < widgets.length ? widgets[mActiveWidget] : null); }
341 final @property void activeWidget (AmpWidget w) pure nothrow @safe @nogc {
342 if (w is null || w.parent !is this) { mActiveWidget = -1; return; }
343 foreach (immutable idx, AmpWidget ww; widgets) if (ww is w) { mActiveWidget = cast(int)idx; return; }
344 mActiveWidget = -1;
347 final AmpWidget widgetAt (int x, int y) pure nothrow @safe @nogc {
348 foreach_reverse (AmpWidget w; widgets) {
349 if (w.rc.inside(x, y)) return w;
351 return null;
354 protected void paintWidgets () {
355 foreach (AmpWidget w; widgets) {
356 gxClipRect = w.rc;
357 w.onPaint();
361 protected void paintBackground () {
362 gxClipReset();
363 img.blitAt(0, 0);
366 protected void paintFinished () {
369 void onPaint () {
370 paintBackground();
371 paintWidgets();
372 paintFinished();
375 bool onKeyPre (KeyEvent event) { return false; }
377 bool onKeyPost (KeyEvent event) { return false; }
379 bool onKey (KeyEvent event) {
380 if (onKeyPre(event)) return true;
381 if (auto aw = activeWidget) {
382 if (aw.onKey(event)) return true;
384 if (onKeyPost(event)) return true;
385 return false;
388 bool onMousePre (MouseEvent event) { return false; }
389 bool onMousePost (MouseEvent event) { return false; }
391 bool onMouse (MouseEvent event) {
392 bool wasAwOrAct = false;
393 if (onMousePre(event)) return true;
394 auto aw = activeWidget;
395 if (aw !is null) {
396 if (aw.onMouse(event)) return true;
397 wasAwOrAct = true;
399 if (auto ww = widgetAt(event.x, event.y)) {
400 if (ww !is aw) {
401 if (ww.onMouse(event)) return true;
402 wasAwOrAct = true;
405 if (onMousePost(event)) return true;
406 if (wasAwOrAct && event.type != MouseEventType.motion) return true;
407 return false;
412 // ////////////////////////////////////////////////////////////////////////// //
413 class AmpWidget {
414 AmpWindow parent;
415 EImage img;
416 GxRect rc;
417 GxRect imgrc;
418 string cmd;
420 void delegate () onAction;
422 protected this () {}
424 this (EImage aimg, int x, int y, GxRect aimgrc, string acmd=null) {
425 if (aimg is null) throw new Exception("no image for window");
426 img = aimg;
427 imgrc = aimgrc;
428 rc = GxRect(x, y, aimgrc.width, aimgrc.height);
429 cmd = acmd;
432 void onPaint () {
433 bool active = (parent !is null && parent.activeWidget is this);
434 auto irc = imgrc;
435 if (active) irc.moveBy(0, imgrc.height);
436 img.blitRect(rc.x0, rc.y0, irc);
439 bool onKey (KeyEvent event) {
440 return false;
443 bool onMouse (MouseEvent event) {
444 if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) {
445 parent.activeWidget = this;
446 return true;
448 if (event.type == MouseEventType.buttonReleased && event.button == MouseButton.left) {
449 if (parent.activeWidget is this) {
450 parent.activeWidget = null;
451 if (rc.inside(event.x, event.y)) {
452 concmd(cmd);
453 if (onAction !is null) onAction();
455 return true;
458 return false;
463 // ////////////////////////////////////////////////////////////////////////// //
464 class AmpWidgetToggle : AmpWidget {
465 bool* checked;
466 private bool checkedvar;
468 this (bool* cvp, EImage aimg, int x, int y, GxRect aimgrc, string acmd=null) {
469 super(aimg, x, y, aimgrc, acmd);
470 if (cvp is null) cvp = &checkedvar;
471 checked = cvp;
474 override void onPaint () {
475 bool active = (parent !is null && parent.activeWidget is this);
476 auto irc = imgrc;
477 if (*checked) irc.moveBy(0, imgrc.height*2);
478 if (active) irc.moveBy(0, imgrc.height);
479 img.blitRect(rc.x0, rc.y0, irc);
484 // ////////////////////////////////////////////////////////////////////////// //
485 class AmpWidgetSongTitle : AmpWidget {
486 int titleofs = 0;
487 int titleofsmovedir = 1;
488 int titlemovepause = 8;
489 string title;
490 bool dragging = false;
492 this(T:const(char)[]) (GxRect arc, T atitle) {
493 static if (is(T == typeof(null))) title = null;
494 else static if (is(T == string)) title = atitle;
495 else title = atitle.idup;
496 super();
497 rc = arc;
500 override void onPaint () {
501 if (title.length) {
502 gxClipRect = rc;
503 gxDrawTextUtf(rc.x0-titleofs, rc.y0-2, title, gxRGB!(0, 255, 0));
507 final void scrollTitle () {
508 if (parent.activeWidget !is this) dragging = false;
509 if (dragging) return;
510 if (titlemovepause-- >= 0) return;
511 titlemovepause = 0;
512 if (title.length == 0) { titleofs = 0; titleofsmovedir = 1; titlemovepause = 8; return; }
513 auto tw = gxTextWidthUtf(title);
514 titleofs += titleofsmovedir;
515 if (titleofs <= 0) { titleofs = 0; titleofsmovedir = 1; titlemovepause = 8; }
516 else if (titleofs >= tw-rc.width) { titleofs = tw-rc.width; titleofsmovedir = -1; titlemovepause = 8; }
519 override bool onKey (KeyEvent event) {
520 return false;
523 override bool onMouse (MouseEvent event) {
524 if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) {
525 parent.activeWidget = this;
526 dragging = true;
527 return true;
529 if (event.type == MouseEventType.buttonReleased && event.button == MouseButton.left) {
530 if (parent.activeWidget is this) {
531 dragging = false;
532 if (title.length != 0) {
533 titlemovepause = 10;
534 auto tw = gxTextWidthUtf(title);
535 if (titleofs <= 0) { titleofs = 0; titleofsmovedir = 1; }
536 else if (titleofs >= tw-rc.width) { titleofs = tw-rc.width; titleofsmovedir = -1; }
538 parent.activeWidget = null;
539 return true;
542 if (dragging && event.type == MouseEventType.motion && event.modifierState&ModifierState.leftButtonDown) {
543 if (title.length != 0 && event.dx != 0) {
544 titleofs -= event.dx;
545 //conwriteln("dx=", event.dx, "; titleofs=", titleofs);
546 titleofsmovedir = (event.dx < 0 ? 1 : -1);
547 auto tw = gxTextWidthUtf(title);
548 if (titleofs <= 0) titleofs = 0;
549 else if (titleofs >= tw-rc.width) titleofs = tw-rc.width;
552 return false;
557 // ////////////////////////////////////////////////////////////////////////// //
558 class AmpMainWindow : AmpWindow {
559 AmpWidgetSongTitle wtitle;
561 this () {
562 super(skinImgMain);
563 addWidget(new AmpWidget(skinImgCButtons, 16, 88, GxRect(0, 0, 23, 18), "song_prev"));
564 addWidget(new AmpWidget(skinImgCButtons, 39, 88, GxRect(23, 0, 23, 18), "song_play"));
565 addWidget(new AmpWidget(skinImgCButtons, 62, 88, GxRect(46, 0, 23, 18), "song_pause"));
566 addWidget(new AmpWidget(skinImgCButtons, 85, 88, GxRect(69, 0, 23, 18), "song_stop"));
567 addWidget(new AmpWidget(skinImgCButtons, 108, 88, GxRect(92, 0, 22, 18), "song_next"));
568 addWidget(new AmpWidget(skinImgCButtons, 136, 88, GxRect(114, 0, 22, 16), "song_eject"));
569 addWidget(new AmpWidgetToggle(&modeShuffle, skinImgShufRep, 164, 89, GxRect(28, 0, 46, 15), "mode_shuffle toggle"));
570 addWidget(new AmpWidgetToggle(&modeRepeat, skinImgShufRep, 210, 89, GxRect(0, 0, 28, 15), "mode_repeat toggle"));
571 wtitle = cast(AmpWidgetSongTitle)addWidget(new AmpWidgetSongTitle(GxRect(109, 24, 157, 12), "Testing text... жопф дохуя текста"));
574 final void scrollTitle () {
575 if (wtitle !is null) wtitle.scrollTitle();
578 protected override void paintBackground () {
579 gxClipReset();
580 img.blitAt(0, 0);
581 auto irc = GxRect(27, 0, 275, 14);
582 if (!active) irc.moveBy(0, 15);
583 skinImgTitle.blitRect(0, 0, irc);
586 override bool onKeyPost (KeyEvent event) {
587 if (event == "C-Q") { if (event.pressed) postQuitEvent(); return true; }
588 return false;
593 // ////////////////////////////////////////////////////////////////////////// //
594 struct PListItem {
595 string title;
596 string fname;
597 int duration; // in seconds
601 class AmpPListWindow : AmpWindow {
602 static struct SavedData {
603 PListItem[] items;
604 int totaldur = 0;
605 int topitem = 0;
606 int curitem = 0;
609 PListItem[] items;
610 int totaldur = 0;
611 int topitem = 0;
612 int curitem = 0;
614 public:
615 this () {
616 super(skinImgMain);
617 img = skinImgPlaylist;
620 auto saveData () {
621 SavedData res;
622 res.items = items;
623 res.totaldur = totaldur;
624 res.topitem = topitem;
625 res.curitem = curitem;
626 return res;
629 void restoreData (ref SavedData sd) {
630 items = sd.items;
631 totaldur = sd.totaldur;
632 topitem = sd.topitem;
633 curitem = sd.curitem;
636 void appendListItem (PListItem li) {
637 if (li.duration > 0) totaldur += li.duration;
638 auto optr = items.ptr;
639 items ~= li;
640 // check if runtime allocated a new memory block due to array growth
641 if (items.ptr !is optr) {
642 import core.memory : GC;
643 // check if our array really *IS* the head
644 if (items.ptr is GC.addrOf(items.ptr)) {
645 // set the magic flag!
646 GC.setAttr(items.ptr, GC.BlkAttr.NO_INTERIOR);
651 final @property int visibleFullItems () const nothrow @trusted {
652 int hgt = imgrc.height-20-38;
653 int res = hgt/gxTextHeightUtf;
654 if (res < 1) res = 1;
655 return res;
658 final @property int visibleItems () const nothrow @trusted {
659 int hgt = imgrc.height-20-38;
660 int res = hgt/gxTextHeightUtf;
661 if (hgt%gxTextHeightUtf) ++res;
662 if (res < 1) res = 1;
663 return res;
666 void normTop () nothrow @trusted {
667 if (curitem < 0) curitem = 0;
668 if (items.length == 0) { topitem = 0; return; }
669 if (curitem >= items.length) curitem = cast(int)items.length-1;
670 int tl = topitem;
671 if (tl < 0) tl = 0;
672 if (tl < curitem) tl = curitem;
673 if (tl+visibleFullItems >= curitem) tl = curitem-visibleFullItems+1;
674 int el = topitem+visibleItems-1;
675 if (el >= items.length) {
676 tl = cast(int)items.length-visibleItems;
678 if (tl < 0) tl = 0;
679 topitem = tl;
682 final @property int knobYOfs () nothrow @trusted {
683 normTop();
684 if (items.length < visibleItems+1) return 0;
685 int hgt = imgrc.height-37-19-18;
686 if (hgt < 1) return 0;
687 return cast(int)(hgt*topitem/(items.length-visibleItems));
688 // normal knob: 52,53 8x18 at width-15,[19..height-37)
691 final void paintScrollKnob () {
692 int xofs = imgrc.width-15;
693 int yofs = knobYOfs+19;
694 img.blitRect(xofs, yofs, GxRect(52, 53, 8, 18));
697 final void setListClip () {
698 gxClipRect = GxRect(11, 20, imgrc.width-11-20+1, imgrc.height-20-38);
701 final void paintList () {
702 char[1024] buf = void;
703 setListClip();
704 normTop();
705 if (items.length < 1) return;
706 int ty = 20;
707 int idx = topitem;
708 assert(idx >= 0);
709 while (idx < items.length && ty < imgrc.height-38) {
710 import core.stdc.stdio : snprintf;
711 auto len = snprintf(buf.ptr, buf.length, "%d. %.*s", idx+1, cast(uint)items.ptr[idx].title.length, items.ptr[idx].title.ptr);
712 if (curitem == idx) gxFillRect(gxClipRect.x0, ty, gxClipRect.width, gxTextHeightUtf, skinPListColCurrentBG);
713 gxDrawTextUtf(gxClipRect.x0, ty, buf[0..len], (curitem != idx ? skinPListColNormal : skinPListColCurrent));
714 ty += gxTextHeightUtf;
715 ++idx;
719 protected override void paintBackground () {
720 int tx, ty;
721 gxFillRect(imgrc, skinPListColNormalBG);
722 auto irc = GxRect(0, 0, 25, 20);
723 if (!active) irc.moveBy(0, 21);
724 // top frame
725 // tiles
726 irc.x0 = 127;
727 tx = irc.width;
728 while (tx < imgrc.width-irc.width) {
729 img.blitRect(tx, 0, irc);
730 tx += irc.width;
732 // top title
733 auto irtitle = irc;
734 irtitle.x0 = 26;
735 irtitle.width = 100;
736 img.blitRect((imgrc.width-irtitle.width)/2, 0, irtitle);
737 // top left corner
738 irc.x0 = 0;
739 img.blitRect(0, 0, irc);
740 // top right corner
741 irc.x0 = 153;
742 img.blitRect(imgrc.width-irc.width, 0, irc);
743 // left and right bars
744 ty = irc.height;
745 irc.width = 25;
746 irc.height = 29;
747 irc.y0 = 42;
748 while (ty < imgrc.height-irc.height) {
749 // left bar
750 irc.x0 = 0;
751 img.blitRect(0, ty, irc);
752 // right bar
753 irc.x0 = 26;
754 img.blitRect(imgrc.width-irc.width, ty, irc);
755 ty += irc.height;
757 // bottom frame
758 // left part: 0,72 125x38
759 // right part: 126,72 150x38
760 // tile: 179,0 25x38
761 irc.height = 38;
762 ty = imgrc.height-irc.height;
763 // tiles
764 irc.width = 25;
765 irc.x0 = 179;
766 irc.y0 = 0;
767 tx = 125;
768 while (tx < imgrc.width-150) {
769 img.blitRect(tx, ty, irc);
770 tx += irc.width;
772 // bottom left corner
773 irc.x0 = 0;
774 irc.y0 = 72;
775 irc.width = 125;
776 img.blitRect(0, ty, irc);
777 // bottom right corner
778 irc.x0 = 126;
779 irc.width = 150;
780 img.blitRect(imgrc.width-irc.width, ty, irc);
781 // list
782 paintList();
785 protected override void paintFinished () {
786 gxClipReset();
787 paintScrollKnob();
788 // close pressed: 52,42 9x9 at width-10
789 // normal knob: 52,53 8x18 at width-15,[19..height-37)
790 // pressed knob: 61,53 8x18 at width-15,[19..height-37)
791 // resize knob at top-right, size is 20x20
794 override bool onKeyPost (KeyEvent event) {
795 if (event == "C-Q") { if (event.pressed) postQuitEvent(); return true; }
796 return false;
799 override bool onMousePost (MouseEvent event) {
800 setListClip();
801 if (gxClipRect.inside(event.x, event.y)) {
802 normTop();
803 scope(exit) normTop();
804 if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.wheelUp) {
805 if (curitem > 0) --curitem;
806 return true;
808 if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.wheelDown) {
809 ++curitem;
810 return true;
813 return false;