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