1 /+
2 The MIT License (MIT)
3 
4     Copyright (c) <2013> <Oleg Butko (deviator), Anton Akzhigitov (Akzwar)>
5 
6     Permission is hereby granted, free of charge, to any person obtaining a copy
7     of this software and associated documentation files (the "Software"), to deal
8     in the Software without restriction, including without limitation the rights
9     to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10     copies of the Software, and to permit persons to whom the Software is
11     furnished to do so, subject to the following conditions:
12 
13     The above copyright notice and this permission notice shall be included in
14     all copies or substantial portions of the Software.
15 
16     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17     IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18     FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19     AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20     LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21     OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22     THE SOFTWARE.
23 +/
24 
25 module desgui.core.widget;
26 
27 public import desutil.signal;
28 public import desgui.core.event;
29 public import desgui.core.context;
30 
31 import desutil.helpers;
32 
33 alias ref const(ivec2) in_ivec2;
34 alias ref const(DiKeyboardEvent) in_DiKeyboardEvent;
35 alias ref const(DiTextEvent) in_DiTextEvent;
36 alias ref const(DiMouseEvent) in_DiMouseEvent;
37 alias ref const(DiJoyEvent) in_DiJoyEvent;
38 
39 alias vrect!int irect;
40 alias ref const(irect) in_irect;
41 
42 alias ConditionSignal!(in_ivec2, in_DiKeyboardEvent) CondDiKeyboardSignal;
43 alias ConditionSignal!(in_ivec2, in_DiTextEvent )    CondDiTextSignal;
44 alias ConditionSignal!(in_ivec2, in_DiMouseEvent)    CondDiMouseSignal;
45 alias ConditionSignal!(in_ivec2, in_DiJoyEvent)      CondDiJoySignal;
46 alias Signal!(in_irect) ReshapeSignal;
47 
48 class DiWidgetException : DiException 
49 { 
50     this( string msg, string file=__FILE__, int ln=__LINE__ ) @safe pure nothrow
51     { super( msg, file, ln ); } 
52 }
53 
54 interface DiLayout { void opCall( in irect, DiWidget[] ); }
55 
56 struct size_lim_t(T) if( isNumeric!T )
57 {
58     lim_t!T w, h;
59     auto opCall(A,B)( in A old, in B nval ) const
60         if( isCompVector!(2,T,A) && isCompVector!(2,T,B) )
61     { return vec!(2,T)( w( old[0], nval[0] ), h( old[1], nval[1] ) ); }
62 }
63 
64 interface DiArea
65 {
66     bool containts( in vec2 pnt ) const;
67     final bool opBinaryRight(string op)( in vec2 pnt ) const 
68         if( op == "in" )
69     { return containts( pnt ); }
70 }
71 
72 class DiWidget : DiViewport
73 {
74 private:
75     irect bbox;
76 
77     /++ область отрисовки в собственных координатах +/
78     irect draw_rect;
79     
80     class ActiveArea : DiArea
81     {
82         bool containts( in vec2 pnt ) const
83         { return ( ivec2(pnt) in draw_rect ); }
84     }
85 
86     /++ захват фокуса +/
87     bool focus_grab = false;
88 
89     void prepare()
90     {
91         if( parent !is null ) parent.addChild( this );
92         else if( ctx !is null )
93         {
94             // TODO
95             //ctx.makeWindow( this );
96         }
97         else throw new DiWidgetException( "no parent and no context" );
98 
99         act_area = new ActiveArea;
100 
101         changeChildsList.connect({ relayout(); update(); });
102 
103         reshape.connect( (r) 
104         {
105             auto old_bbox_size = bbox.size;
106             bbox.pos = r.pos;
107             bbox.size = size_lim( bbox.size, r.size );
108             if( old_bbox_size != bbox.size ) relayout();
109             draw_rect = irect(0,0,bbox.size);
110         });
111 
112         draw.addBegin({ draw_rect = ctx.drawStack.push( this ); });
113 
114         draw.addEnd(
115         { 
116             if( draw_rect.area > 0 )
117                 foreach_reverse( ch; childs ) 
118                     if( ch !is null && !ch.isDestructed && ch.visible ) 
119                         ch.draw();
120             ctx.drawStack.pull();
121         });
122 
123         static string prepareCond( string name )
124         {
125             import std..string, std.conv;
126             enum fmt = `
127             %1$s.addCondition( (mpos, ev) 
128             {
129                 auto ff = find( mapToLocal( mpos ), EventCode.%2$s );
130                 if( cur != ff ) 
131                 {
132                     if( cur !is null ) cur.release();
133                     cur = ff;
134                     if( cur !is null ) cur.activate();
135                 }
136                 return cur !is null;
137             }, false );
138             %1$s.connectAlt( (mpos, ev) 
139             {
140                 if( cur !is null )
141                     cur.%1$s( ivec2( mapToLocal( mpos ) ), ev );
142             });`;
143             return format( fmt, name, toUpper(name) );
144         }
145 
146         mixin( prepareCond( "keyboard" ) );
147         mixin( prepareCond( "mouse" ) );
148         mixin( prepareCond( "joystick" ) );
149         mixin( prepareCond( "evtext" ) );
150 
151         release.connect(
152         {
153             if( cur !is null ) cur.release(); 
154             if( parent !is null && focus ) focus = false;
155         });
156 
157         idle.connect({ foreach( ch; childs ) ch.idle(); });
158         activate.connect({ if( cur !is null ) cur.activate(); });
159         update.connect({ foreach( ch; childs ) ch.update(); });
160 
161         relayout.connect({ if( layout ) layout( rect, childs ); });
162     }
163 
164 protected:
165 
166     /+ область реакции в собственных координатах +/
167     DiArea act_area;
168 
169     final void setContext( DiContext nctx )
170     {
171         ctx = nctx;
172         foreach( ch; childs )
173             ch.setContext( ctx );
174     }
175 
176     final void addChild( DiWidget e )
177     {
178         if( e is null ) throw new DiWidgetException( "null child added" );
179 
180         void checkThis( DiWidget w )
181         {
182             if( w is this ) throw new DiWidgetException( "cycle parents" );
183             foreach( ch; w.childs ) checkThis( ch );
184         }
185         checkThis( e );
186 
187         if( e.parent !is null ) e.parent.removeChilds( e );
188 
189         e.setContext( ctx );
190         e.parent = this;
191         childs ~= e;
192 
193         changeChildsList();
194     }
195 
196     debug static DiWidget[] garbage;
197 
198     // удаляет из списка дочерних элементов елементы переданного списка
199     final auto removeChilds( DiWidget[] list... )
200     {
201         DiWidget[] buf;
202         DiWidget[] rem;
203 
204         m1:
205         foreach( w; childs ) 
206         {
207             foreach( e; list )
208                 if( w is e || w is null || w.isDestructed ) 
209                 {
210                     rem ~= w;
211                     continue m1;
212                 }
213             buf ~= w;
214         }
215 
216         childs = buf;
217 
218         foreach( ref e; rem )
219             if( e !is null ) 
220                 e.parent = null;
221 
222         if( rem.length ) 
223             changeChildsList();
224 
225         debug garbage ~= rem;
226 
227         return rem;
228     }
229 
230     EmptySignal changeChildsList;
231 
232     /++ пределы для размера bbox +/
233     size_lim_t!int size_lim;
234 
235     /++ принудительное изменение размера bbox, вне зависимости от фиксированности +/
236     final void forceReshape( in irect r )
237     {
238         bool fw = size_lim.w.fix;
239         bool fh = size_lim.h.fix;
240         size_lim.w.fix = false;
241         size_lim.h.fix = false;
242         reshape( r );
243         size_lim.w.fix = fw;
244         size_lim.h.fix = fh;
245     }
246 
247     /++ обрабатывает ли элемент события +/
248     ubyte processEventMask = EventCode.ALL;
249 
250     /++ контекст +/
251     DiContext ctx;
252 
253     /++ родительский элемент +/
254     DiWidget parent;
255 
256     /++ список дочерних элементов +/
257     DiWidget[] childs;
258 
259     /++ текущий дочерний элемент +/
260     DiWidget cur;
261 
262     /++ внутреннее смещение области для дочерних элементов +/
263     ivec2 inner_offset = ivec2(0,0);
264 
265     vec2 inner_scale = vec2(1,1);
266 
267     final @property
268     {
269         bool focus() const { return focus_grab; }
270         void focus( bool g )
271         {
272             if( focus_grab == g ) return;
273             focus_grab = g;
274             if( parent ) parent.focus = g;
275         }
276     }
277 
278     enum EventCode
279     {
280         NONE = cast(ubyte)0,
281         KEYBOARD    = 0b0001,
282         MOUSE       = 0b0010,
283         JOYSTICK    = 0b0100,
284         EVTEXT      = 0b1000,
285         ALL         = ubyte.max
286     }
287 
288     /++ поиск дочернего элемента по локальному положению мыши и коду события +/
289     DiWidget find( in vec2 mpos, ubyte evcode=EventCode.ALL )
290     {
291         /+ если фокус захвачен, поиск не производится +/
292         if( focus_grab ) return cur;
293 
294         foreach( v; childs )
295         {
296             if( v !is null && (v.processEventMask & evcode) && 
297                 v.is_visible && vec2(mpos-v.rect.pos) in v.activeArea )
298                 return v;
299         }
300 
301         return null;
302     }
303 
304     bool is_visible = true;
305 
306 public:
307 
308     this( DiWidget par )
309     {
310         parent = par;
311         prepare();
312     }
313 
314     this( DiContext context )
315     {
316         parent = null;
317         ctx = context;
318         prepare();
319     }
320 
321     @property
322     {
323         nothrow bool visible() const { return is_visible; }
324 
325         void visible( bool vis ) 
326         {
327             if( is_visible != vis )
328             {
329                 is_visible = vis; 
330                 if( parent ) parent.relayout();
331                 if( !vis ) release(); 
332             }
333         }
334 
335         nothrow irect drawRect() const { return draw_rect; }
336 
337         vec2 offset() const { return vec2(inner_offset); }
338         void offset( in vec2 o ) { inner_offset = ivec2(o); }
339 
340         vec2 scale() const { return inner_scale; }
341         void scale( in vec2 s ) { inner_scale = s; }
342 
343         const(DiArea) activeArea() const { return act_area; }
344 
345         final
346         {
347             /++ возвращает копию прямоугольника +/
348             irect rect() const { return bbox; }
349 
350             /++ вызывает сигнал reshape +/
351             void rect( in irect r ) { reshape( r ); }
352 
353             /++ возвращает копию пределов размера прямоугольника +/
354             nothrow size_lim_t!int lims() const { return size_lim; }
355         }
356     }
357 
358     void reparent( DiWidget npar )
359     {
360         parent.removeChilds( this );
361         npar.addChild( this );
362     }
363 
364     ReshapeSignal reshape;
365 
366     DiLayout layout;
367     EmptySignal relayout;
368                              
369     EmptySignal activate;
370     EmptySignal release;
371     EmptySignal update;
372 
373     EmptySignal idle;
374 
375     SignalBoxNoArgs draw;
376 
377     CondDiKeyboardSignal keyboard;
378     CondDiMouseSignal mouse;
379     CondDiJoySignal joystick;
380     CondDiTextSignal evtext;
381 
382     EmptySignal onDestruct;
383 
384     final void destruct()
385     {
386         onDestruct();
387 
388         release();
389         if( focus ) focus = 0;
390 
391         foreach( ref ch; childs ) ch.parent = null;
392         childs.length = 0;
393 
394         if( parent ) parent.removeChilds( this );
395 
396         reshape.clear();
397         relayout.clear();
398         activate.clear();
399         release.clear();
400         update.clear();
401         idle.clear();
402         draw.clear();
403         keyboard.clear();
404         mouse.clear();
405         joystick.clear();
406         evtext.clear();
407         changeChildsList.clear();
408         onDestruct.clear();
409 
410         parent = null;
411         destructed = true;
412     }
413 
414     private bool destructed = false;
415     final @property bool isDestructed() const { return destructed; }
416 }
417 
418 unittest
419 {
420     auto ctx = new TestContext;
421     scope par = new DiWidget( ctx );
422     par.reshape( irect(0,0,100,100) );
423     assert( par.rect == irect(0,0,100,100) );
424     par.reshape( irect(8,5,20,14) );
425     assert( par.rect == irect(8,5,20,14) );
426 
427     assert( par.visible );
428     par.reshape( irect(0,0,0,0) );
429     assert( par.visible );
430 
431     par.reshape( irect(0,0,100,100) );
432 
433     assert( par.drawRect == par.rect );
434     assert( par.offset == vec2(0,0) );
435 
436     par.draw();
437     assert( par.drawRect == par.rect );
438 
439     bool resh = false;
440     par.reshape.connect((r){ resh = true; });
441     assert( !resh );
442     par.rect = par.rect;
443     assert( resh );
444 
445     par.visible = false;
446     assert( !par.visible );
447     par.visible = true;
448 
449     scope ch1 = new DiWidget( par );
450     assert( ch1.visible );
451     assert( par.childs.length == 1 );
452     assert( ch1.parent is par );
453 
454     par.removeChilds( ch1 );
455     assert( par.childs.length == 0 );
456     assert( ch1.parent is null );
457 
458     par.addChild( ch1 );
459     assert( par.childs.length == 1 );
460     assert( ch1.parent is par );
461 
462     scope ctx2 = new TestContext;
463     scope par2 = new DiWidget( ctx2 );
464 
465     ch1.reparent( par2 );
466     assert( par.childs.length == 0 );
467     assert( par2.childs.length == 1 );
468     assert( ch1.parent is par2 );
469 
470     foreach( i; 0 .. 100 )
471         auto cc = new DiWidget( par );
472 
473     void sig()
474     {
475         par.activate();
476         par.update();
477         par.idle();
478         par.draw();
479         par.release();
480     }
481 
482     sig();
483 
484     par.removeChilds( par.childs );
485     assert( par.childs.length == 0 );
486 
487     sig();
488 
489     DiWidget[] list;
490     DiKeyboardEvent check;
491     auto origin = DiKeyboardEvent( true, false, 0, 1, 2 );
492     ivec2 mpos;
493     foreach( i; 0 .. 100 )
494     {
495         auto cc = new DiWidget( par );
496         if( i == 33 ) 
497         {
498             cc.keyboard.connect((m,e){ mpos = m; check = e; });
499             cc.reshape( irect(10,10,10,10) );
500         }
501         list ~= cc;
502     }
503     assert( par.childs.length == 100 );
504 
505     par.keyboard( ivec2( 15, 15 ), origin );
506     assert( check == origin );
507     assert( mpos == ivec2(15,15) );
508     mpos = ivec2( 666,666 );
509     check = DiKeyboardEvent( true, false, 2, 0, 1 );
510 
511     foreach( i; 50 .. 100 )
512         list[i].reparent( par2 );
513 
514     par.offset = vec2(5,5);
515     par.keyboard( ivec2( 15, 15 ), origin );
516     assert( check == origin );
517     assert( mpos == ivec2(10,10) );
518     mpos = ivec2( 666,666 );
519     check = DiKeyboardEvent( true, false, 2, 0, 1 );
520 
521     par.reshape( irect(-2,-2,100,100) );
522     par.keyboard( ivec2( 15, 15 ), origin );
523     assert( check == origin );
524     assert( mpos == ivec2(12,12) );
525     mpos = ivec2( 666,666 );
526     check = DiKeyboardEvent( true, false, 2, 0, 1 );
527 
528     sig();
529 
530     par.removeChilds( par.childs );
531     par.keyboard( ivec2( 15, 15 ), origin );
532     assert( check != origin );
533 
534     assert( par.childs.length == 0 );
535     assert( par2.childs.length == 51 );
536 }
537 
538 unittest
539 {
540     auto ctx = new TestContext;
541     scope par = new DiWidget( ctx );
542     par.reshape( irect(20,20,100,40) );
543     par.scale = vec2(0.5,0.5);
544     ivec2 mpos = ivec2(24,29);
545     assert( par.mapToLocal(mpos) == vec2(8,18) );
546     par.offset = vec2(18,32);
547     assert( par.mapToLocal(mpos) == vec2(-28,-46) );
548 }