1 var gamejs = require('gamejs');
  2 var draw=gamejs.draw;
  3 
  4 var EVT_FOCUS = exports.EVT_FOCUS = 'focus';
  5 var EVT_BLUR = exports.EVT_BLUR = 'blur';
  6 var EVT_MOUSE_OVER = exports.EVT_MOUSE_OVER = 'mouse_over';
  7 var EVT_MOUSE_OUT = exports.EVT_MOUSE_OUT = 'mouse_out';
  8 var EVT_KEY_DOWN = exports.EVT_KEY_DOWN= gamejs.event.KEY_DOWN;
  9 var EVT_KEY_UP = exports.EVT_KEY_UP = gamejs.event.KEY_UP;
 10 var EVT_MOUSE_UP = exports.EVT_MOUSE_UP = gamejs.event.MOUSE_UP;
 11 var EVT_MOUSE_DOWN = exports.EVT_MOUSE_DOWN = gamejs.event.MOUSE_DOWN;
 12 var EVT_MOUSE_WHEEL = exports.EVT_MOUSE_WHEEL = gamejs.event.MOUSE_WHEEL;
 13 var EVT_MOUSE_MOTION = exports.EVT_MOUSE_MOTION = gamejs.event.MOUSE_MOTION;
 14 var EVT_BTN_CLICK = exports.EVT_BTN_CLICK = 'btn_click';
 15 var EVT_CLOSE = exports.EVT_CLOSE = 'close';
 16 var EVT_SCROLL = exports.EVT_SCROLL = 'scroll';
 17 var EVT_DRAG = exports.EVT_DRAG = 'drag';
 18 var EVT_MOVE = exports.EVT_MOVE = 'move';
 19 var EVT_RESIZE = exports.EVT_RESIZE = 'resize';
 20 var EVT_PAINT = exports.EVT_PAINT = 'paint';
 21 var EVT_CHANGE = exports.EVT_CHANGE = 'change'; //input change for input elements
 22 var DEFAULT_FONT_DESCR='14px Verdana';
 23 var gamejs_ui_next_id=1;
 24 
 25 
 26 /**
 27  *@ignore
 28  */
 29 function cloneEvent(evt, offset){
 30     var new_evt={};
 31     for(key in evt){
 32         new_evt[key]=evt[key];
 33     }
 34     if(new_evt.pos && offset){
 35         new_evt.pos=[new_evt.pos[0]-offset[0], new_evt.pos[1]-offset[1]];
 36     }
 37     return new_evt; 
 38 }
 39 
 40 /**
 41  *@ignore
 42  */
 43 function getCenterPos(size1, size2){
 44     return [Math.max(parseInt((size1[0]-size2[0])/2), 0),
 45             Math.max(parseInt((size1[1]-size2[1])/2), 0)];
 46 }
 47 
 48 /**
 49  *Make a view draggable within it's parent. Dragging generates EVT_DRAG
 50  *@function
 51  *@param {View} view view to make draggable.
 52  */
 53 var draggable=exports.draggable=function(view){
 54     view.grab_pos=null;
 55     view.on(EVT_MOUSE_DOWN, function(event){
 56         this.grab_pos=event.global_pos;
 57     }, view);
 58     view.getGUI().on(EVT_MOUSE_UP, function(event){
 59         this.grab_pos=null;
 60     }, view);
 61     
 62     view.getGUI().on(EVT_MOUSE_MOTION, function(event){
 63         if(this.grab_pos){
 64             var old_position=this.position;
 65 
 66             var new_position=[this.position[0]+event.pos[0]-this.grab_pos[0],
 67                               this.position[1]+event.pos[1]-this.grab_pos[1]];
 68             
 69 
 70             this.grab_pos=event.pos;
 71             this.move(new_position);
 72             this.despatchEvent({'type':EVT_DRAG,
 73                                 'old_pos':old_position,
 74                                 'new_pos':this.position});
 75         }
 76     }, view);
 77 };
 78 
 79 /**
 80  *implements lazy caching for individual letters
 81  *@class cached font
 82  *@constructor
 83  *
 84  *@param {String|Array} font either font description as string, or assoc array character:gamejs.Surface
 85  *@param {String} color a valid #RGB String, "#ffcc00"
 86  *
 87  *@property {Number} space_width space between lettersin pixels. Default 'm' width divided by 3
 88  *@property {Number} tab_width tab width in pixels. Default 3*space_width
 89  *@property {gamejs.font.Font} font font object used to render characters. Default 14px Verdana
 90  */
 91 var CachedFont=exports.CachedFont=function(font, color){
 92     this.space_width=3;
 93     this.tab_width=12;
 94     this.chars={}; //character:surface;
 95     this.font=null;
 96     if((typeof font)=='string'){
 97         color = color ? color : '#000';
 98         this.color=color;
 99         this.font=new gamejs.font.Font(font);
100         
101     }else{
102         this.chars=font;
103         this.font=new gamejs.font.Font(DEFAULT_FONT_DESCR);
104         this.color='#000';
105     }
106     //space width - 1/3 of m's width
107     this.space_width=parseInt(Math.ceil(this.getCharSurface('m').getSize()[0]/3));
108     this.tab_width=3*this.space_width;
109 };
110 
111 
112 /**
113  *returns gamejs.Surface for a character. Caches this surface if it is not cached
114  *
115  *@function
116  *@param {String} c single character
117  *
118  *@returns {gamejs.Surface} surface object with the character painted on. Not a copy, don't paint on it!
119  */
120 CachedFont.prototype.getCharSurface=function(c){
121     if(!this.chars[c]){
122         var s=this.font.render(c, this.color);
123         this.chars[c]=s;
124     }
125     return this.chars[c];
126 };
127 
128 /**
129  *get size text would occupy if it was rendered
130  *@function
131  *
132  *@param {String} text
133  *
134  *@returns {Array} size, eg. [width, height]
135  */
136 CachedFont.prototype.getTextSize=function(text){
137     var w=0, h=0, c, l, sz;
138     if(text){ 
139         for(var i=0;i<text.length;i++){
140             c=text[i];
141             if(c==' ')w+=this.space_width;
142             else if(c=='\t')w+=this.tab_width;
143             else{
144                 l=this.getCharSurface(c);
145                 if(l){
146                     sz=l.getSize();
147                     w+=sz[0];
148                     h=Math.max(sz[1], h);
149                 }
150             }
151         }
152         if(!h) h=this.getCharSurface('m').getSize()[1];
153         return [w, h];
154     }else return [0, 0];
155 };
156 
157 /**
158  *render text on a surface
159  *@function
160  *
161  *@param {gamejs.Surface} surface surface to render text on
162  *@param {String} text text to render
163  *@param {Array} position position to render the text at
164  *@param {Number} space_width OPTIONAL, override space width
165  */
166 CachedFont.prototype.render=function(surface, text, position, space_width){
167     ofst=position[0];
168     space_width=space_width? space_width : this.space_width;
169     var i, c, s;
170     for(i=0;i<text.length;i++){
171         c=text[i];
172         if(c==' ')ofst+=space_width;
173         else if(c=='\t')ofst+=this.tab_width;
174         else{
175             s=this.getCharSurface(c);
176             r1=[ofst, position[1]];
177             surface.blit(s, r1);
178             ofst+=s.getSize()[0];
179         }
180     }        
181 };
182 
183 
184 exports.DEFAULT_FONT=new CachedFont('12px Verdana', 'black');
185 
186 /**
187  *View
188  *@class base gui object !CONSTRUCTOR PARAMETERS ARE PROVIDED IN A OBJECT LITERAL!
189  *@constructor
190  *
191  *@param {View} parent parent element
192  *@param {Array} size  array containing width & height of the element, eg. [width, height]
193  *@param {Array} position position of the view relative to parent, eg. [x, y]. OPTIONAL, default [0, 0]
194  *@param {gamejs.Surface} surface surface to render this view on, OPTIONAL
195  *@param {Bool} visible is this view visible? default true
196  *
197  *@property {Array} size view size, [width, length]
198  *@property {Array} position view position relative to parent, [x, y]
199  *@property {gamejs.Surface} surface surface this view is rendered on
200  *@property {View} parent parent view of this view
201  *@property {Array} children array of this views children views
202  */
203 var View=exports.View=function(pars){
204     this.type='view';
205     this.id=gamejs_ui_next_id++;
206     if(!pars.size) throw "View: size must be specified"
207     this.size=pars.size;
208     if(!pars.position) pars.position=[0, 0];
209     this.position=[parseInt(pars.position[0]), parseInt(pars.position[1])];
210     this.surface=pars.surface ? pars.surface : new gamejs.Surface(this.size);
211     if(pars.parent===undefined) throw "Element's parent not given."
212     this.parent=pars.parent;
213     if(pars.visible===undefined){
214         this.visible=true;
215     }else{
216         this.visible=pars.visible;
217     }
218     if(this.parent) this.parent.addChild(this);
219     
220     this.children=[];
221     this._refresh=true;
222     
223     //is the mouse over this view?
224     this.is_hovered=false;
225     
226     //is this view focused?
227     this.is_focused=false;
228     
229     //evenet type: [{'callback':x, 'scope':y, ...]
230     this.listeners={};
231     return this;
232     
233 };
234 
235 /*
236  *get is focus state
237  *@function
238  *@returns {Bool} is this view focused?
239  */
240 View.prototype.isFocused=function(){
241     return this.is_focused;  
242 };
243 
244 /**
245  *get hover state
246  *@function
247  *@returns {Bool} is mouse hovering on this element?
248  */
249 View.prototype.isHovered=function(){
250     return this.is_hovered;  
251 };
252 /**
253  *get view size
254  *@function
255  *@returns {Array} view size, [width, height]
256  */
257 View.prototype.getSize=function(){
258     return [this.size[0], this.size[1]];
259 };
260 
261 /**
262  *get view position within it's parent element
263  *@function
264  *@returns {Array} view position within it's parent element, [x, y]
265  */
266 View.prototype.getPosition=function(){
267     return [this.position[0], this.position[1]];
268 };
269 
270 /**
271  *get visible state
272  *@function
273  *@returns {Bool} is this view visible?
274  */
275 
276 View.prototype.isVisible=function(){
277   return this.visible;
278 };
279 
280 /**
281  *detaches a child from this view
282  *@function
283  *@param {View|Number} child View or or child View id.
284  */
285 View.prototype.removeChild=function(child){
286     if(typeof(child)!='number')child=child.id;
287     for(var i=0;i<this.children.length;i++){
288         if(this.children[i].id==child){
289             child=this.children.splice(i, 1);
290             this.refresh();
291             return true;
292         }
293     }
294     return false;
295 }
296 
297 /**
298  *detaches view from it's parent, effectively destroying it
299  *@function
300  */
301 View.prototype.destroy=function(){
302     if(this.parent)this.parent.removeChild(this);
303 }
304 
305 /**
306  *get position & size rect
307  *@returns gamejs.Rect instance. Position is relative to parent
308  */
309 View.prototype.getRect=function(){
310     return new gamejs.Rect(this.position, this.size);  
311 };
312 
313 /**
314  *add child to this view
315  *@param {View} child view to add as a child of this view
316  */
317 View.prototype.addChild=function(child){
318     this.children.push(child);
319 };
320 
321 /**
322  *if needed, paints this view, draws children and coposites them on this views surface
323  *@function
324  *@returns {Bool} has this view been repainted
325  */
326 View.prototype.draw=function(){
327     if(!this.visible){
328         if(this._refresh){
329             this._refresh=false;
330             return true;
331         }
332         return false;
333     }
334     
335     var painted=false; //has something been repainted in this view?
336     //does this view need repainting?
337   
338     this.children.forEach(function(child){
339         //draw children if this view has been repainted or child has been repainted
340         if(child.draw() || this._refresh){
341             painted=true;
342         }
343     }, this);
344     
345     if(this._refresh || painted){
346         this.paint();
347         this.despatchEvent({'type':EVT_PAINT, 'surface':this.surface});
348         this.children.forEach(function(child){
349             if(child.visible) this.blitChild(child);
350         }, this)
351         painted=true;
352         this._refresh=false;
353     }
354     
355     return painted;
356 };
357 /**
358  *blit child's surface on this view's surface
359  *@function
360  *@param {View} child child view to blit
361  */
362 View.prototype.blitChild=function(child){
363     this.surface.blit(child.surface, child.position);
364 };
365 
366 /**
367  *paint this view on it's surface. does not repaint/blit children! override this to implement custom drawing of the element itself. by default, only clears the surface
368  *@function
369  */
370 View.prototype.paint=function(){
371     this.surface.clear();
372 };
373 
374 
375 /**
376  *update view. does nothing by default
377  *@function
378  *@param {Number} miliseconds since last update
379  */
380 View.prototype.update=function(msDuration){};
381 
382 /**
383  *recursively calls _update on this views children, then calls update for this view.
384  *@ignore
385  */
386 View.prototype._update=function(msDuration){
387     this.children.forEach(function(child){
388         child._update(msDuration);        
389     });
390     this.update(msDuration);
391 };
392 
393 /**
394  *register a callback for an event. When event is triggered, callback is called with event object as argument
395  *@function
396  *@param {Event ID} event event ID of the event to be registered on, eg gamejs-gui.EVT_BLUR, gamejs-gui.EVT_KEY_DOWN, etc.
397  *@param {Function} callback function to call when event is triggered
398  *@param {Object} scope - this object for the callback
399  */
400 View.prototype.on=function(event_type, callback, scope){
401     if(!this.listeners[event_type])this.listeners[event_type]=[];
402     this.listeners[event_type].push({'callback':callback, 'scope':scope});
403 };
404 
405 /**
406  *despatches event to all children. internal
407  *@ignore
408  */
409 View.prototype.despatchEventToChildren= function(event){
410     this.children.forEach(function(child){child.despatchEvent(event);});
411 };
412 
413 /**
414  *Move view relative to its parent. Generates EVT_MOVE event
415  *@function
416  *@param {Array} new position relative to parent element, eg. [x, y]
417  */
418 View.prototype.move=function(position){
419     var old_position=this.position;
420     this.position=position;
421     this.parent.refresh();
422     this.despatchEvent({'type':EVT_MOVE,
423                        'old_pos':old_position,
424                        'new_pos':position});
425 };
426 
427 /**
428  *Move view relative to its position. Generates EVT_MOVE event
429  *@function
430  *@param {Array} delta coordinates relative to current position ,eg. [delta x, delta y]
431  */
432 View.prototype.moveRelative=function(position){
433     this.move([this.position[0]+position[0], this.position[1]+position[1]]);  
434 };  
435 
436 /**
437  *Resize this view. Generates EVT_RESIZE event
438  *@function
439  *@param {Array} new size, eg. [width, height]
440  */
441 View.prototype.resize=function(size){
442     var old_size=this.size;
443     this.size=size;
444     this.surface=new gamejs.Surface([Math.max(size[0], 1), Math.max(size[1], 1)]);
445     this.refresh();
446     this.despatchEvent({'type':EVT_RESIZE,
447                        'old_size':old_size,
448                        'new_size':size});
449 };
450 
451 /**
452  *Redraw this view and its children.
453  *@function
454  */
455 View.prototype.refresh=function(){
456     this._refresh=true;
457 };
458 
459 /**
460  *If this view is hidden, make it visible
461  *@function
462  */
463 View.prototype.show=function(){
464     if(!this.visible){
465         this.visible=true;
466         this.refresh();
467     }
468 };
469 
470 /**
471  *If this view is visible, hide it. This also blurs and mouse-outs the view, if applicable
472  *@function
473  */
474 View.prototype.hide=function(){
475     if(this.visible){
476         this.despatchEvent({'type':EVT_BLUR});
477         this.despatchEvent({'type':EVT_MOUSE_OUT});
478         this.visible=false;
479         this.refresh();
480     }
481 };
482 
483 /**
484  *Despatch event to this view. Event is despatched to children if applicable, then handled by this view.
485  *@function
486  *@param {Event} event event to despatch
487  */
488 View.prototype.despatchEvent=function(event){
489     if(!this.visible) return;
490     var inside=false; //event position inside this view
491     
492     if(event.type==EVT_BLUR){
493         if(this.is_focused){
494             this.is_focused=false;
495             this.refresh();
496             this.handleEvent(event);
497         }
498         this.despatchEventToChildren(event);
499     }
500     else if(event.type==EVT_MOUSE_OUT){
501         if(this.is_hovered){
502             this.is_hovered=false;
503             this.refresh();
504             this.handleEvent(event);
505         }
506         this.despatchEventToChildren(event);
507     }
508     else if(event.type==EVT_MOUSE_OVER){
509         this.is_hovered=true;
510         this.refresh();
511         this.handleEvent(event);
512     }
513     
514     else if(event.type==EVT_FOCUS){
515         this.is_focused=true;
516         this.refresh();
517         this.handleEvent(event);
518     }
519     
520     else if(event.type==EVT_MOUSE_DOWN){
521         if(!this.isFocused()){
522             this.despatchEvent({'type':EVT_FOCUS});
523         }
524         this.children.forEach(function(child){
525             //click inside child: despatch
526             if(child.getRect().collidePoint(event.pos)){
527                 child.despatchEvent(cloneEvent(event, child.position));
528             }else{
529                 //not inside, but child is focused: blur
530                 if(child.isFocused()) child.despatchEvent({'type':EVT_BLUR});
531             }
532         }, this);
533         this.handleEvent(event);
534     }
535     
536     else if(event.type==EVT_MOUSE_UP){
537         this.children.forEach(function(child){
538             //click inside child: despatch
539             if(child.getRect().collidePoint(event.pos)){
540                 child.despatchEvent(cloneEvent(event, child.position));
541             }
542         }, this);
543         this.handleEvent(event);
544     }
545     
546     else if(event.type==EVT_MOUSE_MOTION){
547         
548         //mouse moved onto this view - hover
549         this.children.forEach(function(child){
550             //click inside child: despatch
551             if(child.getRect().collidePoint(event.pos)){
552                 //inside, not hovering: hover
553                 if(!child.isHovered()) child.despatchEvent(cloneEvent({'type':EVT_MOUSE_OVER, 'pos':event.pos}, child.position));
554                 child.despatchEvent(cloneEvent(event, child.position));
555             }else{
556                 //not inside, but child is focused: blur
557                 if(child.isHovered()) child.despatchEvent(cloneEvent({'type':EVT_MOUSE_OUT, 'pos':event.pos}, child.position));
558             }
559         }, this);
560         this.handleEvent(event);
561         
562     }
563     else if(event.type==EVT_KEY_UP || event.type==EVT_KEY_DOWN || event.type==EVT_KEY_UP){
564         if(this.isFocused()){     
565             this.children.forEach(function(child){
566                 if(child.isFocused()) child.despatchEvent(cloneEvent(event));
567             });
568             this.handleEvent(event);
569         }
570     //default
571     }else{
572         this.handleEvent(event);
573     }
574 
575 };
576 
577 /**
578  *returns GUI object at the base of this views branch
579  *@function
580  *@returns {GUI} GUI object at the base if this views branch
581  */
582 View.prototype.getGUI=function(){
583     var parent=this.parent;
584     while(parent!=null && parent.type!='gui'){
585         parent=parent.parent;
586     }
587     return parent;
588 };
589 
590 /**
591  *Center a child view within this view. Must be direct child
592  *@function
593  *@param {View} child child view
594  */
595 View.prototype.center=function(child){
596     child.move(getCenterPos(this.size, child.size));   
597 };
598 
599 /**
600  *execute any registered callbacks for this event. Should only be called by despatchEvent!
601  *@ignore
602  */
603 View.prototype.handleEvent=function(event){
604     if(this.listeners[event.type]){
605         this.listeners[event.type].forEach(function(listener){
606             if(listener.scope) listener.callback.apply(listener.scope, [event, this]);
607             else listener.callback(event, this);
608         });
609     }
610 };
611 
612 /**
613  *@class single-line text display !CONSTRUCTOR PARAMETERS ARE PROVIDED IN A OBJECT LITERAL!
614  *@augments View
615  *@constructor 
616  *
617  *@param {CachedFont} font font to draw text with. OPTIONAL, default gamejs-gui.DEFAULT_FONT
618  *@param {String} text text to draw
619  *@param {View} parent parent element
620  *@param {Array} position position of the view relative to parent, eg. [x, y]. OPTIONAL, default [0, 0]
621  *@param {gamejs.Surface} surface surface to render this view on, OPTIONAL
622  *@param {Bool} visible is this view visible? OPTIONAL, EFAULT true
623  *
624  *resized automatically to house text.
625  *
626  *@property {CachedFont} font
627  *@property {String} text
628 
629  */
630 
631 var Label=exports.Label=function(pars){
632     this.font=pars.font ? pars.font : exports.DEFAULT_FONT;
633     pars.size=[1, 1];
634     Label.superConstructor.apply(this, [pars]);
635     if(!pars.text) throw "Label: label text must be provided!"
636     this.setText(pars.text);
637     this.type='label'; 
638 };
639 
640 gamejs.utils.objects.extend(Label, View);
641 
642 /**
643  *set new text for this label. Resizes the view automatically.
644  *@function
645  *
646  *@param {String} text new text
647  */
648 Label.prototype.setText=function(text){
649     this.text=text ? text : ' ';
650     this.size=this.font.getTextSize(text);
651     this.resize(this.size);
652 };
653 
654 /***
655  *paint implementation for label. clears surface and renders text
656  *@function
657  */
658 Label.prototype.paint=function(){
659     this.surface.clear();
660     this.font.render(this.surface, this.text, [0, 0]);
661 };
662 
663 /**
664  *@class button !CONSTRUCTOR PARAMETERS ARE PROVIDED IN A OBJECT LITERAL!
665  *@augments View
666  *
667  *@param {gamejs.Surface} image button image, OPTIONAL
668  *@param {gamejs.Surface} image_down button image when pressed down, OPTIONAL
669  *@param {gamejs.Surface} image_hover button image when hovered on, OPTIONAL
670  *@param {String} text text to display on button, OPTIONAL
671  *@param {CachedFont} font to render text with, OPTIONAL
672  *@param {View} parent parent element
673  *@param {Array} size  array containing width & height of the element, eg. [width, height]
674  *@param {Array} position position of the view relative to parent, eg. [x, y]. OPTIONAL, default [0, 0]
675  *@param {gamejs.Surface} surface surface to render this view on, OPTIONAL
676  *@param {Bool} visible is this view visible? default true
677  *
678  *@property {String} text
679  *@property {CachedFont} font
680  *@property {Label} label  Label obejct, created if text was provided.
681  */
682 
683 var Button=exports.Button=function(pars){
684     Button.superConstructor.apply(this, [pars]);
685     this.type='button';
686     this.image=null;
687     this.label=null;
688     this.image=pars.image;
689     this.image_down=pars.image_down;
690     this.image_hover=pars.image_hover;
691     
692     if(!this.image){
693         this.image=new gamejs.Surface(this.size);
694         this.image.fill('#FFF');
695         gamejs.draw.rect(this.image, '#808080', new gamejs.Rect([0, 0], this.size), 1);
696     }
697     
698     if(pars.text){     
699         this.label=new Label({'parent':this,
700                              'position':[0, 0],
701                              'text':pars.text,
702                              'font':pars.font});
703         this.center(this.label);
704     }
705     
706     this.pressed_down=false;
707     this.on(EVT_MOUSE_DOWN, function(){
708         if(!this.pressed_down){
709             this.pressed_down=true;
710             this.refresh();
711         }
712     }, this);
713     
714     this.on(EVT_MOUSE_UP, function(){
715         if(this.pressed_down){
716             this.pressed_down=false;
717             this.despatchEvent({'type':EVT_BTN_CLICK});
718             this.refresh();
719         }
720     }, this);
721     
722     this.on(EVT_MOUSE_OUT, function(){
723         if(this.pressed_down){
724             this.pressed_down=false;
725             this.refresh();
726         }
727     }, this)
728 };
729 
730 gamejs.utils.objects.extend(Button, View);
731 
732 /**
733  *short hand for on(EVT_BTN_CLICK, callback, scope)
734  *@function
735  *
736  *@param {Function} callback function to call when EVT_BTN_CLICK event is triggered
737  *@param {Object} scope this object for callback, OPTIONAL
738  */
739 Button.prototype.onClick=function(callback, scope){
740     this.on(EVT_BTN_CLICK, callback, scope);
741 };
742 
743 /**
744  *default button paint implementation paints image, image_down or image_hover based on button sotate
745  *@function
746  */
747 Button.prototype.paint=function(){
748     var img;
749     if(this.pressed_down && this.image_down) img=this.image_down;
750     else if(this.isHovered() && this.image_hover) img=this.image_hover;
751     else img=this.image;
752     this.surface.blit(img, new gamejs.Rect([0, 0], this.surface.getSize()), new gamejs.Rect([0, 0], img.getSize()));
753     
754 };
755 
756 /**
757  *set button text
758  *@function
759  *@param {String} text
760  */
761 Button.prototype.setText=function(text){
762     if(this.label){
763         this.label.setText(text);
764         this.center(this.label);
765     }
766 };
767 
768 /**
769  *@class image !CONSTRUCTOR PARAMETERS ARE PROVIDED IN A OBJECT LITERAL!
770  *@augments View
771  *
772  *@param {gamejs.Surface} image to paint
773  *@param {View} parent parent element
774  *@param {Array} size  array containing width & height of the element, eg. [width, height]
775  *@param {Array} position position of the view relative to parent, eg. [x, y]. OPTIONAL, default [0, 0]
776  *@param {gamejs.Surface} surface surface to render this view on, OPTIONAL
777  *@param {Bool} visible is this view visible? OPTIONAL, DEFAULT true
778  *
779  *@property {gamejs.Surface} image
780  */
781 
782 var Image=exports.Image=function(pars){
783     if(!pars.image) throw 'Image: parameter image is required';
784     if(!pars.size) pars.size=pars.image.getSize();
785     this.image=pars.image;
786     Image.superConstructor.apply(this, [pars]);
787     this.type='image';
788     return this;
789 };
790 
791 gamejs.utils.objects.extend(Image, View);
792 
793 /**
794  *set new image
795  *@function
796  *@param {gamejs.Surface} image
797  */
798 Image.prototype.setImage=function(image){
799     this.image=image;
800     this.refresh();
801 };
802 
803 /**
804  *default paint implementation for image. If Image object size!=provided image surface size, image is stretched.
805  *@function
806  */
807 Image.prototype.paint=function(){
808     View.prototype.paint.apply(this, []);
809     this.surface.blit(this.image, new gamejs.Rect([0, 0], this.surface.getSize()), new gamejs.Rect([0, 0], this.image.getSize()));  
810 };
811 
812 /**
813  *@class draggable frame header with a close button !CONSTRUCTOR PARAMETERS ARE PROVIDED IN A OBJECT LITERAL!
814  *@augments View
815  *
816  *@param {Frame} parent frame object this header is applied to
817  *@param {Number} height frame height, OPTIONAL, DEFAULT 20
818  *@param {String} title frame title OPTIONAL
819  *@param {CachedFont} title_font font for title OPTIONAL
820  *@param {Bool} close_btn show close button? OPTIONAL, DEFAULT false
821  *@param {gamejs.Surface} close_icon image to use for close button, OPTIONAL
822  *@param {gamejs.Surface} close_btn close button image OPTIONAL
823  *@param {gamejs.Surface} surface surface to render this view on, OPTIONAL
824  *@param {Bool} visible is this view visible? OPTIONAL, DEFAULT true
825  *
826  *@property {CachedFont) title_font
827  *@property {Number} height
828  */
829 var FrameHeader=exports.FrameHeader=function(pars){
830     if(!pars.parent) throw 'FrameHeader: parent parameter is required';
831     this.height=pars.height || 20;
832     pars.width=pars.parent.size[0];
833     pars.size=[pars.width, this.height];
834     pars.position=[0, 0];
835     this.title_font=pars.title_font;
836     
837     FrameHeader.superConstructor.apply(this, [pars]);
838     draggable(this);
839     
840     if(pars.title){
841         this.setTitle(pars.title);
842     }
843     
844     if(pars.close_btn){
845         var img;
846         if(pars.close_icon){
847             img=pars.close_icon;
848         }
849         else{
850             img=new gamejs.Surface([this.height, this.height]);
851             gamejs.draw.line(img, '#000', [3, 3], [this.height-3, this.height-3], 3);
852             gamejs.draw.line(img, '#000', [3, this.height-3], [this.height-3, 3], 3);
853         }
854       
855         img=new Image({'parent':this,
856                       'position':[this.size[0]-img.getSize()[0], 0],
857                       'image':img});
858         img.on(EVT_MOUSE_DOWN, function(){
859             this.close();
860             this.despatchEvent({'type':EVT_CLOSE});
861         }, this.parent);
862     }
863     
864     this.type='frameheader';       
865 };
866 
867 gamejs.utils.objects.extend(FrameHeader, View);
868 
869 /**
870  *moving header moves parent frame too
871  *@function
872  *
873  *@param {Array} pos position ot move header to
874  */
875 FrameHeader.prototype.move=function(pos){
876     this.parent.move([this.parent.position[0]+pos[0]-this.position[0],
877                       this.parent.position[1]+pos[1]-this.position[1]]);
878 };
879 
880 /**
881  *set header title
882  *@function
883  *
884  *@param {String} text new header title
885  */
886 FrameHeader.prototype.setTitle=function(text){
887     if(!this.title_label)this.title_label=new Label({'parent':this,
888                                                     'position':[0, 0],
889                                                     'font':this.title_font,
890                                                     'text':text});
891     else this.title_label.setText(text);
892     var font=this.title_label.font;
893     var size=font.getTextSize(text);
894     this.title_label.move([font.space_width, Math.max(parseInt(this.height-size[1]))], 0);
895     draggable(this);
896 };
897 
898 /**
899  *default paint implementation: gray background
900  *@function
901  */
902 FrameHeader.prototype.paint=function(){
903     gamejs.draw.rect(this.surface, '#C0C0C0', new gamejs.Rect([0, 0], this.size));
904 };
905 
906 
907 
908 /**
909  *@class a overlay view with it's own space and hierarchy, a 'window' in OS talk. Hidden by default !CONSTRUCTOR PARAMETERS ARE PROVIDED IN A OBJECT LITERAL!
910  *@augments View
911  *
912  *@param {GUI} parent parent GUI object
913  *@param {Bool} constrain if true, frame cannot be moved out of visible area
914  *@param {Array} size  array containing width & height of the element, eg. [width, height]
915  *@param {Array} position position of the view relative to parent, eg. [x, y]. OPTIONAL, default [0, 0]
916  *@param {gamejs.Surface} surface surface to render this view on, OPTIONAL
917  *@param {Bool} visible is this view visible? OPTIONAL, DEFAULT true
918  *
919  *@property {Bool} constrain
920  */
921 var Frame=exports.Frame=function(pars){
922     if(!pars.parent) throw 'Frame: parent parameter is required';
923     if(pars.parent.type!='gui') throw 'Frame: parent object must be instance of GUI';
924     var gui=pars.parent;
925     pars.parent=null;
926     Frame.superConstructor.apply(this, [pars]);
927     this.type='frame';
928     this.visible=false;
929     gui.frames.push(this);
930     this.parent=gui;    
931     //constrain
932     this.constrain=pars.constrain;
933     return this;
934 };
935 gamejs.utils.objects.extend(Frame, View);
936 
937 /**
938  *Default implementation, white fill and gray border.
939  *@function
940  */
941 Frame.prototype.paint=function(){
942     //fill
943     gamejs.draw.rect(this.surface, '#FFF', new gamejs.Rect([0, 0], this.size));
944     
945     //draw border
946     gamejs.draw.rect(this.surface, '#404040', new gamejs.Rect([0, 0], this.size), 1);
947 };
948 
949 /**
950  *Show frame, move it to top of the screen
951  *@function
952  */
953 Frame.prototype.show=function(){
954     View.prototype.show.apply(this, []);
955     this.parent.moveFrameToTop(this);
956 };
957 
958 /**
959  *Close frame. You propably want to use this instead of hide()! generates EVT_CLOSE
960  *@function
961  */
962 Frame.prototype.close=function(){
963     View.prototype.hide.apply(this, []);
964     this.despatchEvent({'type':EVT_CLOSE});
965 };
966 
967 /**
968  *implements restricting frame to GUI bounds. generates EVT_MOVE
969  *@function
970  *@param {Array} position position to move frame to
971  */
972 
973 Frame.prototype.move=function(position){
974     if(this.constrain){
975         if(position[0]<0)position[0]=0;
976         if(position[0]>this.parent.size[0]-this.size[0]) position[0]=this.parent.size[0]-this.size[0];
977         if(position[1]<0)position[1]=0;
978         if(position[1]>this.parent.size[1]-this.size[1]) position[1]=this.parent.size[1]-this.size[1];
979     }
980     View.prototype.move.apply(this, [position]);
981 };
982 
983 /**
984  *closes frame, then destroys it
985  *@function
986  */
987 Frame.prototype.destroy=function(){
988     if(this.visible) this.close();
989     if(this.parent) this.parent.removeFrame(this);
990 };
991 
992 /**
993  *@class draggable view: can be dragged within its parent by holding down left mouse btn
994  *@augments View
995  *
996  *@param {Number} min_x minimum x coordinate view can be dragged to, OPTIONAL
997  *@param {Number} max_x maximum x coordinate view can be dragged to, OPTIONAL
998  *@param {Number} min_y minimum y coordinate view can be dragged to, OPTIONAL
999  *@param {Number} max_y maximum y coordinate view can be dragged to, OPTIONAL
1000  *@param {View} parent parent element
1001  *@param {Array} size  array containing width & height of the element, eg. [width, height]
1002  *@param {Array} position position of the view relative to parent, eg. [x, y]. OPTIONAL, default [0, 0]
1003  *@param {gamejs.Surface} surface surface to render this view on, OPTIONAL
1004  *@param {Bool} visible is this view visible? default true
1005  *
1006  *@property {Number} min_x
1007  *@property {Number} max_x
1008  *@property {Number} min_y
1009  *@property {Number} max_y
1010  */
1011 
1012 var DraggableView=exports.DraggableView=function(pars){
1013     DraggableView.superConstructor.apply(this, [pars]);
1014     draggable(this);
1015     this.min_x=pars.min_x;
1016     this.max_x=pars.max_x;
1017     this.min_y=pars.min_y;
1018     this.max_y=pars.max_y;
1019     this.type='draggableview';
1020 };
1021 
1022 gamejs.utils.objects.extend(DraggableView, View);
1023 
1024 /**
1025  *implements restricting to coordinates, if applicable
1026  *@function
1027  *@param {Array} pos new position
1028  */
1029 DraggableView.prototype.move=function(pos){
1030     var x=pos[0];
1031     if(this.min_x || (this.min_x==0)) x=Math.max(x, this.min_x);
1032     if(this.max_x || (this.max_x==0)) x=Math.min(x, this.max_x);
1033     
1034     var y=pos[1];
1035     if(this.min_y || (this.min_y==0)) y=Math.max(y, this.min_y);
1036     if(this.max_y || (this.max_y==0)) y=Math.min(y, this.max_y);
1037     
1038     View.prototype.move.apply(this, [[x, y]]);
1039 };
1040 
1041 
1042 /**
1043  *@class draggable part of the scrollbar !CONSTRUCTOR PARAMETERS ARE PROVIDED IN A OBJECT LITERAL!
1044  *@augments DraggableView
1045  *
1046  *@param {View} parent parent element
1047  *@param {Array} size  array containing width & height of the element, eg. [width, height]
1048  *@param {gamejs.Surface} image iamge to use for the scroller, OPTIONAL
1049  *@param {Number} min_x minimum x coordinate view can be dragged to, OPTIONAL
1050  *@param {Number} max_x maximum x coordinate view can be dragged to, OPTIONAL
1051  *@param {Number} min_y minimum y coordinate view can be dragged to, OPTIONAL
1052  *@param {Number} max_y maximum y coordinate view can be dragged to, OPTIONAL
1053  *@param {Array} position position of the view relative to parent, eg. [x, y]. OPTIONAL, default [0, 0]
1054  *@param {gamejs.Surface} surface surface to render this view on, OPTIONAL
1055  *@param {Bool} visible is this view visible? OPTIONAL, DEFAULT true
1056  *
1057  *@property {Image} img Image object created if image parameter was provided
1058  */
1059 var Scroller=exports.Scroller=function(pars){
1060     Scroller.superConstructor.apply(this, [pars]);
1061     this.img=null;
1062     if(pars.image){
1063         this.img=new Image({'parent':this,
1064                     'position':[0, 0],
1065                     'size':this.size,
1066                     'image':pars.image});
1067     }
1068 };
1069 gamejs.utils.objects.extend(Scroller, DraggableView);
1070 
1071 /**
1072  *resizes image along with scroller
1073  *@function 
1074  */
1075 Scroller.prototype.resize=function(size){
1076     DraggableView.prototype.resize.apply(this,[size]);
1077     if(this.img)this.img.resize(size);
1078 
1079 };
1080 
1081 /**
1082  *@class !CONSTRUCTOR PARAMETERS ARE PROVIDED IN A OBJECT LITERAL!
1083  *@augments View
1084  *
1085  *@param {gamejs.Surface} left_btn_image image for left scrollbar button
1086  *@param {gamejs.Surface} scroller_image image for scroller
1087  *@param {gamejs.Surface} right_btn_image image for right scrollbar button
1088  *@param {View} parent parent element
1089  *@param {Array} size  array containing width & height of the element, eg. [width, height]
1090  *@param {Array} position position of the view relative to parent, eg. [x, y]. OPTIONAL, default [0, 0]
1091  *@param {gamejs.Surface} surface surface to render this view on, OPTIONAL
1092  *@param {Bool} visible is this view visible? OPTIONAL, DEFAULT true
1093  *
1094  *@property {Button} left_btn left scrollbar button
1095  *@property {Button} right_btn right scrollbar button
1096  *@property {Scroller} scroller
1097  *@property {Function} scroller_class scroller class used to create scroller #TODO find better way to implement customization
1098  */
1099 
1100 var HorizontalScrollbar=exports.HorizontalScrollbar=function(pars){
1101     HorizontalScrollbar.superConstructor.apply(this, [pars]);
1102     this.type='horizontalscrollbar';
1103     var left_btn_image=pars.left_btn_image;
1104     if(!left_btn_image){
1105         left_btn_image=new gamejs.Surface([this.size[1], this.size[1]]);
1106         var pts=[[0, this.size[1]/2],
1107                  [this.size[1], 0],
1108                  [this.size[1], this.size[1]]];
1109         gamejs.draw.polygon(left_btn_image, '#C0C0C0', pts);
1110     }
1111     this.left_btn=new Button({'parent':this,
1112                             'position':[0, 0],
1113                             'size':[this.size[1], this.size[1]],
1114                             'image':left_btn_image});
1115     this.left_btn.onClick(this.scrollLeft, this);
1116     
1117     var right_btn_image=pars.right_btn_image;
1118     if(!right_btn_image){
1119         right_btn_image=new gamejs.Surface([this.size[1], this.size[1]]);
1120         var pts=[[0, 0],
1121                  [this.size[1], this.size[1]/2],
1122                  [0, this.size[1]]];
1123         gamejs.draw.polygon(right_btn_image, '#C0C0C0', pts);
1124     }
1125     this.right_btn=new Button({'parent':this,
1126                             'position':[this.size[0]-this.size[1], 0],
1127                             'size':[this.size[1], this.size[1]],
1128                             'image':right_btn_image});
1129     
1130     this.right_btn.onClick(this.scrollRight, this);
1131 
1132     //scroller track size
1133     this.sts=this.size[0]-this.right_btn.size[0]-this.left_btn.size[0];
1134     
1135     var scroller_image=pars.scroller_image;
1136     if(!scroller_image){
1137         scroller_image=new gamejs.Surface([this.size[1], this.size[1]]);
1138         var sz=scroller_image.getSize()
1139         gamejs.draw.rect(scroller_image, '#C0C0C0', new gamejs.Rect([0, 0],[sz[0], sz[1]]));
1140     }
1141     var size=[Math.max(parseInt((this.size[0]-2*this.size[1])/2),scroller_image.getSize()[0]), this.size[1]];
1142     this.scroller=new this.scroller_class({'parent':this,
1143                                             'position':[this.size[1], 0],
1144                                             'image':scroller_image,
1145                                             'size':size,
1146                                             'min_x':this.size[1],
1147                                             'max_x':this.size[0]-this.right_btn.size[0]-size[0],
1148                                             'min_y':0,
1149                                             'max_y':0});
1150 
1151     
1152     this.scroll_pos=0;
1153     this.max_scroll_pos=this.sts-this.scroller.size[0];
1154     
1155     this.scroller.on(EVT_DRAG, function(event){
1156         this.setScrollPX(event.new_pos[0]-this.size[1]);
1157     }, this);
1158 };
1159 gamejs.utils.objects.extend(HorizontalScrollbar, View);
1160 
1161 HorizontalScrollbar.prototype.scroller_class=Scroller;
1162 
1163 /**
1164  *set relative scroller width
1165  *@function
1166  *@param {Number} sz relative scroller width, between 0.1 and 1, 
1167  */
1168 HorizontalScrollbar.prototype.setScrollerSize=function(sz){
1169     sz=Math.min(Math.max(sz, 0.1), 1);
1170     this.scroller.resize([this.sts*sz, this.scroller.size[1]]);
1171    
1172     this.max_scroll_pos=this.sts-this.scroller.size[0];
1173     this.scroller.max_x=this.size[0]-this.left_btn.size[0]-this.scroller.size[0];
1174     this.refresh();
1175 };
1176 
1177 /**
1178  *set scroll amount, px
1179  *@function
1180  *@param {Number} pos scroll amount, px
1181  */
1182 HorizontalScrollbar.prototype.setScrollPX=function(pos){
1183     this.scroller.move([pos+this.left_btn.size[0], 0]);
1184     var pos_x=this.scroller.position[0]-this.left_btn.size[0];
1185     this.scroll_pos=pos_x;
1186     var scroll=0;
1187     if(this.max_scroll_pos>0){
1188         scroll=pos_x/this.max_scroll_pos;
1189     }
1190     this.despatchEvent({'type':EVT_SCROLL,
1191                        'scroll_px':pos_x,
1192                        'scroll':scroll});
1193     this.refresh();
1194 };
1195 
1196 /**
1197  *set scroll amount, relative
1198  *@function
1199  *@param {Number} pos scroll amount, between 0 and 1
1200  */
1201 HorizontalScrollbar.prototype.setScroll=function(pos){
1202     this.setScrollPX(parseInt(this.max_scroll_pos*pos));
1203 };
1204 
1205 /**
1206  *scroll left by 0.1 of max scrollable amount
1207  *@function
1208  */
1209 HorizontalScrollbar.prototype.scrollLeft=function(){
1210     this.setScrollPX(Math.max(0, this.scroll_pos-this.max_scroll_pos*0.1));
1211 };
1212 
1213 /**
1214  *scroll right by 0.1 of max scrollable amount
1215  *@function
1216  */
1217 HorizontalScrollbar.prototype.scrollRight=function(){
1218     this.setScrollPX(Math.min(this.max_scroll_pos, this.scroll_pos+this.max_scroll_pos*0.1));
1219 };
1220 
1221 /**
1222  *@class !CONSTRUCTOR PARAMETERS ARE PROVIDED IN A OBJECT LITERAL!
1223  *@augments View
1224  *
1225  *@param {gamejs.Surface} top_btn_image image for top scrollbar button
1226  *@param {gamejs.Surface} scroller_image image for scroller
1227  *@param {gamejs.Surface} bot_btn_image image for bottom scrollbar button
1228  *@param {View} parent parent element
1229  *@param {Array} size  array containing width & height of the element, eg. [width, height]
1230  *@param {Array} position position of the view relative to parent, eg. [x, y]. OPTIONAL, default [0, 0]
1231  *@param {gamejs.Surface} surface surface to render this view on, OPTIONAL
1232  *@param {Bool} visible is this view visible? OPTIONAL, DEFAULT true
1233  *
1234  *@property {Button} top_btn top scrollbar button
1235  *@property {Button} bot_btn bottom scrollbar button
1236  *@property {Scroller} scroller
1237  *@property {Function} scroller_class scroller class used to create scroller #TODO find better way to implement customization
1238  */
1239 
1240 var VerticalScrollbar=exports.VerticalScrollbar=function(pars){
1241     VerticalScrollbar.superConstructor.apply(this, [pars]);
1242     this.type='verticalscrollbar';
1243     var top_btn_image=pars.top_btn_image;
1244     if(!top_btn_image){
1245         top_btn_image=new gamejs.Surface([this.size[0], this.size[0]]);
1246         var pts=[[this.size[0]/2, 0],
1247                  [0, this.size[0]],
1248                  [this.size[0], this.size[0]]];
1249         gamejs.draw.polygon(top_btn_image, '#C0C0C0', pts);
1250     }
1251     this.top_btn=new Button({'parent':this,
1252                             'position':[0, 0],
1253                             'size':[this.size[0], this.size[0]],
1254                             'image':top_btn_image});
1255     this.top_btn.onClick(this.scrollUp, this);
1256     
1257     var bot_btn_image=pars.bot_btn_image;
1258     if(!bot_btn_image){
1259         bot_btn_image=new gamejs.Surface([this.size[0], this.size[0]]);
1260         var pts=[[0, 0],
1261                  [this.size[0], 0],
1262                  [this.size[0]/2, this.size[0]]];
1263         gamejs.draw.polygon(bot_btn_image, '#C0C0C0', pts);
1264     }
1265     this.bot_btn=new Button({'parent':this,
1266                             'position':[0, this.size[1]-this.size[0]],
1267                             'size':[this.size[0], this.size[0]],
1268                             'image':bot_btn_image});
1269     
1270     this.bot_btn.onClick(this.scrollDown, this);
1271 
1272     //scroller track size
1273     this.sts=this.size[1]-this.bot_btn.size[1]-this.top_btn.size[1];
1274     
1275     var scroller_image=pars.scroller_image;
1276     if(!scroller_image){
1277         scroller_image=new gamejs.Surface([this.size[0], this.size[0]]);
1278         var sz=scroller_image.getSize()
1279         gamejs.draw.rect(scroller_image, '#C0C0C0', new gamejs.Rect([0, 0],[sz[0], sz[1]]));
1280     }
1281     var size=[this.size[0], Math.max(parseInt((this.size[1]-2*this.size[0])/2),scroller_image.getSize()[1])];
1282     this.scroller=new this.scroller_class({'parent':this,
1283                                             'position':[0, this.size[0]],
1284                                             'image':scroller_image,
1285                                             'size':size,
1286                                             'min_x':0,
1287                                             'max_x':0,
1288                                             'min_y':this.size[0],
1289                                             'max_y':this.size[1]-this.bot_btn.size[1]-size[1]});
1290 
1291     
1292     this.scroll_pos=0;
1293     this.max_scroll_pos=this.sts-this.scroller.size[1];
1294     
1295     this.scroller.on(EVT_DRAG, function(event){
1296         this.setScrollPX(event.new_pos[1]-this.size[0]);
1297     }, this);
1298 };
1299 gamejs.utils.objects.extend(VerticalScrollbar, View);
1300 
1301 VerticalScrollbar.prototype.scroller_class=Scroller;
1302 
1303 /**
1304  *set relative scroller width
1305  *@function
1306  *@param {Number} sz relative scroller width, between 0.1 and 1, 
1307  */
1308 VerticalScrollbar.prototype.setScrollerSize=function(sz){
1309     sz=Math.min(Math.max(sz, 0.1), 1);
1310     this.scroller.resize([this.scroller.size[0], this.sts*sz]);
1311    
1312     this.max_scroll_pos=this.sts-this.scroller.size[1];
1313     this.scroller.max_y=this.size[1]-this.bot_btn.size[1]-this.scroller.size[1];
1314     this.refresh();
1315 };
1316 
1317 /**
1318  *set scroll amount, px
1319  *@function
1320  *@param {Number} pos scroll amount, px
1321  */
1322 VerticalScrollbar.prototype.setScrollPX=function(pos){
1323     this.scroller.move([0, pos+this.top_btn.size[1]]);
1324     var pos_y=this.scroller.position[1]-this.top_btn.size[1];
1325     this.scroll_pos=pos_y;
1326     var scroll=0;
1327     if(this.max_scroll_pos>0){
1328         scroll=pos_y/this.max_scroll_pos;
1329     }
1330     this.despatchEvent({'type':EVT_SCROLL,
1331                        'scroll_px':pos_y,
1332                        'scroll':scroll});
1333     this.refresh();
1334 };
1335 
1336 /**
1337  *set scroll amount, relative
1338  *@function
1339  *@param {Number} pos scroll amount, between 0 and 1
1340  */
1341 VerticalScrollbar.prototype.setScroll=function(pos){
1342     this.setScrollPX(parseInt(this.max_scroll_pos*pos));
1343 };
1344 
1345 /**
1346  *@function
1347  *scroll up by 0.1 of max scrollable amount
1348  */
1349 VerticalScrollbar.prototype.scrollUp=function(){
1350     this.setScrollPX(Math.max(0, this.scroll_pos-this.max_scroll_pos*0.1));
1351 };
1352 
1353 /**
1354  *@function
1355  *scroll down by 0.1 of max scrollable amount
1356  */
1357 VerticalScrollbar.prototype.scrollDown=function(){
1358     this.setScrollPX(Math.min(this.max_scroll_pos, this.scroll_pos+this.max_scroll_pos*0.1));
1359 };
1360 
1361 /**
1362  *@class view with scrollable content !CONSTRUCTOR PARAMETERS ARE PROVIDED IN A OBJECT LITERAL!
1363  *@augments View
1364  *
1365  *@param {View} parent parent element
1366  *@param {Array} size  array containing width & height of the element, eg. [width, height]
1367  *@param {Array} position position of the view relative to parent, eg. [x, y]. OPTIONAL, default [0, 0]
1368  *@param {gamejs.Surface} surface surface to render this view on, OPTIONAL
1369  *@param {Bool} visible is this view visible? OPTIONAL, DEFAULT true
1370  */
1371 var ScrollableView=exports.ScrollableView=function(pars){
1372     ScrollableView.superConstructor.apply(this, [pars]);
1373     this.type='scrollableview';
1374     this.scroll_x=0;
1375     this.scroll_y=0;
1376     this.max_scroll_x=0;
1377     this.max_scroll_y=0;
1378     this.scrollable_area=[0, 0];
1379     this.setScrollableArea(this.size);
1380     this.vertical_scrollbar=null;
1381     this.horizontal_scrollbar=null;
1382 };
1383 gamejs.utils.objects.extend(ScrollableView, View);
1384 
1385 /**
1386  *set vertical scrollbar for this view
1387  *@function
1388  *@param {VerticalScrollbar} scrollbar
1389  */
1390 ScrollableView.prototype.setVerticalScrollbar=function(scrollbar){
1391     this.vertical_scrollbar=scrollbar;
1392     scrollbar.on(EVT_SCROLL, function(event){
1393         this.setScrollY(Math.ceil(event.scroll*this.max_scroll_y));
1394     }, this);
1395 };
1396 
1397 /**
1398  *set horizontal scrollbar for this view
1399  *@function
1400  *@param {HorizontalScrollbar} scrollbar
1401  */
1402 ScrollableView.prototype.setHorizontalScrollbar=function(scrollbar){
1403     this.horizontal_scrollbar=scrollbar;
1404     scrollbar.on(EVT_SCROLL, function(event){
1405         this.setScrollX(Math.ceil(event.scroll*this.max_scroll_x));
1406     }, this);
1407 };
1408 
1409 /**
1410  *manually set size of scrollable area
1411  *@function
1412  *@param {Array} area scrollbale area, [width, height]
1413  */
1414 ScrollableView.prototype.setScrollableArea=function(area){
1415     this.scrollable_area=area;
1416     this.max_scroll_y=Math.max(area[1]-this.size[1], 0);
1417     this.max_scroll_x=Math.max(area[0]-this.size[0], 0);
1418     if(this.vertical_scrollbar){
1419         var sz=Math.max(Math.min(1, this.size[1]/area[1]), 0.1);
1420         this.vertical_scrollbar.setScrollerSize(sz);
1421     }
1422     if(this.horizontal_scrollbar){
1423         var sz=Math.max(Math.min(1, this.size[0]/area[0]), 0.1);
1424         this.horizontal_scrollbar.setScrollerSize(sz);
1425     }
1426 };
1427 
1428 /**
1429  *automatically set scrollable area based on children positions and sizes
1430  *@function
1431  */
1432 ScrollableView.prototype.autoSetScrollableArea=function(){
1433     scrollable_area=[0, 0];
1434     this.children.forEach(function(child){
1435             scrollable_area[0]=Math.max(scrollable_area[0], child.position[0]+child.size[0]+20);
1436             scrollable_area[1]=Math.max(scrollable_area[1], child.position[1]+child.size[1]+20);
1437     }, this);
1438     this.setScrollableArea(scrollable_area);
1439 };
1440 
1441 /**
1442  *TODO: implement optional auto setting scrollable area when children are added
1443  *@function
1444  */
1445 ScrollableView.prototype.addChild=function(child){
1446     View.prototype.addChild.apply(this, [child]);
1447     this.refresh();    
1448 };
1449 
1450 /**
1451  *implements child blitting adjusted to scroll state
1452  *@function
1453  */
1454 ScrollableView.prototype.blitChild=function(child){
1455     this.surface.blit(child.surface, [child.position[0]-this.scroll_x, child.position[1]-this.scroll_y]);
1456 };
1457 
1458 /**
1459  *adjusts event position based on scroll state
1460  *@function
1461  */
1462 ScrollableView.prototype.despatchEvent=function(event){
1463     if(event.pos){
1464         event=cloneEvent(event);
1465         event.pos=[event.pos[0]+this.scroll_x, event.pos[1]+this.scroll_y];
1466     }
1467     View.prototype.despatchEvent.apply(this, [event]);
1468 };
1469 
1470 /**
1471  *increment horizontal scroll
1472  *@function
1473  *@param {Number} x px to increment horizontal scroll by
1474  */
1475 ScrollableView.prototype.scrollX=function(x){
1476   this.setScrollX(this.scroll_x+x);
1477   this.refresh();
1478 };
1479 
1480 /**
1481  *increment vertical scroll
1482  *@function
1483  *@param {Number} y px to increment vertical scroll by
1484  */
1485 ScrollableView.prototype.scrollY=function(y){
1486     this.setScrollY(this.scroll_y+y);
1487     this.refresh();
1488 };
1489 
1490 /**
1491  *set horizontal scroll
1492  *@function
1493  *@param {Number} x horizontal scroll, px
1494  */
1495 ScrollableView.prototype.setScrollX=function(x){
1496     this.scroll_x=Math.min(Math.max(x, 0), this.max_scroll_x);
1497     this.refresh();
1498 };
1499 
1500 /**
1501  *set vertical scroll
1502  *@function
1503  *@param {Number} y vertical scroll, px
1504  */
1505 ScrollableView.prototype.setScrollY=function(y){
1506     this.scroll_y=Math.min(Math.max(y, 0), this.max_scroll_y);
1507     this.refresh();
1508 };
1509 
1510 /**
1511  *@class text input !CONSTRUCTOR PARAMETERS ARE PROVIDED IN A OBJECT LITERAL!
1512  *@augments View
1513  *
1514  *@param {CachedFont} font 
1515  *@param {String} text
1516  *@param {Array} scw_size actual text display size, [width, height].
1517  *@param {View} parent parent element
1518  *@param {Array} size  array containing width & height of the element, eg. [width, height]
1519  *@param {Array} position position of the view relative to parent, eg. [x, y]. OPTIONAL, default [0, 0]
1520  *@param {gamejs.Surface} surface surface to render this view on, OPTIONAL
1521  *@param {Bool} visible is this view visible? OPTIONAL, DEFAULT true
1522  *
1523  *@property {CachedFont} font
1524  *@property {String} text
1525  */
1526 var TextInput=exports.TextInput=function(pars){
1527     TextInput.superConstructor.apply(this, [pars]);
1528     this.font=pars.font ? pars.font : exports.DEFAULT_FONT;
1529     this.text=pars.text ? pars.text : '';
1530     this.blip=false;
1531     this.pos=0;
1532     this.ms=500;
1533     
1534     this.scw=new ScrollableView({'parent':this,
1535                               'position':[0, 0],
1536                               'size':pars.scw_size || this.size});
1537     this.center(this.scw);
1538     this.label=new Label({'parent':this.scw,
1539                          'position':[0, 0],
1540                          'font':this.font,
1541                          'text':this.text});
1542     this.scw.center(this.label);
1543     this.label.move([3, this.label.position[1]]);
1544     
1545     this.bliplabel=new Label({'parent':this.scw,
1546                              'position':[0, 0],
1547                              'font':this.font,
1548                              'visible':false,
1549                              'text':'|'});
1550     
1551     this.on(EVT_KEY_DOWN, this.onKeyDown, this);
1552     this.on(EVT_FOCUS, this.blipon, this);
1553     this.on(EVT_BLUR, function(){
1554         this.blip=false;
1555     }, this);
1556     this.setPos(this.text.length);
1557 };
1558 gamejs.utils.objects.extend(TextInput, View);
1559 
1560 /**
1561  *turn blip on
1562  *@ignore
1563  */
1564 TextInput.prototype.blipon=function(event){
1565     this.blip=true;
1566     this.ms=500;
1567     this.refresh();
1568 };
1569 
1570 /**
1571  *implements blip updating
1572  *@function
1573  *@param {Number} msDuration
1574  */
1575 TextInput.prototype.update=function(msDuration){
1576     if(this.isFocused()){
1577         this.ms-=msDuration;
1578         if(this.ms<0){
1579             this.ms=500;
1580             this.blip=!this.blip;
1581         };
1582         if(this.blip){
1583             this.bliplabel.show();    
1584         }else{
1585             this.bliplabel.hide();
1586         }
1587     }else{
1588         this.bliplabel.hide();
1589     }
1590 };
1591 
1592 /**
1593  *default implementation: white fill, gray border
1594  *@function
1595  */
1596 TextInput.prototype.paint=function(){
1597     this.surface.fill('#FFF');
1598     gamejs.draw.rect(this.surface, '#COCOCO', new gamejs.Rect([0, 0], this.size), 1);
1599 };
1600 
1601 /**
1602  *set input text. generates EVT_CHANGE
1603  *@function
1604  *@param {String} text
1605  */
1606 TextInput.prototype.setText=function(text){
1607     this.setPos(this.text.length);
1608     this._setText(text);
1609 };
1610 
1611 /**
1612  *set blip position
1613  *@ignore
1614  */
1615 TextInput.prototype.setPos=function(pos){
1616     this.pos=Math.min(Math.max(pos, 0), this.text.length);
1617 
1618     //calc offset for scorllable area
1619     var ofst=0;
1620     var ofst=0;
1621     var tlen=this.font.getTextSize(this.text.substr(0, this.pos))[0];
1622     ofst=Math.max(tlen-this.scw.size[0]+this.font.getTextSize('s')[0]);
1623     
1624     this.scw.setScrollX(ofst);
1625     this.bliplabel.move([Math.max(this.font.getTextSize(this.text.substr(0, this.pos))[0]+this.label.position[0]-2, 0), this.label.position[1]]);
1626            
1627 };
1628 
1629 /**
1630  *@ignore
1631  */
1632 TextInput.prototype._setText=function(text){
1633     this.text=text;
1634     this.label.setText(text);
1635     this.scw.autoSetScrollableArea();
1636     this.refresh();
1637     this.despatchEvent({'type':EVT_CHANGE,'value':text});
1638 };
1639 
1640 /**
1641  *key down handler
1642  *@ignore
1643  */
1644 TextInput.prototype.onKeyDown=function(event){
1645     var charcode=event.key;
1646     if(charcode==13){
1647         //TODO
1648     }
1649     //BACKSPACE
1650     if(charcode==8){
1651         if(this.text){
1652             if(this.pos==this.text.length){
1653                 this._setText(this.text.substr(0,this.text.length-1));
1654             }else {
1655                 this._setText(this.text.substr(0, this.pos-1)+this.text.substr(this.pos, this.text.length));
1656             }
1657             this.blipon();
1658             this.setPos(this.pos-1);
1659         }
1660     }
1661     //WRITEABLE SYMBOLS, 0 to z or space
1662     if(((charcode>=48) && (charcode<=90))||(charcode==32)){
1663         var c=String.fromCharCode(charcode);
1664         if(event.shiftKey)c=c.toUpperCase();
1665         else c=c.toLowerCase();
1666         if(this.pos==this.text.length){
1667             this._setText(this.text+c);
1668         }else{
1669             this._setText(this.text.substr(0, this.pos)+c+this.text.substr(this.pos, this.text.length));
1670         }
1671         this.setPos(this.pos+1);
1672         this.blipon();
1673     }
1674 
1675     //LEFT
1676     if(charcode==37){
1677         this.setPos(this.pos-1);
1678         this.blipon();
1679     }
1680     //RIGHT
1681     if(charcode==39){
1682         this.setPos(this.pos+1);
1683         this.blipon();
1684     }
1685 };
1686 
1687 /**
1688  *@class a centered dialog position at the top of the GUI. disables and grays out rest of the guy
1689  *@augments Frame
1690  *
1691  *@param {GUI} parent parent GUI object
1692  *@param {Bool} constrain if true, frame cannot be moved out of visible area
1693  *@param {Array} size  array containing width & height of the element, eg. [width, height]
1694  *@param {Array} position position of the view relative to parent, eg. [x, y]. OPTIONAL, default [0, 0]
1695  *@param {gamejs.Surface} surface surface to render this view on, OPTIONAL
1696  *@param {Bool} visible is this view visible? default true
1697  */
1698 
1699 var Dialog=exports.Dialog=function(pars){
1700     pars.position=getCenterPos(pars.parent.size, pars.size);
1701     Dialog.superConstructor.apply(this, [pars]);
1702     
1703 };
1704 
1705 gamejs.utils.objects.extend(Dialog, Frame);
1706 
1707 /**
1708  *lock GUI & show dialog
1709  *@function
1710  */
1711 Dialog.prototype.show=function(){
1712     this.getGUI().lockFrame(this);
1713     Frame.prototype.show.apply(this, []);
1714 };
1715 
1716 /**
1717  *unlock gui & hide dialog
1718  *@function
1719  */
1720 Dialog.prototype.close=function(){
1721     this.getGUI().unlockFrame();
1722     Frame.prototype.close.apply(this, []);
1723 };
1724 
1725 /**
1726  *@class multi-line, line-wrapped text dislay !CONSTRUCTOR PARAMETERS ARE PROVIDED IN A OBJECT LITERAL!
1727  *@augments View
1728  *
1729  *@param {CachedFont} font 
1730  *@param {String} text
1731  *@param {Bool} justify if true, text is justified. By default, it's left-aligned
1732  *@param {View} parent parent element
1733  *@param {Array} size  array containing width & height of the element, eg. [width, height]
1734  *@param {Array} position position of the view relative to parent, eg. [x, y]. OPTIONAL, default [0, 0]
1735  *@param {gamejs.Surface} surface surface to render this view on, OPTIONAL
1736  *@param {Bool} visible is this view visible? OPTIONAL, DEFAULT true
1737  */
1738 var Text=exports.Text=function(pars){
1739     pars.size=[pars.width, 1];
1740     Text.superConstructor.apply(this, [pars]);
1741     this.width=pars.width;
1742     this.font=pars.font ? pars.font : exports.DEFAULT_FONT;
1743     this.setText(pars.text);
1744     this.justify=pars.justify;
1745 };
1746 gamejs.utils.objects.extend(Text, View);
1747 
1748 /**
1749  *set text
1750  *@function
1751  *@param {String} text
1752  */
1753 Text.prototype.setText=function(text){
1754     //wow, is this a hacky mess!
1755     
1756     this.text=text;
1757     this.lines=[];
1758     var nlines=text.split(/\r\n|\r|\n/);
1759     nlines.push(' ');
1760     var line, words;
1761     var i, ci, c, l;
1762     var word='';
1763     var height=0;
1764     var n_line_length=0;
1765     var n_line='';
1766     for(i=0;i<nlines.length;i++){
1767         line=nlines[i];
1768         line+=' ';
1769         for(ci=0;ci<line.length;ci++){
1770             c=line[ci];
1771             if(c==' ' || c=='\t'){
1772                 if(word){
1773                     l=this.font.getTextSize(word)[0];
1774                     if((n_line_length+l>this.size[0]) && (!(n_line==''))){
1775                         this.lines.push({'t':n_line, 'e':false});
1776                         height+=this.font.getTextSize(n_line)[1];
1777                         n_line='';
1778                         n_line_length=0;
1779                         if(word[0]==' ' || word[0]=='\t'){
1780                             word=word.substr(1, word.length);
1781                         }
1782                     }
1783                     n_line+=word;
1784                     n_line_length+=l;
1785                     
1786                 }
1787                 word='';
1788                 word+=c;
1789             }else{
1790                 word+=c;
1791             }
1792         }
1793         if(n_line){
1794             this.lines.push({'t':n_line, 'e':true});
1795             height+=this.font.getTextSize(n_line)[1];
1796             n_line='';
1797             n_line_length=0;
1798         }
1799     }
1800     this.resize([this.width, height]);
1801     this.refresh();
1802 };
1803 
1804 /**
1805  *@function
1806  */
1807 Text.prototype.paint=function(){
1808     View.prototype.paint.apply(this, []);
1809     var pos=0;
1810     this.lines.forEach(function(line){
1811         var sz=this.font.getTextSize(line.t);
1812         var extra_w=0;
1813         if(this.justify && (!line.e)){
1814             var space_count=0;
1815             for(var i=0;i<line.t.length;i++){
1816                 if(line.t[i]==' ') space_count++;
1817             }
1818             extra_w=Math.floor((this.size[0]-sz[0])/space_count);
1819         }
1820         this.font.render(this.surface, line.t, [0, pos], this.font.space_width+extra_w);
1821         pos+=sz[1];
1822     }, this);
1823 }
1824 
1825 /**
1826  *@class root GUI object. Handles gamejs events, frames
1827  *@augments View
1828  *
1829  *@param {gamejs.Surface} surface surface to render GUI on
1830  */
1831 var GUI=exports.GUI=function(surface){
1832     GUI.superConstructor.apply(this, [{'position':[0, 0],
1833                                       'parent':null,
1834                                       'size':surface.getSize(),
1835                                       'surface':surface}]);
1836     this.type='gui';
1837     this.locked_frame=null;
1838     this.frames=[];
1839 };
1840 
1841 gamejs.utils.objects.extend(GUI, View);
1842 
1843 /**
1844  *redraw GUI, if needed
1845  *@function
1846  *@param {Bool} force_redraw if true GUI is redrawn even if tehre are no internal changes
1847  */
1848 GUI.prototype.draw=function(force_redraw){
1849     if(force_redraw)this.refresh();
1850     var painted=View.prototype.draw.apply(this, []);
1851     this.frames.forEach(function(frame){
1852         if(frame.visible && (frame.draw() || painted)){
1853             if(this.locked_frame && (this.locked_frame.id==frame.id)){
1854                 this.refresh();
1855                 this.blur_bg();
1856             }
1857             this.surface.blit(frame.surface, frame.position);
1858         }
1859     }, this);
1860 };
1861 
1862 /**
1863  *Does nothing! don't remove!
1864  *@function
1865  */
1866 GUI.prototype.paint=function(){};
1867 
1868 GUI.prototype.blur_bg=function(){
1869     gamejs.draw.rect(this.surface, 'rgba(192,192, 192, 0.5)', new gamejs.Rect([0, 0], this.size),0); 
1870 };
1871 
1872 /**
1873  *Remove a frame from GUI
1874  *@function
1875  *@param {Frame|Id} frame frame object or id of frame to remove
1876  */
1877 
1878 GUI.prototype.removeFrame=function(frame){
1879     if(typeof(frame)!='number')frame=frame.id;
1880     for(var i=0;i<this.frames.length;i++){
1881         if(this.frames[i].id==frame){
1882             frame=this.frames.splice(i, 1);
1883             this.refresh();
1884             return true;
1885         }
1886     }
1887     return false;
1888 };
1889 
1890 /**
1891  *Move a frame to the top
1892  *@function
1893  *@param {Frame} frame
1894  */
1895 GUI.prototype.moveFrameToTop=function(frame){   
1896     for(var i=0;i<this.frames.length;i++){
1897         var f=this.frames[i];
1898         if(f.id==frame.id){
1899             if(i==this.frames.length-1) return;
1900             this.despatchEvent({'type':EVT_BLUR});
1901             this.frames.splice(i, 1);
1902             this.frames.push(f);
1903             this.refresh();
1904             frame.despatchEvent({'type':EVT_FOCUS});
1905             break;
1906         }
1907     }
1908 };
1909 
1910 /**
1911  *Update GUI and its child objects
1912  *@function
1913  *@param {Number} msDuration miliseconds since last update
1914  */
1915 GUI.prototype.update=function(msDuration){
1916     this.children.forEach(function(child){
1917         child._update(msDuration);  
1918     });
1919     this.frames.forEach(function(frame){
1920         frame._update(msDuration);  
1921     });
1922     
1923 };
1924 
1925 /**
1926  *@ignore
1927  */
1928 GUI.prototype.lockFrame=function(frame){
1929     this.locked_frame=frame;
1930     this.refresh();
1931 };
1932 
1933 /**
1934  *@ignore
1935  */
1936 GUI.prototype.unlockFrame=function(){
1937     this.locked_frame=null;
1938     this.refresh();
1939 };
1940 
1941 /**
1942  *despatch gamejs event
1943  *@function
1944  *@param {gamejs Event| GUI event} event event generated by gamejs, or a GUI event if needed.
1945  */
1946 GUI.prototype.despatchEvent=function(event){
1947     if(event.pos)event.global_pos=event.pos;
1948     
1949     var i, frame;
1950     //dispatching mouse events to frames: if event is dispatched to a frame, don't dispatch it anywhere else.
1951     if(event.type==EVT_MOUSE_DOWN || event.type==EVT_MOUSE_MOTION || event.type==EVT_MOUSE_UP){
1952         var frame;
1953         var hit=false;
1954         var clicked_on=null;
1955         var moused_on=null;
1956         var topframe=null;
1957         for(i=this.frames.length-1; i>=0;i--){
1958             frame=this.frames[i];
1959             
1960             //if frame is locked, dispatch events only to that frame
1961             if(this.locked_frame &&(this.locked_frame.id!=frame.id)) continue;
1962             
1963             if(frame.visible && frame.getRect().collidePoint(event.pos)){
1964                 frame.despatchEvent(cloneEvent(event, frame.position));
1965                 if(event.type==EVT_MOUSE_DOWN){
1966                     clicked_on=i;
1967                 }
1968                 else if(event.type==EVT_MOUSE_MOTION){
1969                     moused_on=i;
1970                 }
1971                 hit=true;
1972                 //mouseout view if mouse is on a frame
1973                 if(frame.isFocused())topframe=i;
1974                 break;
1975             }else{
1976                 //blur frame if focused but clicked somewhere else
1977                 if((event.type==EVT_MOUSE_DOWN) && (frame.isFocused())){
1978                     frame.despatchEvent({'type':EVT_BLUR});
1979                 }
1980             }
1981         }
1982         
1983         //blur everything else if clicked on a frame
1984         if(clicked_on!=null){
1985             View.prototype.despatchEvent.apply(this, [{'type':EVT_BLUR}]);
1986             for(i=0;i<this.frames.length;i++){
1987                 if(i!=clicked_on) this.frames[i].despatchEvent({'type':EVT_BLUR});
1988             }
1989         }
1990          
1991         //mouseout everyhting else if clicked on a frame
1992         if(moused_on!=null){
1993             View.prototype.despatchEvent.apply(this, [{'type':EVT_MOUSE_OUT}]);
1994             for(i=0;i<this.frames.length;i++){
1995                 if(i!=moused_on) this.frames[i].despatchEvent({'type':EVT_MOUSE_OUT});
1996             } 
1997         }
1998         
1999         if(!hit &&(!this.locked_frame)){
2000             View.prototype.despatchEvent.apply(this, [event]);
2001         }
2002         else{
2003             View.prototype.handleEvent.apply(this, [event]);
2004         }
2005         
2006         if(topframe!=null){
2007             this.moveFrameToTop(this.frames[topframe]);      
2008         }
2009         
2010     }else{
2011         if(event.type==EVT_BLUR || event.type==EVT_MOUSE_OUT || event.type==EVT_KEY_DOWN || event.type==EVT_KEY_UP){
2012             this.frames.forEach(function(frame){
2013                 if(frame.visible &&(!this.locked_frame || (this.locked_frame.id==frame.id))){
2014                     frame.despatchEvent(cloneEvent(event, frame.position));
2015                 }
2016             });  
2017         }
2018         
2019         if(!this.locked_frame) View.prototype.despatchEvent.apply(this, [event]);
2020         else View.prototype.handleEvent.apply(this, [event]);
2021     }
2022 };
2023 
2024 var layout=exports.layout={
2025 
2026 /**
2027  *arranges obejcts vertically
2028  *@function
2029  *@name vertical
2030  *@lends layout
2031  *@param {Array} objects a list of gui objects (with the same parent)
2032  *@param {Number} y starting y coordinate, default 0
2033  *@param {Number} space space between objects px, default 0
2034  */
2035 'vertical':function(objects, y, space){
2036         y=y || 0;
2037         space = space || 0;
2038         objects.forEach(function(object){
2039             object.move([object.position[0], y]);
2040             y+=object.size[1]+space;
2041         });
2042         
2043     },
2044 
2045 /**
2046  *arranges obejcts horizontally
2047  *@function
2048  *@name horizontal
2049  *@lends layout
2050  *@param {Array} objects a list of gui objects (with the same parent)
2051  *@param {Number} x startingx coordinate, default 0
2052  *@param {Number} space space between objects px, default 0
2053  */
2054 'horizontal':function(objects, x, space){
2055         x=x || 0;
2056         space = space || 0;
2057         objects.forEach(function(object){
2058             object.move([x, object.position[1]]);
2059             x+=object.size[0]+space;
2060         });
2061     }
2062 };
2063 
2064