1 module amperskin
is aliced
;
6 import arsd
.simpledisplay
;
16 // ////////////////////////////////////////////////////////////////////////// //
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 // ////////////////////////////////////////////////////////////////////////// //
58 static struct KeyValue
{
61 nothrow @trusted @nogc:
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
] == '=') {
69 if (pos
< s
.length
&& s
.ptr
[pos
] <= ' ') ++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
;
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
;
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
;
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
) {
103 if (line
.length
== 0 || line
.ptr
[0] == ';') continue;
104 if (line
[0] == '[') {
105 inTextSection
= line
.strEquCI("[Text]");
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
..$];
125 fname
.strEquCI(".bmp") ||
126 fname
.strEquCI(".png") ||
127 fname
.strEquCI(".jpg") ||
128 fname
.strEquCI(".jpeg") ||
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
);
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
);
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
);
164 skinPListColNormal
= gxRGB
!(0, 255, 0);
165 skinPListColCurrent
= gxRGB
!(255, 255, 255);
166 skinPListColNormalBG
= gxRGB
!(0, 0, 0);
167 skinPListColCurrentBG
= gxRGB
!(0, 0, 255);
171 static struct ImageInfo
{
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
));
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
);
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 // ////////////////////////////////////////////////////////////////////////// //
235 this (MemoryImage img
) {
236 if (img
is null) throw new Exception("can't create eimage from nothing");
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];
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
);
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
;
285 assert(src
+dw <= data
.ptr
+width
*height
);
286 assert(dst
+dw <= vglTexBuf
+VBufWidth
*VBufHeight
);
287 dst
[0..dw] = src
[0..dw];
295 // ////////////////////////////////////////////////////////////////////////// //
301 int mActiveWidget
= -1;
304 if (aimg
is null) throw new Exception("no image for window");
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");
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; }
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
;
348 foreach (AmpWidget w
; widgets
) {
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;
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
;
375 if (aw
.onMouse(event
)) return true;
378 if (auto ww
= widgetAt(event
.x
, event
.y
)) {
380 if (ww
.onMouse(event
)) return true;
384 if (onMousePost(event
)) return true;
385 if (wasAwOrAct
&& event
.type
!= MouseEventType
.motion
) return true;
391 // ////////////////////////////////////////////////////////////////////////// //
399 void delegate () onAction
;
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");
407 rc
= GxRect(x
, y
, aimgrc
.width
, aimgrc
.height
);
412 bool active
= (parent
!is null && parent
.activeWidget
is this);
414 if (active
) irc
.moveBy(0, imgrc
.height
);
415 img
.blitRect(rc
.x0
, rc
.y0
, irc
);
418 bool onKey (KeyEvent event
) {
422 bool onMouse (MouseEvent event
) {
423 if (event
.type
== MouseEventType
.buttonPressed
&& event
.button
== MouseButton
.left
) {
424 parent
.activeWidget
= this;
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
)) {
432 if (onAction
!is null) onAction();
442 // ////////////////////////////////////////////////////////////////////////// //
443 class AmpWidgetToggle
: AmpWidget
{
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
;
453 override void onPaint () {
454 bool active
= (parent
!is null && parent
.activeWidget
is this);
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
{
466 int titleofsmovedir
= 1;
467 int titlemovepause
= 8;
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
;
479 override void onPaint () {
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;
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
) {
502 override bool onMouse (MouseEvent event
) {
503 if (event
.type
== MouseEventType
.buttonPressed
&& event
.button
== MouseButton
.left
) {
504 parent
.activeWidget
= this;
508 if (event
.type
== MouseEventType
.buttonReleased
&& event
.button
== MouseButton
.left
) {
509 if (parent
.activeWidget
is this) {
511 if (title
.length
!= 0) {
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;
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
;
536 // ////////////////////////////////////////////////////////////////////////// //
537 class AmpMainWindow
: AmpWindow
{
538 AmpWidgetSongTitle wtitle
;
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; }
564 // ////////////////////////////////////////////////////////////////////////// //
568 int duration
; // in seconds
572 class AmpPListWindow
: AmpWindow
{
573 static struct SavedData
{
588 img
= skinImgPlaylist
;
594 res
.totaldur
= totaldur
;
595 res
.topitem
= topitem
;
596 res
.curitem
= curitem
;
600 void restoreData (ref SavedData sd
) {
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
;
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;
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;
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;
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
;
653 final @property int knobYOfs () nothrow @trusted {
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;
676 if (items
.length
< 1) return;
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
;
690 override void onPaint () {
692 gxFillRect(imgrc
, skinPListColNormalBG
);
693 auto irc
= GxRect(0, 0, 25, 20);
694 if (!active
) irc
.moveBy(0, 21);
699 while (tx
< imgrc
.width
-irc
.width
) {
700 img
.blitRect(tx
, 0, irc
);
707 img
.blitRect((imgrc
.width
-irtitle
.width
)/2, 0, irtitle
);
710 img
.blitRect(0, 0, irc
);
713 img
.blitRect(imgrc
.width
-irc
.width
, 0, irc
);
714 // left and right bars
719 while (ty
< imgrc
.height
-irc
.height
) {
722 img
.blitRect(0, ty
, irc
);
725 img
.blitRect(imgrc
.width
-irc
.width
, ty
, irc
);
729 // left part: 0,72 125x38
730 // right part: 126,72 150x38
733 ty
= imgrc
.height
-irc
.height
;
739 while (tx
< imgrc
.width
-150) {
740 img
.blitRect(tx
, ty
, irc
);
743 // bottom left corner
747 img
.blitRect(0, ty
, irc
);
748 // bottom right corner
751 img
.blitRect(imgrc
.width
-irc
.width
, ty
, irc
);
757 foreach (AmpWidget w
; widgets
) {
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; }
772 override bool onMousePost (MouseEvent event
) {
774 if (gxClipRect
.inside(event
.x
, event
.y
)) {
776 scope(exit
) normTop();
777 if (event
.type
== MouseEventType
.buttonPressed
&& event
.button
== MouseButton
.wheelUp
) {
778 if (curitem
> 0) --curitem
;
781 if (event
.type
== MouseEventType
.buttonPressed
&& event
.button
== MouseButton
.wheelDown
) {