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 }