focus
[amper.git] / amperskin.d
blobc7154d4c80b2cd228b2201c12044f2a78203883e
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 uint skinPListColNormal = gxRGB!(0, 255, 0);
46 __gshared uint skinPListColCurrent = gxRGB!(255, 255, 255);
47 __gshared uint skinPListColNormalBG = gxRGB!(0, 0, 0);
48 __gshared uint skinPListColCurrentBG = gxRGB!(0, 0, 255);
51 // ////////////////////////////////////////////////////////////////////////// //
52 __gshared AmpMainWindow ampMain;
53 __gshared AmpPListWindow ampPList;
56 // ////////////////////////////////////////////////////////////////////////// //
57 void loadSkin () {
58 static struct KeyValue {
59 ConString key;
60 ConString value;
61 nothrow @trusted @nogc:
62 this (ConString s) {
63 s = s.xstrip;
64 usize pos = 0;
65 while (pos < s.length && s.ptr[pos] != '=' && s.ptr[pos] != ';') ++pos;
66 key = s[0..pos].xstrip;
67 if (pos < s.length && s.ptr[pos] == '=') {
68 ++pos;
69 if (pos < s.length && s.ptr[pos] <= ' ') ++pos;
70 auto stpos = pos;
71 if (pos < s.length && s.ptr[pos] != ';') ++pos;
72 value = s[stpos..pos].xstrip;
76 uint asColor () const {
77 auto val = value.xstrip;
78 if (val.length != 4 && val.length != 7) return gxTransparent;
79 int[3] rgb;
80 if (val.length == 4) {
81 foreach (immutable idx, char ch; val[1..$]) {
82 int dg = digitInBase(ch, 16);
83 if (dg < 0) return gxTransparent;
84 dg = 255*dg/15;
85 rgb[idx] = dg;
87 } else {
88 foreach (immutable idx; 0..3) {
89 int d0 = digitInBase(val[1+idx*2], 16);
90 int d1 = digitInBase(val[2+idx*2], 16);
91 if (d0 < 0 || d1 < 0) return gxTransparent;
92 rgb[idx] = d0*16+d1;
95 return gxrgb(rgb[0], rgb[1], rgb[2]);
99 static void parsePlaylistConfig (VFile fl) {
100 bool inTextSection = false;
101 foreach (auto line; fl.byLine) {
102 line = line.xstrip;
103 if (line.length == 0 || line.ptr[0] == ';') continue;
104 if (line[0] == '[') {
105 inTextSection = line.strEquCI("[Text]");
106 continue;
108 if (inTextSection) {
109 auto kv = KeyValue(line);
110 auto clr = kv.asColor;
111 if (clr.gxIsTransparent) continue;
112 if (kv.key.strEquCI("Normal")) skinPListColNormal = clr;
113 else if (kv.key.strEquCI("Current")) skinPListColCurrent = clr;
114 else if (kv.key.strEquCI("NormalBG")) skinPListColNormalBG = clr;
115 else if (kv.key.strEquCI("CurrentBG")) skinPListColCurrentBG = clr;
120 static bool isGoodImageExtension (ConString fname) {
121 auto epos = fname.lastIndexOf('.');
122 if (epos < 0) return false;
123 fname = fname[epos..$];
124 return
125 fname.strEquCI(".bmp") ||
126 fname.strEquCI(".png") ||
127 fname.strEquCI(".jpg") ||
128 fname.strEquCI(".jpeg") ||
129 false;
132 void loadImage (EImage* img, string fname) {
133 auto epos = fname.lastIndexOf('.');
134 if (epos < 0) throw new Exception("invalid skin image '"~fname~"'");
135 auto ext = fname[epos..$];
137 auto fl = VFile(fname);
138 auto sz = fl.size;
139 if (sz < 1 || sz > 1024*1024*8) throw new Exception("invalid skin image '"~fname~"'");
141 auto data = new ubyte[](cast(uint)sz);
142 scope(exit) delete data;
143 fl.rawReadExact(data);
145 MemoryImage ximg;
146 scope(exit) delete ximg;
148 if (ext.strEquCI(".bmp")) ximg = readBmp(data);
149 else if (ext.strEquCI(".png")) ximg = imageFromPng(readPng(data));
150 else if (ext.strEquCI(".jpg") || ext.strEquCI(".jpeg")) ximg = readJpegFromMemory(data);
151 else throw new Exception("unknown image format for '"~fname~"'");
152 assert(ximg !is null);
154 *img = new EImage(ximg);
157 AmpPListWindow.SavedData pldata;
158 if (ampPList !is null) pldata = ampPList.saveData();
159 scope(exit) if (ampPList !is null) ampPList.restoreData(pldata);
161 ampMain = null;
162 ampPList = null;
164 skinPListColNormal = gxRGB!(0, 255, 0);
165 skinPListColCurrent = gxRGB!(255, 255, 255);
166 skinPListColNormalBG = gxRGB!(0, 0, 0);
167 skinPListColCurrentBG = gxRGB!(0, 0, 255);
169 EImage numsex;
171 static struct ImageInfo {
172 string name;
173 EImage* img;
175 ImageInfo[$] imglist = [
176 ImageInfo("main", &skinImgMain),
177 ImageInfo("cbuttons", &skinImgCButtons),
178 ImageInfo("shufrep", &skinImgShufRep),
179 ImageInfo("numbers", &numsex),
180 ImageInfo("nums_ex", &skinImgNums),
181 ImageInfo("pledit", &skinImgPlaylist),
184 foreach (ref ii; imglist) { *(ii.img) = null; }
186 auto vid = vfsAddPak(skinfile, "skin:");
187 scope(exit) vfsRemovePak(vid);
188 foreach (auto de; vfsFileList) {
189 if (!isGoodImageExtension(de.name)) continue;
191 auto fname = de.name;
192 auto xpos = fname.lastIndexOf('/');
193 if (xpos >= 0) fname = fname[xpos+1..$];
194 else if (fname.startsWith("skin:")) fname = fname[5..$];
195 xpos = fname.lastIndexOf('.');
196 if (xpos <= 0) continue;
197 auto iname = fname[0..xpos];
198 //conwriteln("[", de.name, "]:[", fname, "]:[", iname, "]");
200 if (fname.strEquCI("pledit.txt")) {
201 parsePlaylistConfig(VFile(de.name));
202 continue;
205 foreach (ref ii; imglist) {
206 if (*(ii.img) is null && iname.strEquCI(ii.name)) {
207 //conwriteln(de.name);
208 loadImage(ii.img, de.name);
209 break;
214 if (skinImgNums is null) {
215 foreach (ref ii; imglist) if (ii.name == "numbers" && *(ii.img) !is null) skinImgNums = *(ii.img);
218 foreach (ref ii; imglist) {
219 if (ii.name == "numbers" && *(ii.img) is null) *(ii.img) = skinImgNums;
220 if (ii.name == "nums_ex" && *(ii.img) is null) *(ii.img) = numsex;
221 if (*(ii.img) is null) throw new Exception("invalid skin (missing '"~ii.name~"' image)");
224 ampMain = new AmpMainWindow();
225 ampPList = new AmpPListWindow();
229 // ////////////////////////////////////////////////////////////////////////// //
230 final class EImage {
231 int width;
232 int height;
233 uint[] data;
235 this (MemoryImage img) {
236 if (img is null) throw new Exception("can't create eimage from nothing");
237 width = img.width;
238 height = img.height;
239 data.length = width*height;
240 { import core.memory : GC; GC.setAttr(data.ptr, GC.BlkAttr.NO_INTERIOR|GC.BlkAttr.NO_SCAN); }
241 foreach (immutable int dy; 0..img.height) {
242 foreach (immutable int dx; 0..img.width) {
243 auto cc = img.getPixel(dx, dy);
244 data[dy*width+dx] = gxrgb(cc.r, cc.g, cc.b);
249 final void blitAt (int x, int y) nothrow @trusted @nogc {
251 if (x < 0 || y < 0 || x+width > VBufWidth || y+height > VBufHeight) return;
252 const(uint)* src = data.ptr;
253 uint* dst = vglTexBuf+y*VBufWidth+x;
254 immutable int w = width;
255 foreach (immutable dy; 0..height) {
256 dst[0..w] = src[0..w];
257 src += width;
258 dst += VBufWidth;
261 blitRect(x, y, GxRect(0, 0, width, height));
264 final void blitRect (int destx, int desty, GxRect srect) nothrow @trusted @nogc {
265 srect.intersect(GxRect(0, 0, width, height));
266 if (srect.empty) return;
267 assert(srect.x0 >= 0 && srect.x0 < width);
268 assert(srect.y0 >= 0 && srect.y0 < height);
269 assert(srect.x1 >= 0 && srect.x1 < width);
270 assert(srect.y1 >= 0 && srect.y1 < height);
271 int dx = destx;
272 int dy = desty;
273 int dw = srect.width;
274 int dh = srect.height;
275 int skipLeft, skipTop;
276 if (!gxClipRect.clipHVStripes(dx, dy, dw, dh, &skipLeft, &skipTop)) return;
277 if (!GxRect(0, 0, VBufWidth, VBufHeight).clipHVStripes(dx, dy, dw, dh, &skipLeft, &skipTop)) return;
278 assert(dw > 0 && dw <= width);
279 assert(dh > 0 && dh <= height);
280 assert(skipLeft+srect.x0+dw <= width);
281 assert(skipTop+srect.y0+dh <= height);
282 const(uint)* src = data.ptr+(skipTop+srect.y0)*width+(skipLeft+srect.x0);
283 uint* dst = vglTexBuf+dy*VBufWidth+dx;
284 while (dh-- > 0) {
285 assert(src+dw <= data.ptr+width*height);
286 assert(dst+dw <= vglTexBuf+VBufWidth*VBufHeight);
287 dst[0..dw] = src[0..dw];
288 src += width;
289 dst += VBufWidth;
295 // ////////////////////////////////////////////////////////////////////////// //
296 class AmpWindow {
297 bool active;
298 EImage img;
299 GxRect imgrc;
300 AmpWidget[] widgets;
301 int mActiveWidget = -1;
303 this (EImage aimg) {
304 if (aimg is null) throw new Exception("no image for window");
305 img = aimg;
306 imgrc = GxRect(0, 0, img.width, img.height);
309 AmpWidget addWidget (AmpWidget w) {
310 if (w is null) return null;
311 if (w.parent !is null) throw new Exception("widget already owned");
312 widgets ~= w;
313 w.parent = this;
314 return w;
317 void defocused () {
318 mActiveWidget = -1;
319 active = false;
322 void focused () {
323 active = true;
326 final void focusChanged (bool focused) {
327 if (focused) this.focused(); else this.defocused();
330 final @property AmpWidget activeWidget () pure nothrow @safe @nogc { pragma(inline, true); return (mActiveWidget >= 0 && mActiveWidget < widgets.length ? widgets[mActiveWidget] : null); }
332 final @property void activeWidget (AmpWidget w) pure nothrow @safe @nogc {
333 if (w is null || w.parent !is this) { mActiveWidget = -1; return; }
334 foreach (immutable idx, AmpWidget ww; widgets) if (ww is w) { mActiveWidget = cast(int)idx; return; }
335 mActiveWidget = -1;
338 final AmpWidget widgetAt (int x, int y) pure nothrow @safe @nogc {
339 foreach_reverse (AmpWidget w; widgets) {
340 if (w.rc.inside(x, y)) return w;
342 return null;
345 void onPaint () {
346 gxClipReset();
347 img.blitAt(0, 0);
348 foreach (AmpWidget w; widgets) {
349 gxClipRect = w.rc;
350 w.onPaint();
354 bool onKeyPre (KeyEvent event) { return false; }
356 bool onKeyPost (KeyEvent event) { return false; }
358 bool onKey (KeyEvent event) {
359 if (onKeyPre(event)) return true;
360 if (auto aw = activeWidget) {
361 if (aw.onKey(event)) return true;
363 if (onKeyPost(event)) return true;
364 return false;
367 bool onMousePre (MouseEvent event) { return false; }
368 bool onMousePost (MouseEvent event) { return false; }
370 bool onMouse (MouseEvent event) {
371 bool wasAwOrAct = false;
372 if (onMousePre(event)) return true;
373 auto aw = activeWidget;
374 if (aw !is null) {
375 if (aw.onMouse(event)) return true;
376 wasAwOrAct = true;
378 if (auto ww = widgetAt(event.x, event.y)) {
379 if (ww !is aw) {
380 if (ww.onMouse(event)) return true;
381 wasAwOrAct = true;
384 if (onMousePost(event)) return true;
385 if (wasAwOrAct && event.type != MouseEventType.motion) return true;
386 return false;
391 // ////////////////////////////////////////////////////////////////////////// //
392 class AmpWidget {
393 AmpWindow parent;
394 EImage img;
395 GxRect rc;
396 GxRect imgrc;
397 string cmd;
399 void delegate () onAction;
401 protected this () {}
403 this (EImage aimg, int x, int y, GxRect aimgrc, string acmd=null) {
404 if (aimg is null) throw new Exception("no image for window");
405 img = aimg;
406 imgrc = aimgrc;
407 rc = GxRect(x, y, aimgrc.width, aimgrc.height);
408 cmd = acmd;
411 void onPaint () {
412 bool active = (parent !is null && parent.activeWidget is this);
413 auto irc = imgrc;
414 if (active) irc.moveBy(0, imgrc.height);
415 img.blitRect(rc.x0, rc.y0, irc);
418 bool onKey (KeyEvent event) {
419 return false;
422 bool onMouse (MouseEvent event) {
423 if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) {
424 parent.activeWidget = this;
425 return true;
427 if (event.type == MouseEventType.buttonReleased && event.button == MouseButton.left) {
428 if (parent.activeWidget is this) {
429 parent.activeWidget = null;
430 if (rc.inside(event.x, event.y)) {
431 concmd(cmd);
432 if (onAction !is null) onAction();
434 return true;
437 return false;
442 // ////////////////////////////////////////////////////////////////////////// //
443 class AmpWidgetToggle : AmpWidget {
444 bool* checked;
445 private bool checkedvar;
447 this (bool* cvp, EImage aimg, int x, int y, GxRect aimgrc, string acmd=null) {
448 super(aimg, x, y, aimgrc, acmd);
449 if (cvp is null) cvp = &checkedvar;
450 checked = cvp;
453 override void onPaint () {
454 bool active = (parent !is null && parent.activeWidget is this);
455 auto irc = imgrc;
456 if (*checked) irc.moveBy(0, imgrc.height*2);
457 if (active) irc.moveBy(0, imgrc.height);
458 img.blitRect(rc.x0, rc.y0, irc);
463 // ////////////////////////////////////////////////////////////////////////// //
464 class AmpWidgetSongTitle : AmpWidget {
465 int titleofs = 0;
466 int titleofsmovedir = 1;
467 int titlemovepause = 8;
468 string title;
469 bool dragging = false;
471 this(T:const(char)[]) (GxRect arc, T atitle) {
472 static if (is(T == typeof(null))) title = null;
473 else static if (is(T == string)) title = atitle;
474 else title = atitle.idup;
475 super();
476 rc = arc;
479 override void onPaint () {
480 if (title.length) {
481 gxClipRect = rc;
482 gxDrawTextUtf(rc.x0-titleofs, rc.y0-2, title, gxRGB!(0, 255, 0));
486 final void scrollTitle () {
487 if (parent.activeWidget !is this) dragging = false;
488 if (dragging) return;
489 if (titlemovepause-- >= 0) return;
490 titlemovepause = 0;
491 if (title.length == 0) { titleofs = 0; titleofsmovedir = 1; titlemovepause = 8; return; }
492 auto tw = gxTextWidthUtf(title);
493 titleofs += titleofsmovedir;
494 if (titleofs <= 0) { titleofs = 0; titleofsmovedir = 1; titlemovepause = 8; }
495 else if (titleofs >= tw-rc.width) { titleofs = tw-rc.width; titleofsmovedir = -1; titlemovepause = 8; }
498 override bool onKey (KeyEvent event) {
499 return false;
502 override bool onMouse (MouseEvent event) {
503 if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) {
504 parent.activeWidget = this;
505 dragging = true;
506 return true;
508 if (event.type == MouseEventType.buttonReleased && event.button == MouseButton.left) {
509 if (parent.activeWidget is this) {
510 dragging = false;
511 if (title.length != 0) {
512 titlemovepause = 10;
513 auto tw = gxTextWidthUtf(title);
514 if (titleofs <= 0) { titleofs = 0; titleofsmovedir = 1; }
515 else if (titleofs >= tw-rc.width) { titleofs = tw-rc.width; titleofsmovedir = -1; }
517 parent.activeWidget = null;
518 return true;
521 if (dragging && event.type == MouseEventType.motion && event.modifierState&ModifierState.leftButtonDown) {
522 if (title.length != 0 && event.dx != 0) {
523 titleofs -= event.dx;
524 //conwriteln("dx=", event.dx, "; titleofs=", titleofs);
525 titleofsmovedir = (event.dx < 0 ? 1 : -1);
526 auto tw = gxTextWidthUtf(title);
527 if (titleofs <= 0) titleofs = 0;
528 else if (titleofs >= tw-rc.width) titleofs = tw-rc.width;
531 return false;
536 // ////////////////////////////////////////////////////////////////////////// //
537 class AmpMainWindow : AmpWindow {
538 AmpWidgetSongTitle wtitle;
540 this () {
541 super(skinImgMain);
542 addWidget(new AmpWidget(skinImgCButtons, 16, 88, GxRect(0, 0, 23, 18), "song_prev"));
543 addWidget(new AmpWidget(skinImgCButtons, 39, 88, GxRect(23, 0, 23, 18), "song_play"));
544 addWidget(new AmpWidget(skinImgCButtons, 62, 88, GxRect(46, 0, 23, 18), "song_pause"));
545 addWidget(new AmpWidget(skinImgCButtons, 85, 88, GxRect(69, 0, 23, 18), "song_stop"));
546 addWidget(new AmpWidget(skinImgCButtons, 108, 88, GxRect(92, 0, 22, 18), "song_next"));
547 addWidget(new AmpWidget(skinImgCButtons, 136, 88, GxRect(114, 0, 22, 16), "song_eject"));
548 addWidget(new AmpWidgetToggle(&modeShuffle, skinImgShufRep, 164, 89, GxRect(28, 0, 46, 15), "mode_shuffle toggle"));
549 addWidget(new AmpWidgetToggle(&modeRepeat, skinImgShufRep, 210, 89, GxRect(0, 0, 28, 15), "mode_repeat toggle"));
550 wtitle = cast(AmpWidgetSongTitle)addWidget(new AmpWidgetSongTitle(GxRect(109, 24, 157, 12), "Testing text... жопф дохуя текста"));
553 final void scrollTitle () {
554 if (wtitle !is null) wtitle.scrollTitle();
557 override bool onKeyPost (KeyEvent event) {
558 if (event == "C-Q") { if (event.pressed) postQuitEvent(); return true; }
559 return false;
564 // ////////////////////////////////////////////////////////////////////////// //
565 struct PListItem {
566 string title;
567 string fname;
568 int duration; // in seconds
572 class AmpPListWindow : AmpWindow {
573 static struct SavedData {
574 PListItem[] items;
575 int totaldur = 0;
576 int topitem = 0;
577 int curitem = 0;
580 PListItem[] items;
581 int totaldur = 0;
582 int topitem = 0;
583 int curitem = 0;
585 public:
586 this () {
587 super(skinImgMain);
588 img = skinImgPlaylist;
591 auto saveData () {
592 SavedData res;
593 res.items = items;
594 res.totaldur = totaldur;
595 res.topitem = topitem;
596 res.curitem = curitem;
597 return res;
600 void restoreData (ref SavedData sd) {
601 items = sd.items;
602 totaldur = sd.totaldur;
603 topitem = sd.topitem;
604 curitem = sd.curitem;
607 void appendListItem (PListItem li) {
608 if (li.duration > 0) totaldur += li.duration;
609 auto optr = items.ptr;
610 items ~= li;
611 // check if runtime allocated a new memory block due to array growth
612 if (items.ptr !is optr) {
613 import core.memory : GC;
614 // check if our array really *IS* the head
615 if (items.ptr is GC.addrOf(items.ptr)) {
616 // set the magic flag!
617 GC.setAttr(items.ptr, GC.BlkAttr.NO_INTERIOR);
622 final @property int visibleFullItems () const nothrow @trusted {
623 int hgt = imgrc.height-20-38;
624 int res = hgt/gxTextHeightUtf;
625 if (res < 1) res = 1;
626 return res;
629 final @property int visibleItems () const nothrow @trusted {
630 int hgt = imgrc.height-20-38;
631 int res = hgt/gxTextHeightUtf;
632 if (hgt%gxTextHeightUtf) ++res;
633 if (res < 1) res = 1;
634 return res;
637 void normTop () nothrow @trusted {
638 if (curitem < 0) curitem = 0;
639 if (items.length == 0) { topitem = 0; return; }
640 if (curitem >= items.length) curitem = cast(int)items.length-1;
641 int tl = topitem;
642 if (tl < 0) tl = 0;
643 if (tl < curitem) tl = curitem;
644 if (tl+visibleFullItems >= curitem) tl = curitem-visibleFullItems+1;
645 int el = topitem+visibleItems-1;
646 if (el >= items.length) {
647 tl = cast(int)items.length-visibleItems;
649 if (tl < 0) tl = 0;
650 topitem = tl;
653 final @property int knobYOfs () nothrow @trusted {
654 normTop();
655 if (items.length < visibleItems+1) return 0;
656 int hgt = imgrc.height-37-19-18;
657 if (hgt < 1) return 0;
658 return cast(int)(hgt*topitem/(items.length-visibleItems));
659 // normal knob: 52,53 8x18 at width-15,[19..height-37)
662 final void paintScrollKnob () {
663 int xofs = imgrc.width-15;
664 int yofs = knobYOfs+19;
665 img.blitRect(xofs, yofs, GxRect(52, 53, 8, 18));
668 final void setListClip () {
669 gxClipRect = GxRect(11, 20, imgrc.width-11-20+1, imgrc.height-20-38);
672 final void paintList () {
673 char[1024] buf = void;
674 setListClip();
675 normTop();
676 if (items.length < 1) return;
677 int ty = 20;
678 int idx = topitem;
679 assert(idx >= 0);
680 while (idx < items.length && ty < imgrc.height-38) {
681 import core.stdc.stdio : snprintf;
682 auto len = snprintf(buf.ptr, buf.length, "%d. %.*s", idx+1, cast(uint)items.ptr[idx].title.length, items.ptr[idx].title.ptr);
683 if (curitem == idx) gxFillRect(gxClipRect.x0, ty, gxClipRect.width, gxTextHeightUtf, skinPListColCurrentBG);
684 gxDrawTextUtf(gxClipRect.x0, ty, buf[0..len], (curitem != idx ? skinPListColNormal : skinPListColCurrent));
685 ty += gxTextHeightUtf;
686 ++idx;
690 override void onPaint () {
691 int tx, ty;
692 gxFillRect(imgrc, skinPListColNormalBG);
693 auto irc = GxRect(0, 0, 25, 20);
694 if (!active) irc.moveBy(0, 21);
695 // top frame
696 // tiles
697 irc.x0 = 127;
698 tx = irc.width;
699 while (tx < imgrc.width-irc.width) {
700 img.blitRect(tx, 0, irc);
701 tx += irc.width;
703 // top title
704 auto irtitle = irc;
705 irtitle.x0 = 26;
706 irtitle.width = 100;
707 img.blitRect((imgrc.width-irtitle.width)/2, 0, irtitle);
708 // top left corner
709 irc.x0 = 0;
710 img.blitRect(0, 0, irc);
711 // top right corner
712 irc.x0 = 153;
713 img.blitRect(imgrc.width-irc.width, 0, irc);
714 // left and right bars
715 ty = irc.height;
716 irc.width = 25;
717 irc.height = 29;
718 irc.y0 = 42;
719 while (ty < imgrc.height-irc.height) {
720 // left bar
721 irc.x0 = 0;
722 img.blitRect(0, ty, irc);
723 // right bar
724 irc.x0 = 26;
725 img.blitRect(imgrc.width-irc.width, ty, irc);
726 ty += irc.height;
728 // bottom frame
729 // left part: 0,72 125x38
730 // right part: 126,72 150x38
731 // tile: 179,0 25x38
732 irc.height = 38;
733 ty = imgrc.height-irc.height;
734 // tiles
735 irc.width = 25;
736 irc.x0 = 179;
737 irc.y0 = 0;
738 tx = 125;
739 while (tx < imgrc.width-150) {
740 img.blitRect(tx, ty, irc);
741 tx += irc.width;
743 // bottom left corner
744 irc.x0 = 0;
745 irc.y0 = 72;
746 irc.width = 125;
747 img.blitRect(0, ty, irc);
748 // bottom right corner
749 irc.x0 = 126;
750 irc.width = 150;
751 img.blitRect(imgrc.width-irc.width, ty, irc);
752 // scrollbar
753 paintScrollKnob();
754 paintList();
755 gxClipReset();
756 // widgets
757 foreach (AmpWidget w; widgets) {
758 gxClipRect = w.rc;
759 w.onPaint();
761 // close pressed: 52,42 9x9 at width-10
762 // normal knob: 52,53 8x18 at width-15,[19..height-37)
763 // pressed knob: 61,53 8x18 at width-15,[19..height-37)
764 // resize knob at top-right, size is 20x20
767 override bool onKeyPost (KeyEvent event) {
768 if (event == "C-Q") { if (event.pressed) postQuitEvent(); return true; }
769 return false;
772 override bool onMousePost (MouseEvent event) {
773 setListClip();
774 if (gxClipRect.inside(event.x, event.y)) {
775 normTop();
776 scope(exit) normTop();
777 if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.wheelUp) {
778 if (curitem > 0) --curitem;
779 return true;
781 if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.wheelDown) {
782 ++curitem;
783 return true;
786 return false;