double mouse event call bug
[amper.git] / amperskin.d
blobfdd0aac0de04d863b928f20ec0ae246ada339988
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 skinMainImg;
41 __gshared EImage skinMainCButtons;
42 __gshared EImage skinMainShufRep;
45 // ////////////////////////////////////////////////////////////////////////// //
46 __gshared AmpMainWindow ampMain;
49 // ////////////////////////////////////////////////////////////////////////// //
50 void loadSkin () {
51 static bool isGoodImageExtension (ConString fname) {
52 auto epos = fname.lastIndexOf('.');
53 if (epos < 0) return false;
54 fname = fname[epos..$];
55 return
56 fname.strEquCI(".bmp") ||
57 fname.strEquCI(".png") ||
58 fname.strEquCI(".jpg") ||
59 fname.strEquCI(".jpeg") ||
60 false;
63 void loadImage (EImage* img, string fname) {
64 auto epos = fname.lastIndexOf('.');
65 if (epos < 0) throw new Exception("invalid skin image '"~fname~"'");
66 auto ext = fname[epos..$];
68 auto fl = VFile(fname);
69 auto sz = fl.size;
70 if (sz < 1 || sz > 1024*1024*8) throw new Exception("invalid skin image '"~fname~"'");
72 auto data = new ubyte[](cast(uint)sz);
73 scope(exit) delete data;
74 fl.rawReadExact(data);
76 MemoryImage ximg;
77 scope(exit) delete ximg;
79 if (ext.strEquCI(".bmp")) ximg = readBmp(data);
80 else if (ext.strEquCI(".png")) ximg = imageFromPng(readPng(data));
81 else if (ext.strEquCI(".jpg") || ext.strEquCI(".jpeg")) ximg = readJpegFromMemory(data);
82 else throw new Exception("unknown image format for '"~fname~"'");
83 assert(ximg !is null);
85 *img = new EImage(ximg);
88 ampMain = null;
90 static struct ImageInfo {
91 string name;
92 EImage* img;
94 ImageInfo[$] imglist = [
95 ImageInfo("main", &skinMainImg),
96 ImageInfo("cbuttons", &skinMainCButtons),
97 ImageInfo("shufrep", &skinMainShufRep),
100 foreach (ref ii; imglist) { *(ii.img) = null; }
102 auto vid = vfsAddPak(skinfile, "skin:");
103 scope(exit) vfsRemovePak(vid);
104 foreach (auto de; vfsFileList) {
105 if (!isGoodImageExtension(de.name)) continue;
107 auto fname = de.name;
108 auto xpos = fname.lastIndexOf('/');
109 if (xpos >= 0) fname = fname[xpos+1..$];
110 else if (fname.startsWith("skin:")) fname = fname[5..$];
111 xpos = fname.lastIndexOf('.');
112 if (xpos <= 0) continue;
113 auto iname = fname[0..xpos];
114 //conwriteln("[", de.name, "]:[", fname, "]:[", iname, "]");
116 foreach (ref ii; imglist) {
117 if (*(ii.img) is null && iname.strEquCI(ii.name)) {
118 conwriteln(de.name);
119 loadImage(ii.img, de.name);
120 break;
125 foreach (ref ii; imglist) if (*(ii.img) is null) throw new Exception("invalid skin (missing '"~ii.name~"' image)");
127 ampMain = new AmpMainWindow();
131 // ////////////////////////////////////////////////////////////////////////// //
132 final class EImage {
133 int width;
134 int height;
135 uint[] data;
137 this (MemoryImage img) {
138 if (img is null) throw new Exception("can't create eimage from nothing");
139 width = img.width;
140 height = img.height;
141 data.length = width*height;
142 { import core.memory : GC; GC.setAttr(data.ptr, GC.BlkAttr.NO_INTERIOR|GC.BlkAttr.NO_SCAN); }
143 foreach (immutable int dy; 0..img.height) {
144 foreach (immutable int dx; 0..img.width) {
145 auto cc = img.getPixel(dx, dy);
146 data[dy*width+dx] = gxrgb(cc.r, cc.g, cc.b);
151 final void blitAt (int x, int y) nothrow @trusted @nogc {
153 if (x < 0 || y < 0 || x+width > VBufWidth || y+height > VBufHeight) return;
154 const(uint)* src = data.ptr;
155 uint* dst = vglTexBuf+y*VBufWidth+x;
156 immutable int w = width;
157 foreach (immutable dy; 0..height) {
158 dst[0..w] = src[0..w];
159 src += width;
160 dst += VBufWidth;
163 blitRect(x, y, GxRect(0, 0, width, height));
166 final void blitRect (int destx, int desty, GxRect srect) nothrow @trusted @nogc {
167 srect.intersect(GxRect(0, 0, width, height));
168 if (srect.empty) return;
169 assert(srect.x0 >= 0 && srect.x0 < width);
170 assert(srect.y0 >= 0 && srect.y0 < height);
171 assert(srect.x1 >= 0 && srect.x1 < width);
172 assert(srect.y1 >= 0 && srect.y1 < height);
173 int dx = destx;
174 int dy = desty;
175 int dw = srect.width;
176 int dh = srect.height;
177 int skipLeft, skipTop;
178 if (!gxClipRect.clipHVStripes(dx, dy, dw, dh, &skipLeft, &skipTop)) return;
179 if (!GxRect(0, 0, VBufWidth, VBufHeight).clipHVStripes(dx, dy, dw, dh, &skipLeft, &skipTop)) return;
180 assert(dw > 0 && dw <= width);
181 assert(dh > 0 && dh <= height);
182 assert(skipLeft+srect.x0+dw <= width);
183 assert(skipTop+srect.y0+dh <= height);
184 const(uint)* src = data.ptr+(skipTop+srect.y0)*width+(skipLeft+srect.x0);
185 uint* dst = vglTexBuf+dy*VBufWidth+dx;
186 while (dh-- > 0) {
187 assert(src+dw <= data.ptr+width*height);
188 assert(dst+dw <= vglTexBuf+VBufWidth*VBufHeight);
189 dst[0..dw] = src[0..dw];
190 src += width;
191 dst += VBufWidth;
197 // ////////////////////////////////////////////////////////////////////////// //
198 class AmpWindow {
199 EImage img;
200 GxRect imgrc;
201 AmpWidget[] widgets;
202 int mActiveWidget = -1;
204 this (EImage aimg) {
205 if (aimg is null) throw new Exception("no image for window");
206 img = aimg;
207 imgrc = GxRect(0, 0, img.width, img.height);
210 AmpWidget addWidget (AmpWidget w) {
211 if (w is null) return null;
212 if (w.parent !is null) throw new Exception("widget already owned");
213 widgets ~= w;
214 w.parent = this;
215 return w;
218 final @property AmpWidget activeWidget () pure nothrow @safe @nogc { pragma(inline, true); return (mActiveWidget >= 0 && mActiveWidget < widgets.length ? widgets[mActiveWidget] : null); }
220 final @property void activeWidget (AmpWidget w) pure nothrow @safe @nogc {
221 if (w is null || w.parent !is this) { mActiveWidget = -1; return; }
222 foreach (immutable idx, AmpWidget ww; widgets) if (ww is w) { mActiveWidget = cast(int)idx; return; }
223 mActiveWidget = -1;
226 final AmpWidget widgetAt (int x, int y) pure nothrow @safe @nogc {
227 foreach_reverse (AmpWidget w; widgets) {
228 if (w.rc.inside(x, y)) return w;
230 return null;
233 void onPaint () {
234 img.blitAt(0, 0);
235 foreach (AmpWidget w; widgets) w.onPaint();
238 bool onKeyPre (KeyEvent event) { return false; }
240 bool onKeyPost (KeyEvent event) {
241 if (event == "C-Q") { if (event.pressed) postQuitEvent(); return true; }
242 return false;
245 bool onKey (KeyEvent event) {
246 if (onKeyPre(event)) return true;
247 if (auto aw = activeWidget) {
248 if (aw.onKey(event)) return true;
250 if (onKeyPost(event)) return true;
251 return false;
254 bool onMousePre (MouseEvent event) { return false; }
255 bool onMousePost (MouseEvent event) { return false; }
257 bool onMouse (MouseEvent event) {
258 bool wasAwOrAct = false;
259 if (onMousePre(event)) return true;
260 auto aw = activeWidget;
261 if (aw !is null) {
262 if (aw.onMouse(event)) return true;
263 wasAwOrAct = true;
265 if (auto ww = widgetAt(event.x, event.y)) {
266 if (ww !is aw) {
267 if (ww.onMouse(event)) return true;
268 wasAwOrAct = true;
271 if (onMousePost(event)) return true;
272 if (wasAwOrAct && event.type != MouseEventType.motion) return true;
273 return false;
278 // ////////////////////////////////////////////////////////////////////////// //
279 class AmpWidget {
280 AmpWindow parent;
281 EImage img;
282 GxRect rc;
283 GxRect imgrc;
284 string cmd;
286 void delegate () onAction;
288 protected this () {}
290 this (EImage aimg, int x, int y, GxRect aimgrc, string acmd=null) {
291 if (aimg is null) throw new Exception("no image for window");
292 img = aimg;
293 imgrc = aimgrc;
294 rc = GxRect(x, y, aimgrc.width, aimgrc.height);
295 cmd = acmd;
298 void onPaint () {
299 bool active = (parent !is null && parent.activeWidget is this);
300 auto irc = imgrc;
301 if (active) irc.moveBy(0, imgrc.height);
302 img.blitRect(rc.x0, rc.y0, irc);
305 bool onKey (KeyEvent event) {
306 return false;
309 bool onMouse (MouseEvent event) {
310 if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) {
311 parent.activeWidget = this;
312 return true;
314 if (event.type == MouseEventType.buttonReleased && event.button == MouseButton.left) {
315 if (parent.activeWidget is this) {
316 parent.activeWidget = null;
317 if (rc.inside(event.x, event.y)) {
318 concmd(cmd);
319 if (onAction !is null) onAction();
321 return true;
324 return false;
329 // ////////////////////////////////////////////////////////////////////////// //
330 class AmpWidgetToggle : AmpWidget {
331 bool* checked;
332 private bool checkedvar;
334 this (bool* cvp, EImage aimg, int x, int y, GxRect aimgrc, string acmd=null) {
335 super(aimg, x, y, aimgrc, acmd);
336 if (cvp is null) cvp = &checkedvar;
337 checked = cvp;
340 override void onPaint () {
341 bool active = (parent !is null && parent.activeWidget is this);
342 auto irc = imgrc;
343 if (*checked) irc.moveBy(0, imgrc.height*2);
344 if (active) irc.moveBy(0, imgrc.height);
345 img.blitRect(rc.x0, rc.y0, irc);
350 // ////////////////////////////////////////////////////////////////////////// //
351 class AmpWidgetSongTitle : AmpWidget {
352 int titleofs = 0;
353 int titleofsmovedir = 1;
354 int titlemovepause = 8;
355 string title;
356 bool dragging = false;
358 this(T:const(char)[]) (GxRect arc, T atitle) {
359 static if (is(T == typeof(null))) title = null;
360 else static if (is(T == string)) title = atitle;
361 else title = atitle.idup;
362 super();
363 rc = arc;
366 override void onPaint () {
367 if (title.length) {
368 gxClipRect = rc;
369 gxDrawTextUtf(rc.x0-titleofs, rc.y0-2, title, gxRGB!(0, 255, 0));
373 final void scrollTitle () {
374 if (parent.activeWidget !is this) dragging = false;
375 if (dragging) return;
376 if (titlemovepause-- >= 0) return;
377 titlemovepause = 0;
378 if (title.length == 0) { titleofs = 0; titleofsmovedir = 1; titlemovepause = 8; return; }
379 auto tw = gxTextWidthUtf(title);
380 titleofs += titleofsmovedir;
381 if (titleofs <= 0) { titleofs = 0; titleofsmovedir = 1; titlemovepause = 8; }
382 else if (titleofs >= tw-rc.width) { titleofs = tw-rc.width; titleofsmovedir = -1; titlemovepause = 8; }
385 override bool onKey (KeyEvent event) {
386 return false;
389 override bool onMouse (MouseEvent event) {
390 if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) {
391 parent.activeWidget = this;
392 dragging = true;
393 return true;
395 if (event.type == MouseEventType.buttonReleased && event.button == MouseButton.left) {
396 if (parent.activeWidget is this) {
397 dragging = false;
398 if (title.length != 0) {
399 titlemovepause = 10;
400 auto tw = gxTextWidthUtf(title);
401 if (titleofs <= 0) { titleofs = 0; titleofsmovedir = 1; }
402 else if (titleofs >= tw-rc.width) { titleofs = tw-rc.width; titleofsmovedir = -1; }
404 parent.activeWidget = null;
405 return true;
408 if (dragging && event.type == MouseEventType.motion && event.modifierState&ModifierState.leftButtonDown) {
409 if (title.length != 0 && event.dx != 0) {
410 titleofs -= event.dx;
411 //conwriteln("dx=", event.dx, "; titleofs=", titleofs);
412 titleofsmovedir = (event.dx < 0 ? 1 : -1);
413 auto tw = gxTextWidthUtf(title);
414 if (titleofs <= 0) titleofs = 0;
415 else if (titleofs >= tw-rc.width) titleofs = tw-rc.width;
418 return false;
423 // ////////////////////////////////////////////////////////////////////////// //
424 class AmpMainWindow : AmpWindow {
425 AmpWidgetSongTitle wtitle;
427 this () {
428 super(skinMainImg);
429 addWidget(new AmpWidget(skinMainCButtons, 16, 88, GxRect(0, 0, 23, 18), "song_prev"));
430 addWidget(new AmpWidget(skinMainCButtons, 39, 88, GxRect(23, 0, 23, 18), "song_play"));
431 addWidget(new AmpWidget(skinMainCButtons, 62, 88, GxRect(46, 0, 23, 18), "song_pause"));
432 addWidget(new AmpWidget(skinMainCButtons, 85, 88, GxRect(69, 0, 23, 18), "song_stop"));
433 addWidget(new AmpWidget(skinMainCButtons, 108, 88, GxRect(92, 0, 22, 18), "song_next"));
434 addWidget(new AmpWidget(skinMainCButtons, 136, 88, GxRect(114, 0, 22, 16), "song_eject"));
435 addWidget(new AmpWidgetToggle(&modeShuffle, skinMainShufRep, 164, 89, GxRect(28, 0, 46, 15), "mode_shuffle toggle"));
436 addWidget(new AmpWidgetToggle(&modeRepeat, skinMainShufRep, 210, 89, GxRect(0, 0, 28, 15), "mode_repeat toggle"));
437 wtitle = cast(AmpWidgetSongTitle)addWidget(new AmpWidgetSongTitle(GxRect(109, 24, 157, 12), "Testing text... жопф дохуя текста"));
440 final void scrollTitle () {
441 if (wtitle !is null) wtitle.scrollTitle();