1 /** 2 * ae.ui.wm.controls.control 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <ae@cy.md> 12 */ 13 14 module ae.ui.wm.controls.control; 15 16 import std.algorithm : max; 17 18 import ae.ui.shell.events; 19 import ae.ui.video.renderer; 20 21 /// Root control class. 22 class Control 23 { 24 /// Geometry. 25 int x, y, w, h; 26 27 /// Event handlers. 28 void handleMouseDown(int x, int y, MouseButton button) {} 29 void handleMouseUp(int x, int y, MouseButton button) {} /// ditto 30 void handleMouseMove(int x, int y, MouseButtons buttons) {} /// ditto 31 32 /// Renderer. 33 abstract void render(Renderer r, int x, int y); 34 35 /// Parent getter. 36 final @property ContainerControl parent() 37 { 38 return _parent; 39 } 40 41 /// Parent setter. 42 final @property void parent(ContainerControl newParent) 43 { 44 if (_parent) 45 _parent._removeChild(this); 46 _parent = newParent; 47 _parent._addChild(this); 48 } 49 50 /// rw and rh are recommended (hint) sizes that the parent is allocating to the child, 51 /// but there is no obligation to follow them. 52 protected void arrange(int rw, int rh) { } 53 54 /// Called when a child's dimensions change, to allow the size change to bubble up to parents. 55 final void rearrange() 56 { 57 auto oldW = w, oldH = h; 58 arrange(w, h); 59 if (parent && (w != oldW || h != oldH)) 60 parent.rearrange(); 61 } 62 63 private: 64 ContainerControl _parent; 65 } 66 67 // *************************************************************************** 68 69 /// An abstract base class for a control with children. 70 class ContainerControl : Control 71 { 72 /// Find the child control at the given coordinates. 73 final Control controlAt(int x, int y) 74 { 75 foreach (child; children) 76 if (x>=child.x && x<child.x+child.w && y>=child.y && y<child.y+child.h) 77 return child; 78 return null; 79 } 80 81 /// Propagates the event to the control at the given coordinates. 82 override void handleMouseDown(int x, int y, MouseButton button) 83 { 84 auto child = controlAt(x, y); 85 if (child) 86 child.handleMouseDown(x-child.x, y-child.y, button); 87 } 88 89 override void handleMouseUp(int x, int y, MouseButton button) 90 { 91 auto child = controlAt(x, y); 92 if (child) 93 child.handleMouseUp(x-child.x, y-child.y, button); 94 } /// ditto 95 96 override void handleMouseMove(int x, int y, MouseButtons buttons) 97 { 98 auto child = controlAt(x, y); 99 if (child) 100 child.handleMouseMove(x-child.x, y-child.y, buttons); 101 } /// ditto 102 103 /// Renders all children. 104 override void render(Renderer s, int x, int y) 105 { 106 // background should be rendered by a subclass or parent 107 foreach (child; children) 108 child.render(s, x+child.x, y+child.y); 109 } 110 111 /// Children getter. 112 final @property Control[] children() 113 { 114 return _children; 115 } 116 117 /// Makes this control the given control's parent. 118 final typeof(this) addChild(Control control) 119 { 120 control.parent = this; 121 return this; 122 } 123 124 private: 125 // An array should be fine, performance-wise. 126 // UI manipulations should be infrequent. 127 Control[] _children; 128 129 final void _addChild(Control target) 130 { 131 _children ~= target; 132 } 133 134 final void _removeChild(Control target) 135 { 136 foreach (i, child; _children) 137 if (child is target) 138 { 139 _children = _children[0..i] ~ _children[i+1..$]; 140 return; 141 } 142 assert(false, "Attempting to remove inexisting child"); 143 } 144 } 145 146 /// Container with static child positions. 147 /// Does not rearrange its children. 148 /// Dimensions are bound by the lowest/right-most child. 149 class StaticFitContainerControl : ContainerControl 150 { 151 override void arrange(int rw, int rh) 152 { 153 int maxX, maxY; 154 foreach (child; children) 155 { 156 maxX = max(maxX, child.x + child.w); 157 maxY = max(maxY, child.y + child.h); 158 } 159 w = maxX; 160 h = maxY; 161 } /// 162 } 163 164 // *************************************************************************** 165 166 /// Allow specifying a size as a combination of parent size % and pixels. 167 /// Sizes are summed together. 168 struct RelativeSize 169 { 170 int px; /// Flat size in pixels. 171 float ratio; /// Parent size ratio. 172 // TODO: Add "em", when we have variable font sizes? 173 174 /// Get total resulting size. 175 int toPixels(int parentSize) pure const { return px + cast(int)(parentSize*ratio); } 176 177 RelativeSize opBinary(string op)(RelativeSize other) 178 if (op == "+" || op == "-") 179 { 180 return mixin("RelativeSize(this.px"~op~"other.px, this.ratio"~op~"other.ratio)"); 181 } /// 182 } 183 184 /// Usage: 50.px 185 @property RelativeSize px(int px) { return RelativeSize(px, 0); } 186 /// Usage: 25.percent 187 @property RelativeSize percent(float percent) { return RelativeSize(0, percent/100f); } 188 189 // *************************************************************************** 190 191 /// No-op wrapper 192 class Wrapper : ContainerControl 193 { 194 override void arrange(int rw, int rh) 195 { 196 assert(children.length == 1, "Wrapper does not have exactly one child"); 197 auto child = children[0]; 198 child.arrange(rw, rh); 199 this.w = child.w; 200 this.h = child.h; 201 } /// 202 } 203 204 205 /// Provides default implementations for wrapper behavior methods 206 mixin template ComplementWrapperBehavior(alias WrapperBehavior, Params...) 207 { 208 final: 209 mixin WrapperBehavior; 210 211 void _moreMagic() {} 212 213 /// The default implementations. 214 static if (!is(typeof(adjustHint))) 215 int adjustHint(int hint, Params params) { return hint; } 216 static if (!is(typeof(adjustSize))) 217 int adjustSize(int size, int hint, Params params) { return size; } /// ditto 218 static if (!is(typeof(adjustPos))) 219 int adjustPos(int pos, int size, int hint, Params params) { return pos; } /// ditto 220 } 221 222 private mixin template OneDirectionCustomWrapper(alias WrapperBehavior, Params...) 223 { 224 private Params params; 225 static if (Params.length) 226 this(Params params) 227 { 228 this.params = params; 229 } 230 231 /// Declares adjustHint, adjustSize, adjustPos 232 mixin ComplementWrapperBehavior!(WrapperBehavior, Params); 233 } 234 235 private class WCustomWrapper(alias WrapperBehavior, Params...) : Wrapper 236 { 237 override void arrange(int rw, int rh) 238 { 239 assert(children.length == 1, "Wrapper does not have exactly one child"); 240 auto child = children[0]; 241 child.arrange(adjustHint(rw, params), rh); 242 this.w = adjustSize(child.w, rw, params); 243 this.h = child.h; 244 child.x = adjustPos(child.x, child.w, rw, params); 245 } /// 246 247 mixin OneDirectionCustomWrapper!(WrapperBehavior, Params); 248 } 249 250 private class HCustomWrapper(alias WrapperBehavior, Params...) : Wrapper 251 { 252 override void arrange(int rw, int rh) 253 { 254 assert(children.length == 1, "Wrapper does not have exactly one child"); 255 auto child = children[0]; 256 child.arrange(rw, adjustHint(rh, params)); 257 this.w = child.w; 258 this.h = adjustSize(child.h, rh, params); 259 child.y = adjustPos(child.y, child.h, rh, params); 260 } 261 262 mixin OneDirectionCustomWrapper!(WrapperBehavior, Params); 263 } 264 265 private class CustomWrapper(alias WrapperBehavior, Params...) : Wrapper 266 { 267 override void arrange(int rw, int rh) 268 { 269 assert(children.length == 1, "Wrapper does not have exactly one child"); 270 auto child = children[0]; 271 child.arrange(adjustHint(rw, paramsX), adjustHint(rh, paramsY)); 272 this.w = adjustSize(child.w, rw, paramsX); 273 this.h = adjustSize(child.h, rh, paramsY); 274 child.x = adjustPos(child.x, child.w, rw, paramsX); 275 child.y = adjustPos(child.y, child.h, rh, paramsY); 276 } 277 278 private Params paramsX, paramsY; 279 static if (Params.length) 280 this(Params paramsX, Params paramsY) 281 { 282 this.paramsX = paramsX; 283 this.paramsY = paramsY; 284 } 285 286 /// Declares adjustHint, adjustSize, adjustPos 287 mixin ComplementWrapperBehavior!(WrapperBehavior, Params); 288 } 289 290 mixin template _DeclareWrapper(string name, alias WrapperBehavior, Params...) 291 { 292 mixin(`alias WCustomWrapper!(WrapperBehavior, Params) W`~name~`;`); 293 mixin(`alias HCustomWrapper!(WrapperBehavior, Params) H`~name~`;`); 294 mixin(`alias CustomWrapper!(WrapperBehavior, Params) `~name~`;`); 295 } 296 297 private mixin template SizeBehavior() 298 { 299 int adjustHint(int hint, RelativeSize size) 300 { 301 return size.toPixels(hint); 302 } 303 } 304 /// Wrapper to override the parent hint to a specific size. 305 mixin _DeclareWrapper!("Size", SizeBehavior, RelativeSize); 306 307 private mixin template ShrinkBehavior() 308 { 309 int adjustHint(int hint) 310 { 311 return 0; 312 } 313 } 314 /// Wrapper to override the parent hint to 0, thus making 315 /// the wrapped control as small as it can be. 316 mixin _DeclareWrapper!("Shrink", ShrinkBehavior); 317 318 private mixin template CenterBehavior() 319 { 320 int adjustSize(int size, int hint) 321 { 322 return max(size, hint); 323 } 324 325 int adjustPos(int pos, int size, int hint) 326 { 327 if (hint < size) hint = size; 328 return (hint-size)/2; 329 } 330 } 331 /// If content is smaller than parent hint, center the content and use parent hint for own size. 332 mixin _DeclareWrapper!("Center", CenterBehavior); 333 334 private mixin template PadBehavior() 335 { 336 int adjustHint(int hint, RelativeSize padding) 337 { 338 auto paddingPx = padding.toPixels(hint); 339 return max(0, hint - paddingPx*2); 340 } 341 342 int adjustSize(int size, int hint, RelativeSize padding) 343 { 344 auto paddingPx = padding.toPixels(hint); 345 return size + paddingPx*2; 346 } 347 348 int adjustPos(int pos, int size, int hint, RelativeSize padding) 349 { 350 auto paddingPx = padding.toPixels(hint); 351 return paddingPx; 352 } 353 } 354 /// Add some padding on both sides of the content. 355 mixin _DeclareWrapper!("Pad", PadBehavior, RelativeSize); 356 357 // *************************************************************************** 358 359 /// Space out controls in a 2D grid, according to their dimensions and resizability. 360 class Table : ContainerControl 361 { 362 /// Table size. 363 uint rows, cols; 364 365 this(uint rows, uint cols) 366 { 367 this.rows = rows; 368 this.cols = cols; 369 } /// 370 371 override void arrange(int rw, int rh) 372 { 373 assert(children.length == rows*cols, "Wrong number of table children"); 374 375 static struct Size { int w, h; } 376 Size[][] minSizes = new Size[][](cols, rows); 377 int[] minColSizes = new int[cols]; 378 int[] minRowSizes = new int[rows]; 379 380 foreach (i, child; children) 381 { 382 child.arrange(0, 0); 383 auto col = i % cols; 384 auto row = i / cols; 385 minSizes[row][col] = Size(child.w, child.h); 386 minColSizes[col] = max(minColSizes[col], child.w); 387 minRowSizes[row] = max(minRowSizes[row], child.h); 388 } 389 390 import std.algorithm; 391 int minW = reduce!"a + b"(0, minColSizes); 392 int minH = reduce!"a + b"(0, minRowSizes); 393 394 // If all controls can take up no space, spread them out equivalently 395 if (minW == 0) { minW = cols; minColSizes[] = 1; } 396 if (minH == 0) { minH = rows; minRowSizes[] = 1; } 397 398 // TODO: fixed-size rows / columns 399 // Maybe associate RelativeSize values with rows/columns? 400 401 this.w = max(minW, rw); 402 this.h = max(minH, rh); 403 404 int[] colSizes = new int[cols]; 405 int[] colOffsets = new int[cols]; 406 int p = 0; 407 foreach (col; 0..cols) 408 { 409 colOffsets[col] = p; 410 auto size = minW ? minColSizes[col] * this.w / minW : 0; 411 colSizes[col] = size; 412 p += size; 413 } 414 415 int[] rowSizes = new int[rows]; 416 int[] rowOffsets = new int[rows]; 417 p = 0; 418 foreach (row; 0..rows) 419 { 420 rowOffsets[row] = p; 421 auto size = minH ? minRowSizes[row] * this.h / minH : 0; 422 rowSizes[row] = size; 423 p += size; 424 } 425 426 foreach (i, child; children) 427 { 428 auto col = i % cols; 429 auto row = i / cols; 430 child.x = colOffsets[col]; 431 child.y = rowOffsets[row]; 432 child.arrange(colSizes[col], rowSizes[col]); 433 } 434 } /// 435 } 436 437 /// 1D table for a row of controls. 438 class Row : Table 439 { 440 this() { super(0, 0); } /// 441 442 override void arrange(int rw, int rh) 443 { 444 rows = 1; 445 cols = cast(uint)children.length; 446 super.arrange(rw, rh); 447 } /// 448 } 449 450 /// 1D table for a column of controls. 451 class Column : Table 452 { 453 this() { super(0, 0); } /// 454 455 override void arrange(int rw, int rh) 456 { 457 rows = cast(uint)children.length; 458 cols = 1; 459 super.arrange(rw, rh); 460 } /// 461 } 462 463 // *************************************************************************** 464 465 /// All children occupy the entire area of the control. 466 /// The control grows as necessary to accommodate all layers. 467 class Layers : ContainerControl 468 { 469 override void arrange(int rw, int rh) 470 { 471 w = rw; h = rh; 472 bool changed; 473 do 474 { 475 changed = false; 476 foreach (child; children) 477 { 478 child.arrange(w, h); 479 if (child.w > w) 480 w = child.w, changed = true; 481 if (child.h > h) 482 h = child.h, changed = true; 483 } 484 } while (changed); 485 } /// 486 } 487 488 // *************************************************************************** 489 490 /// Container for all top-level windows. 491 /// The root control's children are, semantically, layers. 492 final class RootControl : ContainerControl 493 { 494 override void arrange(int rw, int rh) 495 { 496 foreach (child; children) 497 child.arrange(w, h); 498 } /// 499 500 // Expose "arrange", which is "protected", to `WMApplication` 501 /// Called by `WMApplication` to notify of size changes. 502 final void sizeChanged() 503 { 504 arrange(w, h); 505 } 506 }