more playlist code
[amper.git] / amperskin.d
blobdd4b69e18dbb158fad34315c6d352b2559c361b8
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;
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;
47 // ////////////////////////////////////////////////////////////////////////// //
48 __gshared AmpMainWindow ampMain;
49 __gshared AmpPListWindow ampPList;
52 // ////////////////////////////////////////////////////////////////////////// //
53 void loadSkin () {
54 static bool isGoodImageExtension (ConString fname) {
55 auto epos = fname.lastIndexOf('.');
56 if (epos < 0) return false;
57 fname = fname[epos..$];
58 return
59 fname.strEquCI(".bmp") ||
60 fname.strEquCI(".png") ||
61 fname.strEquCI(".jpg") ||
62 fname.strEquCI(".jpeg") ||
63 false;
66 void loadImage (EImage* img, string fname) {
67 auto epos = fname.lastIndexOf('.');
68 if (epos < 0) throw new Exception("invalid skin image '"~fname~"'");
69 auto ext = fname[epos..$];
71 auto fl = VFile(fname);
72 auto sz = fl.size;
73 if (sz < 1 || sz > 1024*1024*8) throw new Exception("invalid skin image '"~fname~"'");
75 auto data = new ubyte[](cast(uint)sz);
76 scope(exit) delete data;
77 fl.rawReadExact(data);
79 MemoryImage ximg;
80 scope(exit) delete ximg;
82 if (ext.strEquCI(".bmp")) ximg = readBmp(data);
83 else if (ext.strEquCI(".png")) ximg = imageFromPng(readPng(data));
84 else if (ext.strEquCI(".jpg") || ext.strEquCI(".jpeg")) ximg = readJpegFromMemory(data);
85 else throw new Exception("unknown image format for '"~fname~"'");
86 assert(ximg !is null);
88 *img = new EImage(ximg);
91 AmpPListWindow.SavedData pldata;
92 if (ampPList !is null) pldata = ampPList.saveData();
93 scope(exit) if (ampPList !is null) ampPList.restoreData(pldata);
95 ampMain = null;
96 ampPList = null;
98 EImage numsex;
100 static struct ImageInfo {
101 string name;
102 EImage* img;
104 ImageInfo[$] imglist = [
105 ImageInfo("main", &skinImgMain),
106 ImageInfo("cbuttons", &skinImgCButtons),
107 ImageInfo("shufrep", &skinImgShufRep),
108 ImageInfo("numbers", &numsex),
109 ImageInfo("nums_ex", &skinImgNums),
110 ImageInfo("pledit", &skinImgPlaylist),
113 foreach (ref ii; imglist) { *(ii.img) = null; }
115 auto vid = vfsAddPak(skinfile, "skin:");
116 scope(exit) vfsRemovePak(vid);
117 foreach (auto de; vfsFileList) {
118 if (!isGoodImageExtension(de.name)) continue;
120 auto fname = de.name;
121 auto xpos = fname.lastIndexOf('/');
122 if (xpos >= 0) fname = fname[xpos+1..$];
123 else if (fname.startsWith("skin:")) fname = fname[5..$];
124 xpos = fname.lastIndexOf('.');
125 if (xpos <= 0) continue;
126 auto iname = fname[0..xpos];
127 //conwriteln("[", de.name, "]:[", fname, "]:[", iname, "]");
129 foreach (ref ii; imglist) {
130 if (*(ii.img) is null && iname.strEquCI(ii.name)) {
131 //conwriteln(de.name);
132 loadImage(ii.img, de.name);
133 break;
138 if (skinImgNums is null) {
139 foreach (ref ii; imglist) if (ii.name == "numbers" && *(ii.img) !is null) skinImgNums = *(ii.img);
142 foreach (ref ii; imglist) {
143 if (ii.name == "numbers" && *(ii.img) is null) *(ii.img) = skinImgNums;
144 if (ii.name == "nums_ex" && *(ii.img) is null) *(ii.img) = numsex;
145 if (*(ii.img) is null) throw new Exception("invalid skin (missing '"~ii.name~"' image)");
148 ampMain = new AmpMainWindow();
149 ampPList = new AmpPListWindow();
153 // ////////////////////////////////////////////////////////////////////////// //
154 final class EImage {
155 int width;
156 int height;
157 uint[] data;
159 this (MemoryImage img) {
160 if (img is null) throw new Exception("can't create eimage from nothing");
161 width = img.width;
162 height = img.height;
163 data.length = width*height;
164 { import core.memory : GC; GC.setAttr(data.ptr, GC.BlkAttr.NO_INTERIOR|GC.BlkAttr.NO_SCAN); }
165 foreach (immutable int dy; 0..img.height) {
166 foreach (immutable int dx; 0..img.width) {
167 auto cc = img.getPixel(dx, dy);
168 data[dy*width+dx] = gxrgb(cc.r, cc.g, cc.b);
173 final void blitAt (int x, int y) nothrow @trusted @nogc {
175 if (x < 0 || y < 0 || x+width > VBufWidth || y+height > VBufHeight) return;
176 const(uint)* src = data.ptr;
177 uint* dst = vglTexBuf+y*VBufWidth+x;
178 immutable int w = width;
179 foreach (immutable dy; 0..height) {
180 dst[0..w] = src[0..w];
181 src += width;
182 dst += VBufWidth;
185 blitRect(x, y, GxRect(0, 0, width, height));
188 final void blitRect (int destx, int desty, GxRect srect) nothrow @trusted @nogc {
189 srect.intersect(GxRect(0, 0, width, height));
190 if (srect.empty) return;
191 assert(srect.x0 >= 0 && srect.x0 < width);
192 assert(srect.y0 >= 0 && srect.y0 < height);
193 assert(srect.x1 >= 0 && srect.x1 < width);
194 assert(srect.y1 >= 0 && srect.y1 < height);
195 int dx = destx;
196 int dy = desty;
197 int dw = srect.width;
198 int dh = srect.height;
199 int skipLeft, skipTop;
200 if (!gxClipRect.clipHVStripes(dx, dy, dw, dh, &skipLeft, &skipTop)) return;
201 if (!GxRect(0, 0, VBufWidth, VBufHeight).clipHVStripes(dx, dy, dw, dh, &skipLeft, &skipTop)) return;
202 assert(dw > 0 && dw <= width);
203 assert(dh > 0 && dh <= height);
204 assert(skipLeft+srect.x0+dw <= width);
205 assert(skipTop+srect.y0+dh <= height);
206 const(uint)* src = data.ptr+(skipTop+srect.y0)*width+(skipLeft+srect.x0);
207 uint* dst = vglTexBuf+dy*VBufWidth+dx;
208 while (dh-- > 0) {
209 assert(src+dw <= data.ptr+width*height);
210 assert(dst+dw <= vglTexBuf+VBufWidth*VBufHeight);
211 dst[0..dw] = src[0..dw];
212 src += width;
213 dst += VBufWidth;
219 // ////////////////////////////////////////////////////////////////////////// //
220 class AmpWindow {
221 EImage img;
222 GxRect imgrc;
223 AmpWidget[] widgets;
224 int mActiveWidget = -1;
226 this (EImage aimg) {
227 if (aimg is null) throw new Exception("no image for window");
228 img = aimg;
229 imgrc = GxRect(0, 0, img.width, img.height);
232 AmpWidget addWidget (AmpWidget w) {
233 if (w is null) return null;
234 if (w.parent !is null) throw new Exception("widget already owned");
235 widgets ~= w;
236 w.parent = this;
237 return w;
240 final @property AmpWidget activeWidget () pure nothrow @safe @nogc { pragma(inline, true); return (mActiveWidget >= 0 && mActiveWidget < widgets.length ? widgets[mActiveWidget] : null); }
242 final @property void activeWidget (AmpWidget w) pure nothrow @safe @nogc {
243 if (w is null || w.parent !is this) { mActiveWidget = -1; return; }
244 foreach (immutable idx, AmpWidget ww; widgets) if (ww is w) { mActiveWidget = cast(int)idx; return; }
245 mActiveWidget = -1;
248 final AmpWidget widgetAt (int x, int y) pure nothrow @safe @nogc {
249 foreach_reverse (AmpWidget w; widgets) {
250 if (w.rc.inside(x, y)) return w;
252 return null;
255 void onPaint () {
256 img.blitAt(0, 0);
257 foreach (AmpWidget w; widgets) w.onPaint();
260 bool onKeyPre (KeyEvent event) { return false; }
262 bool onKeyPost (KeyEvent event) {
263 if (event == "C-Q") { if (event.pressed) postQuitEvent(); return true; }
264 return false;
267 bool onKey (KeyEvent event) {
268 if (onKeyPre(event)) return true;
269 if (auto aw = activeWidget) {
270 if (aw.onKey(event)) return true;
272 if (onKeyPost(event)) return true;
273 return false;
276 bool onMousePre (MouseEvent event) { return false; }
277 bool onMousePost (MouseEvent event) { return false; }
279 bool onMouse (MouseEvent event) {
280 bool wasAwOrAct = false;
281 if (onMousePre(event)) return true;
282 auto aw = activeWidget;
283 if (aw !is null) {
284 if (aw.onMouse(event)) return true;
285 wasAwOrAct = true;
287 if (auto ww = widgetAt(event.x, event.y)) {
288 if (ww !is aw) {
289 if (ww.onMouse(event)) return true;
290 wasAwOrAct = true;
293 if (onMousePost(event)) return true;
294 if (wasAwOrAct && event.type != MouseEventType.motion) return true;
295 return false;
300 // ////////////////////////////////////////////////////////////////////////// //
301 class AmpWidget {
302 AmpWindow parent;
303 EImage img;
304 GxRect rc;
305 GxRect imgrc;
306 string cmd;
308 void delegate () onAction;
310 protected this () {}
312 this (EImage aimg, int x, int y, GxRect aimgrc, string acmd=null) {
313 if (aimg is null) throw new Exception("no image for window");
314 img = aimg;
315 imgrc = aimgrc;
316 rc = GxRect(x, y, aimgrc.width, aimgrc.height);
317 cmd = acmd;
320 void onPaint () {
321 bool active = (parent !is null && parent.activeWidget is this);
322 auto irc = imgrc;
323 if (active) irc.moveBy(0, imgrc.height);
324 img.blitRect(rc.x0, rc.y0, irc);
327 bool onKey (KeyEvent event) {
328 return false;
331 bool onMouse (MouseEvent event) {
332 if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) {
333 parent.activeWidget = this;
334 return true;
336 if (event.type == MouseEventType.buttonReleased && event.button == MouseButton.left) {
337 if (parent.activeWidget is this) {
338 parent.activeWidget = null;
339 if (rc.inside(event.x, event.y)) {
340 concmd(cmd);
341 if (onAction !is null) onAction();
343 return true;
346 return false;
351 // ////////////////////////////////////////////////////////////////////////// //
352 class AmpWidgetToggle : AmpWidget {
353 bool* checked;
354 private bool checkedvar;
356 this (bool* cvp, EImage aimg, int x, int y, GxRect aimgrc, string acmd=null) {
357 super(aimg, x, y, aimgrc, acmd);
358 if (cvp is null) cvp = &checkedvar;
359 checked = cvp;
362 override void onPaint () {
363 bool active = (parent !is null && parent.activeWidget is this);
364 auto irc = imgrc;
365 if (*checked) irc.moveBy(0, imgrc.height*2);
366 if (active) irc.moveBy(0, imgrc.height);
367 img.blitRect(rc.x0, rc.y0, irc);
372 // ////////////////////////////////////////////////////////////////////////// //
373 class AmpWidgetSongTitle : AmpWidget {
374 int titleofs = 0;
375 int titleofsmovedir = 1;
376 int titlemovepause = 8;
377 string title;
378 bool dragging = false;
380 this(T:const(char)[]) (GxRect arc, T atitle) {
381 static if (is(T == typeof(null))) title = null;
382 else static if (is(T == string)) title = atitle;
383 else title = atitle.idup;
384 super();
385 rc = arc;
388 override void onPaint () {
389 if (title.length) {
390 gxClipRect = rc;
391 gxDrawTextUtf(rc.x0-titleofs, rc.y0-2, title, gxRGB!(0, 255, 0));
395 final void scrollTitle () {
396 if (parent.activeWidget !is this) dragging = false;
397 if (dragging) return;
398 if (titlemovepause-- >= 0) return;
399 titlemovepause = 0;
400 if (title.length == 0) { titleofs = 0; titleofsmovedir = 1; titlemovepause = 8; return; }
401 auto tw = gxTextWidthUtf(title);
402 titleofs += titleofsmovedir;
403 if (titleofs <= 0) { titleofs = 0; titleofsmovedir = 1; titlemovepause = 8; }
404 else if (titleofs >= tw-rc.width) { titleofs = tw-rc.width; titleofsmovedir = -1; titlemovepause = 8; }
407 override bool onKey (KeyEvent event) {
408 return false;
411 override bool onMouse (MouseEvent event) {
412 if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) {
413 parent.activeWidget = this;
414 dragging = true;
415 return true;
417 if (event.type == MouseEventType.buttonReleased && event.button == MouseButton.left) {
418 if (parent.activeWidget is this) {
419 dragging = false;
420 if (title.length != 0) {
421 titlemovepause = 10;
422 auto tw = gxTextWidthUtf(title);
423 if (titleofs <= 0) { titleofs = 0; titleofsmovedir = 1; }
424 else if (titleofs >= tw-rc.width) { titleofs = tw-rc.width; titleofsmovedir = -1; }
426 parent.activeWidget = null;
427 return true;
430 if (dragging && event.type == MouseEventType.motion && event.modifierState&ModifierState.leftButtonDown) {
431 if (title.length != 0 && event.dx != 0) {
432 titleofs -= event.dx;
433 //conwriteln("dx=", event.dx, "; titleofs=", titleofs);
434 titleofsmovedir = (event.dx < 0 ? 1 : -1);
435 auto tw = gxTextWidthUtf(title);
436 if (titleofs <= 0) titleofs = 0;
437 else if (titleofs >= tw-rc.width) titleofs = tw-rc.width;
440 return false;
445 // ////////////////////////////////////////////////////////////////////////// //
446 class AmpMainWindow : AmpWindow {
447 AmpWidgetSongTitle wtitle;
449 this () {
450 super(skinImgMain);
451 addWidget(new AmpWidget(skinImgCButtons, 16, 88, GxRect(0, 0, 23, 18), "song_prev"));
452 addWidget(new AmpWidget(skinImgCButtons, 39, 88, GxRect(23, 0, 23, 18), "song_play"));
453 addWidget(new AmpWidget(skinImgCButtons, 62, 88, GxRect(46, 0, 23, 18), "song_pause"));
454 addWidget(new AmpWidget(skinImgCButtons, 85, 88, GxRect(69, 0, 23, 18), "song_stop"));
455 addWidget(new AmpWidget(skinImgCButtons, 108, 88, GxRect(92, 0, 22, 18), "song_next"));
456 addWidget(new AmpWidget(skinImgCButtons, 136, 88, GxRect(114, 0, 22, 16), "song_eject"));
457 addWidget(new AmpWidgetToggle(&modeShuffle, skinImgShufRep, 164, 89, GxRect(28, 0, 46, 15), "mode_shuffle toggle"));
458 addWidget(new AmpWidgetToggle(&modeRepeat, skinImgShufRep, 210, 89, GxRect(0, 0, 28, 15), "mode_repeat toggle"));
459 wtitle = cast(AmpWidgetSongTitle)addWidget(new AmpWidgetSongTitle(GxRect(109, 24, 157, 12), "Testing text... жопф дохуя текста"));
462 final void scrollTitle () {
463 if (wtitle !is null) wtitle.scrollTitle();
468 // ////////////////////////////////////////////////////////////////////////// //
469 struct PListItem {
470 string title;
471 string fname;
472 int duration; // in seconds
476 class AmpPListWindow : AmpWindow {
477 static struct SavedData {
478 PListItem[] items;
479 int totaldur = 0;
480 int topitem = 0;
481 int curitem = 0;
484 PListItem[] items;
485 int totaldur = 0;
486 int topitem = 0;
487 int curitem = 0;
489 public:
490 this () {
491 super(skinImgMain);
492 img = skinImgPlaylist;
495 auto saveData () {
496 SavedData res;
497 res.items = items;
498 res.totaldur = totaldur;
499 res.topitem = topitem;
500 res.curitem = curitem;
501 return res;
504 void restoreData (ref SavedData sd) {
505 items = sd.items;
506 totaldur = sd.totaldur;
507 topitem = sd.topitem;
508 curitem = sd.curitem;
511 void appendListItem (PListItem li) {
512 if (li.duration > 0) totaldur += li.duration;
513 auto optr = items.ptr;
514 items ~= li;
515 // check if runtime allocated a new memory block due to array growth
516 if (items.ptr !is optr) {
517 import core.memory : GC;
518 // check if our array really *IS* the head
519 if (items.ptr is GC.addrOf(items.ptr)) {
520 // set the magic flag!
521 GC.setAttr(items.ptr, GC.BlkAttr.NO_INTERIOR);
526 final @property int visibleFullItems () const nothrow @trusted {
527 int hgt = imgrc.height-20-38;
528 int res = hgt/gxTextHeightUtf;
529 if (res < 1) res = 1;
530 return res;
533 final @property int visibleItems () const nothrow @trusted {
534 int hgt = imgrc.height-20-38;
535 int res = hgt/gxTextHeightUtf;
536 if (hgt%gxTextHeightUtf) ++res;
537 if (res < 1) res = 1;
538 return res;
541 void normTop () nothrow @trusted {
542 int tl = topitem;
543 if (tl < 0) tl = 0;
544 int el = topitem+visibleItems-1;
545 if (el >= items.length) {
546 tl = cast(int)items.length-visibleItems;
547 if (tl < 0) tl = 0;
549 topitem = tl;
552 final @property int knobYOfs () nothrow @trusted {
553 normTop();
554 if (items.length < 2) return 0;
555 int hgt = imgrc.height-37-19-18;
556 if (hgt < 1) return 0;
557 return cast(int)(hgt*topitem/(items.length-1));
558 // normal knob: 52,53 8x18 at width-15,[19..height-37)
561 final void paintScrollKnob () {
562 int xofs = imgrc.width-15;
563 int yofs = knobYOfs+19;
564 img.blitRect(xofs, yofs, GxRect(52, 53, 8, 18));
567 final void paintList () {
568 char[1024] buf = void;
569 gxClipRect = GxRect(9, 20, imgrc.width-9-20, imgrc.height-20-38);
570 normTop();
571 if (items.length < 1) return;
572 int ty = 20;
573 int idx = topitem;
574 assert(idx >= 0);
575 while (idx < items.length && ty < imgrc.height-38) {
576 import core.stdc.stdio : snprintf;
577 auto len = snprintf(buf.ptr, buf.length, "%d. %.*s", idx+1, cast(uint)items.ptr[idx].title.length, items.ptr[idx].title.ptr);
578 if (curitem == idx) gxFillRect(9, ty, gxClipRect.width, gxTextHeightUtf, gxRGB!(0, 0, 120));
579 gxDrawTextUtf(9, ty, buf[0..len], gxRGB!(0, 255, 0));
580 ty += gxTextHeightUtf;
581 ++idx;
585 override void onPaint () {
586 int tx, ty;
587 gxFillRect(imgrc, gxRGB!(0, 0, 0));
588 bool active = true; //(parent !is null && parent.activeWidget is this);
589 auto irc = GxRect(0, 0, 25, 20);
590 if (!active) irc.moveBy(0, 21);
591 // top frame
592 // tiles
593 irc.x0 = 127;
594 tx = irc.width;
595 while (tx+irc.width <= imgrc.width-irc.width) {
596 img.blitRect(tx, 0, irc);
597 tx += irc.width;
599 // top title
600 auto irtitle = irc;
601 irtitle.x0 = 26;
602 irtitle.width = 100;
603 img.blitRect((imgrc.width-irtitle.width)/2, 0, irtitle);
604 // top left corner
605 irc.x0 = 0;
606 img.blitRect(0, 0, irc);
607 // top right corner
608 irc.x0 = 153;
609 img.blitRect(imgrc.width-irc.width, 0, irc);
610 // left and right bars
611 ty = irc.height;
612 irc.width = 25;
613 irc.height = 29;
614 irc.y0 = 42;
615 while (ty+irc.height <= imgrc.height-irc.height) {
616 // left bar
617 irc.x0 = 0;
618 img.blitRect(0, ty, irc);
619 // right bar
620 irc.x0 = 26;
621 img.blitRect(imgrc.width-irc.width, ty, irc);
622 ty += irc.height;
624 // bottom frame
625 // left part: 0,72 125x38
626 // right part: 126,72 150x38
627 // tile: 179,0 25x38
628 irc.height = 38;
629 ty = imgrc.height-irc.height;
630 // tiles
631 irc.width = 25;
632 irc.x0 = 179;
633 irc.y0 = 0;
634 tx = 125;
635 while (tx+irc.width <= imgrc.width-125-150) {
636 img.blitRect(tx, ty, irc);
637 tx += irc.width;
639 // bottom left corner
640 irc.x0 = 0;
641 irc.y0 = 72;
642 irc.width = 125;
643 img.blitRect(0, ty, irc);
644 // bottom right corner
645 irc.x0 = 126;
646 irc.width = 150;
647 img.blitRect(imgrc.width-irc.width, ty, irc);
648 // scrollbar
649 paintScrollKnob();
650 paintList();
651 gxClipReset();
652 // widgets
653 foreach (AmpWidget w; widgets) w.onPaint();
654 // close pressed: 52,42 9x9 at width-10
655 // normal knob: 52,53 8x18 at width-15,[19..height-37)
656 // pressed knob: 61,53 8x18 at width-15,[19..height-37)
657 // resize knob at top-right, size is 20x20