1 /// 2 module fluid.style; 3 4 import std.math; 5 import std.range; 6 import std.format; 7 import std.typecons; 8 import std.algorithm; 9 10 import fluid.node; 11 import fluid.utils; 12 import fluid.backend; 13 import fluid.typeface; 14 15 import fluid.io.canvas; 16 17 public import fluid.theme : makeTheme, Theme, Selector, rule, Rule, when, WhenRule, children, ChildrenRule, Field, 18 Breadcrumbs; 19 public import fluid.border; 20 public import fluid.default_theme; 21 public import fluid.backend : color; 22 23 24 @safe: 25 26 27 /// Contains the style for a node. 28 struct Style { 29 30 enum Themable; 31 32 enum Side { 33 34 left, right, top, bottom, 35 36 } 37 38 // Text options 39 @Themable { 40 41 /// Main typeface to be used for text. 42 /// 43 /// Changing the typeface requires a resize. 44 Typeface typeface; 45 46 alias font = typeface; 47 48 /// Size of the font in use, in pixels. 49 /// 50 /// Changing the size requires a resize. 51 float fontSize = 14.pt; 52 53 /// Text color. 54 auto textColor = Color(0, 0, 0, 0); 55 56 } 57 58 // Background & content 59 @Themable { 60 61 /// Color of lines belonging to the node, especially important to separators and sliders. 62 auto lineColor = Color(0, 0, 0, 0); 63 64 /// Background color of the node. 65 auto backgroundColor = Color(0, 0, 0, 0); 66 67 /// Background color for selected text. 68 auto selectionBackgroundColor = Color(0, 0, 0, 0); 69 70 } 71 72 // Spacing 73 @Themable { 74 75 /// Margin (outer margin) of the node. `[left, right, top, bottom]`. 76 /// 77 /// Updating margins requires a resize. 78 /// 79 /// See: `isSideArray`. 80 float[4] margin = 0; 81 82 /// Border size, placed between margin and padding. `[left, right, top, bottom]`. 83 /// 84 /// Updating border requires a resize. 85 /// 86 /// See: `isSideArray` 87 float[4] border = 0; 88 89 /// Padding (inner margin) of the node. `[left, right, top, bottom]`. 90 /// 91 /// Updating padding requires a resize. 92 /// 93 /// See: `isSideArray` 94 float[4] padding = 0; 95 96 /// Margin/gap between two neighboring elements; for container nodes that support it. 97 /// 98 /// Updating the gap requires a resize. 99 float[2] gap = 0; 100 101 /// Border style to use. 102 /// 103 /// Updating border requires a resize. 104 FluidBorder borderStyle; 105 106 } 107 108 // Misc 109 public { 110 111 /// Apply tint to all node contents, including children. 112 @Themable 113 Color tint = Color(0xff, 0xff, 0xff, 0xff); 114 115 /// Cursor icon to use while this node is hovered. 116 /// 117 /// Custom image cursors are not supported yet. 118 @Themable 119 FluidMouseCursor mouseCursor; 120 121 /// Additional information for the node the style applies to. 122 /// 123 /// Ignored if mismatched. 124 @Themable 125 Node.Extra extra; 126 127 /// Get or set node opacity. Value in range [0, 1] — 0 is fully transparent, 1 is fully opaque. 128 float opacity() const { 129 130 return tint.a / 255.0f; 131 132 } 133 134 /// ditto 135 float opacity(float value) { 136 137 tint.a = cast(ubyte) clamp(value * ubyte.max, ubyte.min, ubyte.max); 138 139 return value; 140 141 } 142 143 } 144 145 public { 146 147 /// Breadcrumbs associated with this style. Used to keep track of tree-aware theme selectors, such as 148 /// `children`. Does not include breadcrumbs loaded by parent nodes. 149 Breadcrumbs breadcrumbs; 150 151 } 152 153 private this(Typeface typeface) { 154 155 this.typeface = typeface; 156 157 } 158 159 static Typeface defaultTypeface() { 160 161 return Typeface.defaultTypeface; 162 163 } 164 165 static Typeface loadTypeface(string file) { 166 167 return new FreetypeTypeface(file); 168 169 } 170 171 alias loadFont = loadTypeface; 172 173 bool opCast(T : bool)() const { 174 175 return this !is Style(null); 176 177 } 178 179 bool opEquals(const Style other) const @trusted { 180 181 // @safe: FluidBorder and Typeface are required to provide @safe opEquals. 182 // D doesn't check for opEquals on interfaces, though. 183 return this.tupleof == other.tupleof; 184 185 } 186 187 /// Set current DPI. 188 void setDPI(Vector2 dpi) { 189 190 getTypeface.setSize(dpi, fontSize); 191 192 } 193 194 /// Get current typeface, or fallback to default. 195 Typeface getTypeface() { 196 197 return either(typeface, Typeface.defaultTypeface); 198 199 } 200 201 const(Typeface) getTypeface() const { 202 203 return either(typeface, Typeface.defaultTypeface); 204 205 } 206 207 /// Draw the background & border. 208 void drawBackground(FluidBackend backend, Rectangle rect) const { 209 210 backend.drawRectangle(rect, backgroundColor); 211 212 // Add border if active 213 if (borderStyle) { 214 215 borderStyle.apply(backend, rect, border); 216 217 } 218 219 } 220 221 /// ditto 222 void drawBackground(FluidBackend backend, CanvasIO io, Rectangle rect) const { 223 224 // New I/O system used 225 if (io) { 226 227 const ioBorder = cast(const FluidIOBorder) borderStyle; 228 229 io.drawRectangle(rect, backgroundColor); 230 231 // Draw border if present and compatible 232 if (ioBorder) { 233 ioBorder.apply(io, rect, border); 234 } 235 236 } 237 238 // Old Backend system 239 else drawBackground(backend, rect); 240 241 } 242 243 /// Draw a line. 244 void drawLine(FluidBackend backend, Vector2 start, Vector2 end) const { 245 246 backend.drawLine(start, end, lineColor); 247 248 } 249 250 /// ditto 251 void drawLine(FluidBackend backend, CanvasIO canvasIO, Vector2 start, Vector2 end) const { 252 253 // New I/O system used 254 if (canvasIO) { 255 256 canvasIO.drawLine(start, end, 1, lineColor); 257 258 } 259 260 else drawLine(backend, start, end); 261 262 } 263 264 /// Get a side array holding both the regular margin and the border. 265 float[4] fullMargin() const { 266 267 return [ 268 margin.sideLeft + border.sideLeft, 269 margin.sideRight + border.sideRight, 270 margin.sideTop + border.sideTop, 271 margin.sideBottom + border.sideBottom, 272 ]; 273 274 } 275 276 /// Remove padding from the vector representing size of a box. 277 Vector2 contentBox(Vector2 size) const { 278 279 return cropBox(size, padding); 280 281 } 282 283 /// Remove padding from the given rect. 284 Rectangle contentBox(Rectangle rect) const { 285 286 return cropBox(rect, padding); 287 288 } 289 290 /// Get a sum of margin, border size and padding. 291 float[4] totalMargin() const { 292 293 float[4] ret = margin[] + border[] + padding[]; 294 return ret; 295 296 } 297 298 /// Crop the given box by reducing its size on all sides. 299 static Vector2 cropBox(Vector2 size, float[4] sides) { 300 301 size.x = max(0, size.x - sides.sideLeft - sides.sideRight); 302 size.y = max(0, size.y - sides.sideTop - sides.sideBottom); 303 304 return size; 305 306 } 307 308 /// ditto 309 static Rectangle cropBox(Rectangle rect, float[4] sides) { 310 311 rect.x += sides.sideLeft; 312 rect.y += sides.sideTop; 313 314 const size = cropBox(Vector2(rect.w, rect.h), sides); 315 rect.width = size.x; 316 rect.height = size.y; 317 318 return rect; 319 320 } 321 322 } 323 324 /// Side array is a static array defining a property separately for each side of a box, for example margin and border 325 /// size. Order is as follows: `[left, right, top, bottom]`. You can use `Style.Side` to index this array with an enum. 326 /// 327 /// Because of the default behavior of static arrays, one can set the value for all sides to be equal with a simple 328 /// assignment: `array = 8`. Additionally, to make it easier to manipulate the box, one may use the `sideX` and `sideY` 329 /// functions to get a `float[2]` array of the values corresponding to the given axis (which can also be assigned like 330 /// `array.sideX = 8`) or the `sideLeft`, `sideRight`, `sideTop` and `sideBottom` functions corresponding to the given 331 /// sides. 332 enum isSideArray(T) = is(T == X[4], X) && T.length == 4; 333 334 /// ditto 335 enum isSomeSideArray(T) = isSideArray!T 336 || (is(T == Field!(name, U), string name, U) && isSideArray!U); 337 338 /// 339 unittest { 340 341 float[4] sides; 342 static assert(isSideArray!(float[4])); 343 344 sides.sideX = 4; 345 346 assert(sides.sideLeft == sides.sideRight); 347 assert(sides.sideLeft == 4); 348 349 sides = 8; 350 assert(sides == [8, 8, 8, 8]); 351 assert(sides.sideX == sides.sideY); 352 353 } 354 355 /// An axis array is similar to a size array, but does not distinguish between invididual directions on a single axis. 356 /// Thus, it contains only two values, one for the X axis, and one for the Y axis. 357 /// 358 /// `sideX` and `sideY` can be used to access individual items of an axis array by name. 359 enum isAxisArray(T) = is(T == X[2], X) && T.length == 2; 360 361 static assert(!isSideArray!(float[2])); 362 static assert( isSideArray!(float[4])); 363 364 static assert( isAxisArray!(float[2])); 365 static assert(!isAxisArray!(float[4])); 366 367 /// Get a reference to the left, right, top or bottom side of the given side array. 368 auto ref sideLeft(T)(return auto ref inout T sides) 369 if (isSomeSideArray!T) { 370 371 return sides[Style.Side.left]; 372 373 } 374 375 /// ditto 376 auto ref sideRight(T)(return auto ref inout T sides) 377 if (isSomeSideArray!T) { 378 379 return sides[Style.Side.right]; 380 381 } 382 383 /// ditto 384 auto ref sideTop(T)(return auto ref inout T sides) 385 if (isSomeSideArray!T) { 386 387 return sides[Style.Side.top]; 388 389 } 390 391 /// ditto 392 auto ref sideBottom(T)(return auto ref inout T sides) 393 if (isSomeSideArray!T) { 394 395 return sides[Style.Side.bottom]; 396 397 } 398 399 /// 400 unittest { 401 402 float[4] sides = [8, 0, 4, 2]; 403 404 assert(sides.sideRight == 0); 405 406 sides.sideRight = 8; 407 sides.sideBottom = 4; 408 409 assert(sides == [8, 8, 4, 4]); 410 411 } 412 413 /// Get a reference to the X axis for the given side or axis array. 414 ref inout(ElementType!T[2]) sideX(T)(return ref inout T sides) 415 if (isSideArray!T) { 416 417 const start = Style.Side.left; 418 return sides[start .. start + 2]; 419 420 } 421 422 /// ditto 423 auto ref sideX(T)(return auto ref inout T sides) 424 if (isSomeSideArray!T && !isSideArray!T) { 425 426 const start = Style.Side.left; 427 return sides[start .. start + 2]; 428 429 } 430 431 /// ditto 432 ref inout(ElementType!T) sideX(T)(return ref inout T sides) 433 if (isAxisArray!T) { 434 435 return sides[0]; 436 437 } 438 439 /// Get a reference to the Y axis for the given side or axis array. 440 ref inout(ElementType!T[2]) sideY(T)(return ref inout T sides) 441 if (isSideArray!T) { 442 443 const start = Style.Side.top; 444 return sides[start .. start + 2]; 445 446 } 447 448 /// ditto 449 auto ref sideY(T)(return auto ref inout T sides) 450 if (isSomeSideArray!T && !isSideArray!T) { 451 452 const start = Style.Side.top; 453 return sides[start .. start + 2]; 454 455 } 456 457 /// ditto 458 ref inout(ElementType!T) sideY(T)(return ref inout T sides) 459 if (isAxisArray!T) { 460 461 return sides[1]; 462 463 } 464 465 /// Assigning values to an axis of a side array. 466 unittest { 467 468 float[4] sides = [1, 2, 3, 4]; 469 470 assert(sides.sideX == [sides.sideLeft, sides.sideRight]); 471 assert(sides.sideY == [sides.sideTop, sides.sideBottom]); 472 473 sides.sideX = 8; 474 475 assert(sides == [8, 8, 3, 4]); 476 477 sides.sideY = sides.sideBottom; 478 479 assert(sides == [8, 8, 4, 4]); 480 481 } 482 483 /// Operating on an axis array. 484 @("sideX/sideY work on axis arrays") 485 unittest { 486 487 float[2] sides; 488 489 sides.sideX = 1; 490 sides.sideY = 2; 491 492 assert(sides == [1, 2]); 493 494 assert(sides.sideX == 1); 495 assert(sides.sideY == 2); 496 497 } 498 499 /// Returns a side array created from either: another side array like it, a two item array with each representing an 500 /// axis like `[x, y]`, or a single item array or the element type to fill all values with it. 501 T[4] normalizeSideArray(T, size_t n)(T[n] values) { 502 503 // Already a valid side array 504 static if (n == 4) return values; 505 506 // Axis array 507 else static if (n == 2) return [values[0], values[0], values[1], values[1]]; 508 509 // Single item array 510 else static if (n == 1) return [values[0], values[0], values[0], values[0]]; 511 512 else static assert(false, format!"Unsupported static array size %s, expected 1, 2 or 4 elements."(n)); 513 514 515 } 516 517 /// ditto 518 T[4] normalizeSideArray(T)(T value) { 519 520 return [value, value, value, value]; 521 522 } 523 524 /// Shift the side clockwise (if positive) or counter-clockwise (if negative). 525 Style.Side shiftSide(Style.Side side, int shift) { 526 527 // Convert the side to an "angle" — 0 is the top, 1 is right and so on... 528 const angle = side.predSwitch( 529 Style.Side.top, 0, 530 Style.Side.right, 1, 531 Style.Side.bottom, 2, 532 Style.Side.left, 3, 533 ); 534 535 // Perform the shift 536 const shifted = (angle + shift) % 4; 537 538 // And convert it back 539 return shifted.predSwitch( 540 0, Style.Side.top, 541 1, Style.Side.right, 542 2, Style.Side.bottom, 543 3, Style.Side.left, 544 ); 545 546 } 547 548 unittest { 549 550 assert(shiftSide(Style.Side.left, 0) == Style.Side.left); 551 assert(shiftSide(Style.Side.left, 1) == Style.Side.top); 552 assert(shiftSide(Style.Side.left, 2) == Style.Side.right); 553 assert(shiftSide(Style.Side.left, 4) == Style.Side.left); 554 555 assert(shiftSide(Style.Side.top, 1) == Style.Side.right); 556 557 } 558 559 /// Make a style point the other way around 560 Style.Side reverse(Style.Side side) { 561 562 with (Style.Side) 563 return side.predSwitch( 564 left, right, 565 right, left, 566 top, bottom, 567 bottom, top, 568 ); 569 570 } 571 572 /// Get position of a rectangle's side, on the X axis if `left` or `right`, or on the Y axis if `top` or `bottom`. 573 float getSide(Rectangle rectangle, Style.Side side) { 574 575 with (Style.Side) 576 return side.predSwitch( 577 left, rectangle.x, 578 right, rectangle.x + rectangle.width, 579 top, rectangle.y, 580 bottom, rectangle.y + rectangle.height, 581 582 ); 583 584 } 585 586 unittest { 587 588 const rect = Rectangle(0, 5, 10, 15); 589 590 assert(rect.x == rect.getSide(Style.Side.left)); 591 assert(rect.y == rect.getSide(Style.Side.top)); 592 assert(rect.end.x == rect.getSide(Style.Side.right)); 593 assert(rect.end.y == rect.getSide(Style.Side.bottom)); 594 595 } 596 597 @("Legacy: Style.tint stacks (migrated)") 598 unittest { 599 600 import fluid.frame; 601 import fluid.structs; 602 603 auto io = new HeadlessBackend; 604 auto myTheme = nullTheme.derive( 605 rule!Frame( 606 Rule.backgroundColor = color!"fff", 607 Rule.tint = color!"aaaa", 608 ), 609 ); 610 auto root = vframe( 611 layout!(1, "fill"), 612 myTheme, 613 vframe( 614 layout!(1, "fill"), 615 vframe( 616 layout!(1, "fill"), 617 vframe( 618 layout!(1, "fill"), 619 ) 620 ), 621 ), 622 ); 623 624 root.io = io; 625 root.draw(); 626 627 auto rect = Rectangle(0, 0, 800, 600); 628 auto bg = color!"fff"; 629 630 // Background rectangles — all covering the same area, but with fading color and transparency 631 io.assertRectangle(rect, bg = multiply(bg, color!"aaaa")); 632 io.assertRectangle(rect, bg = multiply(bg, color!"aaaa")); 633 io.assertRectangle(rect, bg = multiply(bg, color!"aaaa")); 634 io.assertRectangle(rect, bg = multiply(bg, color!"aaaa")); 635 636 } 637 638 @("Legacy: Border occupies and takes space (abandoned)") 639 unittest { 640 641 import fluid.frame; 642 import fluid.structs; 643 644 auto io = new HeadlessBackend; 645 auto myTheme = nullTheme.derive( 646 rule!Frame( 647 Rule.backgroundColor = color!"fff", 648 Rule.tint = color!"aaaa", 649 Rule.border.sideRight = 1, 650 Rule.borderStyle = colorBorder(color!"f00"), 651 ) 652 ); 653 auto root = vframe( 654 layout!(1, "fill"), 655 myTheme, 656 vframe( 657 layout!(1, "fill"), 658 vframe( 659 layout!(1, "fill"), 660 vframe( 661 layout!(1, "fill"), 662 ) 663 ), 664 ), 665 ); 666 667 root.io = io; 668 root.draw(); 669 670 auto bg = color!"fff"; 671 672 // Background rectangles — reducing in size every pixel as the border gets added 673 io.assertRectangle(Rectangle(0, 0, 800, 600), bg = multiply(bg, color!"aaaa")); 674 io.assertRectangle(Rectangle(0, 0, 799, 600), bg = multiply(bg, color!"aaaa")); 675 io.assertRectangle(Rectangle(0, 0, 798, 600), bg = multiply(bg, color!"aaaa")); 676 io.assertRectangle(Rectangle(0, 0, 797, 600), bg = multiply(bg, color!"aaaa")); 677 678 auto border = color!"f00"; 679 680 // Border rectangles 681 io.assertRectangle(Rectangle(799, 0, 1, 600), border = multiply(border, color!"aaaa")); 682 io.assertRectangle(Rectangle(798, 0, 1, 600), border = multiply(border, color!"aaaa")); 683 io.assertRectangle(Rectangle(797, 0, 1, 600), border = multiply(border, color!"aaaa")); 684 io.assertRectangle(Rectangle(796, 0, 1, 600), border = multiply(border, color!"aaaa")); 685 686 } 687 688 /// Check if a rectangle is located above (`isAbove`), below (`isBelow`), to the left (`isToLeft`) or to the right 689 /// (`isToRight`) of another rectangle. 690 /// 691 /// The four functions wrap `isBeyond` which accepts a `Side` argument to specify direction at runtime. 692 /// 693 /// Params: 694 /// subject = Rectangle subject to the query. "Is *this* rectangle above the other?" 695 /// reference = Rectangle used as reference. 696 /// side = If using `isBeyond`, the direction the subject is expected to be in relation to the other. 697 bool isAbove(Rectangle subject, Rectangle reference) { 698 return isBeyond(subject, reference, Style.Side.top); 699 } 700 701 /// ditto 702 bool isBelow(Rectangle subject, Rectangle reference) { 703 return isBeyond(subject, reference, Style.Side.bottom); 704 } 705 706 /// ditto 707 bool isToLeft(Rectangle subject, Rectangle reference) { 708 return isBeyond(subject, reference, Style.Side.left); 709 } 710 711 /// ditto 712 bool isToRight(Rectangle subject, Rectangle reference) { 713 return isBeyond(subject, reference, Style.Side.right); 714 } 715 716 /// ditto 717 bool isBeyond(Rectangle subject, Rectangle reference, Style.Side side) { 718 719 // Distance between box sides facing each other. 720 // To illustrate, we're checking if the subject is to the right of the reference box: 721 // (side = right, side.reverse = left) 722 // 723 // ↓ reference ↓ subject 724 // +------+ +======+ 725 // | | | | 726 // | | ~~~~~~ | | 727 // | | | | 728 // +------+ +======+ 729 // side ↑ ↑ side.reverse 730 const distanceExternal = reference.getSide(side) - subject.getSide(side.reverse); 731 732 // Distance between corresponding box sides. 733 // 734 // ↓ reference ↓ subject 735 // +------+ +======+ 736 // | | : | 737 // | | ~~~~~~~~~~~~~ | 738 // | | : | 739 // +------+ +======+ 740 // side ↑ side ↑ 741 const distanceInternal = reference.getSide(side) - subject.getSide(side); 742 743 // The condition for the return value to be true, is for distanceInternal to be greater than distanceExternal. 744 // This is not the case in the opposite situation. 745 // 746 // For example, if we're checking if the subject is on the *right* of reference: 747 // 748 // trueish scenario: falseish scenario: 749 // Subject is to the right of reference Subject is the left of reference 750 // 751 // ↓ reference ↓ subject ↓ subject ↓ reference 752 // +------+ +======+ +======+ +------+ 753 // | | ~~~~~~ : | external | ~~~~~~~~~~~~~~~~~~~~ | external 754 // | | : | < | : : | > 755 // | | ~~~~~~~~~~~~~ | internal | : ~~~~~~~~~~~~~ | internal 756 // +------+ +======+ +======+ +------+ 757 // side ↑ ↑ side.reverse side ↑ side ↑ 758 const condition = abs(distanceInternal) > abs(distanceExternal); 759 760 // ↓ subject There is an edgecase though. If one box entirely overlaps the other on one axis, 761 // +====================+ it will be simultaneously to the left, and to the right, creating an ambiguity. 762 // | ↓ reference | 763 // | +------------+ | This is unwated in scenarios like focus switching. A scrollbar placed to the right 764 // | | | | of the page, should be focused by the right key, not by up or down. 765 // +===| |===+ 766 // | | For this reason, we require both `distanceInternal` and `distanceExternal` to have 767 // +------------+ the same sign, as it normally would, but not in case of an overlap. 768 return condition 769 && distanceInternal * distanceExternal >= 0; 770 771 } 772 773 /// Comparing two rectangles laid out in a column. 774 unittest { 775 776 const rect1 = Rectangle(0, 0, 10, 10); 777 const rect2 = Rectangle(0, 20, 10, 10); 778 779 assert(rect1.isAbove(rect2)); 780 assert(rect2.isBelow(rect1)); 781 782 assert(!rect1.isBelow(rect2)); 783 assert(!rect2.isAbove(rect1)); 784 assert(!rect1.isToLeft(rect2)); 785 assert(!rect2.isToLeft(rect1)); 786 assert(!rect1.isToRight(rect2)); 787 assert(!rect2.isToRight(rect1)); 788 789 } 790 791 /// Comparing two rectangles laid out in a row. 792 unittest { 793 794 const rect1 = Rectangle( 0, 0, 10, 10); 795 const rect2 = Rectangle(20, 0, 10, 10); 796 797 assert(rect1.isToLeft(rect2)); 798 assert(rect2.isToRight(rect1)); 799 800 assert(!rect1.isToRight(rect2)); 801 assert(!rect2.isToLeft(rect1)); 802 assert(!rect1.isAbove(rect2)); 803 assert(!rect2.isAbove(rect1)); 804 assert(!rect1.isBelow(rect2)); 805 assert(!rect2.isBelow(rect1)); 806 807 }