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 public import fluid.theme : makeTheme, Theme, Selector, rule, Rule, when, WhenRule, children, ChildrenRule, Field, 16 Breadcrumbs; 17 public import fluid.border; 18 public import fluid.default_theme; 19 public import fluid.backend : color; 20 21 22 @safe: 23 24 25 /// Contains the style for a node. 26 struct Style { 27 28 enum Themable; 29 30 enum Side { 31 32 left, right, top, bottom, 33 34 } 35 36 // Text options 37 @Themable { 38 39 /// Main typeface to be used for text. 40 /// 41 /// Changing the typeface requires a resize. 42 Typeface typeface; 43 44 alias font = typeface; 45 46 /// Size of the font in use, in pixels. 47 /// 48 /// Changing the size requires a resize. 49 float fontSize = 14.pt; 50 51 /// Text color. 52 Color textColor; 53 54 } 55 56 // Background & content 57 @Themable { 58 59 /// Color of lines belonging to the node, especially important to separators and sliders. 60 Color lineColor; 61 62 /// Background color of the node. 63 Color backgroundColor; 64 65 /// Background color for selected text. 66 Color selectionBackgroundColor; 67 68 } 69 70 // Spacing 71 @Themable { 72 73 /// Margin (outer margin) of the node. `[left, right, top, bottom]`. 74 /// 75 /// Updating margins requires a resize. 76 /// 77 /// See: `isSideArray`. 78 float[4] margin = 0; 79 80 /// Border size, placed between margin and padding. `[left, right, top, bottom]`. 81 /// 82 /// Updating border requires a resize. 83 /// 84 /// See: `isSideArray` 85 float[4] border = 0; 86 87 /// Padding (inner margin) of the node. `[left, right, top, bottom]`. 88 /// 89 /// Updating padding requires a resize. 90 /// 91 /// See: `isSideArray` 92 float[4] padding = 0; 93 94 /// Margin/gap between two neighboring elements; for container nodes that support it. 95 /// 96 /// Updating the gap requires a resize. 97 float[2] gap = 0; 98 99 /// Border style to use. 100 /// 101 /// Updating border requires a resize. 102 FluidBorder borderStyle; 103 104 } 105 106 // Misc 107 public { 108 109 /// Apply tint to all node contents, including children. 110 @Themable 111 Color tint = Color(0xff, 0xff, 0xff, 0xff); 112 113 /// Cursor icon to use while this node is hovered. 114 /// 115 /// Custom image cursors are not supported yet. 116 @Themable 117 FluidMouseCursor mouseCursor; 118 119 /// Additional information for the node the style applies to. 120 /// 121 /// Ignored if mismatched. 122 @Themable 123 Node.Extra extra; 124 125 /// Get or set node opacity. Value in range [0, 1] — 0 is fully transparent, 1 is fully opaque. 126 float opacity() const { 127 128 return tint.a / 255.0f; 129 130 } 131 132 /// ditto 133 float opacity(float value) { 134 135 tint.a = cast(ubyte) clamp(value * ubyte.max, ubyte.min, ubyte.max); 136 137 return value; 138 139 } 140 141 } 142 143 public { 144 145 /// Breadcrumbs associated with this style. Used to keep track of tree-aware theme selectors, such as 146 /// `children`. Does not include breadcrumbs loaded by parent nodes. 147 Breadcrumbs breadcrumbs; 148 149 } 150 151 private this(Typeface typeface) { 152 153 this.typeface = typeface; 154 155 } 156 157 static Typeface defaultTypeface() { 158 159 return Typeface.defaultTypeface; 160 161 } 162 163 static Typeface loadTypeface(string file) { 164 165 return new FreetypeTypeface(file); 166 167 } 168 169 alias loadFont = loadTypeface; 170 171 bool opCast(T : bool)() const { 172 173 return this !is Style(null); 174 175 } 176 177 bool opEquals(const Style other) const @trusted { 178 179 // @safe: FluidBorder and Typeface are required to provide @safe opEquals. 180 // D doesn't check for opEquals on interfaces, though. 181 return this.tupleof == other.tupleof; 182 183 } 184 185 /// Set current DPI. 186 void setDPI(Vector2 dpi) { 187 188 getTypeface.setSize(dpi, fontSize); 189 190 } 191 192 /// Get current typeface, or fallback to default. 193 Typeface getTypeface() { 194 195 return either(typeface, Typeface.defaultTypeface); 196 197 } 198 199 const(Typeface) getTypeface() const { 200 201 return either(typeface, Typeface.defaultTypeface); 202 203 } 204 205 /// Draw the background & border. 206 void drawBackground(FluidBackend backend, Rectangle rect) const { 207 208 backend.drawRectangle(rect, backgroundColor); 209 210 // Add border if active 211 if (borderStyle) { 212 213 borderStyle.apply(backend, rect, border); 214 215 } 216 217 } 218 219 /// Draw a line. 220 void drawLine(FluidBackend backend, Vector2 start, Vector2 end) const { 221 222 backend.drawLine(start, end, lineColor); 223 224 } 225 226 /// Get a side array holding both the regular margin and the border. 227 float[4] fullMargin() const { 228 229 return [ 230 margin.sideLeft + border.sideLeft, 231 margin.sideRight + border.sideRight, 232 margin.sideTop + border.sideTop, 233 margin.sideBottom + border.sideBottom, 234 ]; 235 236 } 237 238 /// Remove padding from the vector representing size of a box. 239 Vector2 contentBox(Vector2 size) const { 240 241 return cropBox(size, padding); 242 243 } 244 245 /// Remove padding from the given rect. 246 Rectangle contentBox(Rectangle rect) const { 247 248 return cropBox(rect, padding); 249 250 } 251 252 /// Get a sum of margin, border size and padding. 253 float[4] totalMargin() const { 254 255 float[4] ret = margin[] + border[] + padding[]; 256 return ret; 257 258 } 259 260 /// Crop the given box by reducing its size on all sides. 261 static Vector2 cropBox(Vector2 size, float[4] sides) { 262 263 size.x = max(0, size.x - sides.sideLeft - sides.sideRight); 264 size.y = max(0, size.y - sides.sideTop - sides.sideBottom); 265 266 return size; 267 268 } 269 270 /// ditto 271 static Rectangle cropBox(Rectangle rect, float[4] sides) { 272 273 rect.x += sides.sideLeft; 274 rect.y += sides.sideTop; 275 276 const size = cropBox(Vector2(rect.w, rect.h), sides); 277 rect.width = size.x; 278 rect.height = size.y; 279 280 return rect; 281 282 } 283 284 } 285 286 /// Side array is a static array defining a property separately for each side of a box, for example margin and border 287 /// size. Order is as follows: `[left, right, top, bottom]`. You can use `Style.Side` to index this array with an enum. 288 /// 289 /// Because of the default behavior of static arrays, one can set the value for all sides to be equal with a simple 290 /// assignment: `array = 8`. Additionally, to make it easier to manipulate the box, one may use the `sideX` and `sideY` 291 /// functions to get a `float[2]` array of the values corresponding to the given axis (which can also be assigned like 292 /// `array.sideX = 8`) or the `sideLeft`, `sideRight`, `sideTop` and `sideBottom` functions corresponding to the given 293 /// sides. 294 enum isSideArray(T) = is(T == X[4], X) && T.length == 4; 295 296 /// ditto 297 enum isSomeSideArray(T) = isSideArray!T 298 || (is(T == Field!(name, U), string name, U) && isSideArray!U); 299 300 /// 301 unittest { 302 303 float[4] sides; 304 static assert(isSideArray!(float[4])); 305 306 sides.sideX = 4; 307 308 assert(sides.sideLeft == sides.sideRight); 309 assert(sides.sideLeft == 4); 310 311 sides = 8; 312 assert(sides == [8, 8, 8, 8]); 313 assert(sides.sideX == sides.sideY); 314 315 } 316 317 /// An axis array is similar to a size array, but does not distinguish between invididual directions on a single axis. 318 /// Thus, it contains only two values, one for the X axis, and one for the Y axis. 319 /// 320 /// `sideX` and `sideY` can be used to access individual items of an axis array by name. 321 enum isAxisArray(T) = is(T == X[2], X) && T.length == 2; 322 323 static assert(!isSideArray!(float[2])); 324 static assert( isSideArray!(float[4])); 325 326 static assert( isAxisArray!(float[2])); 327 static assert(!isAxisArray!(float[4])); 328 329 /// Get a reference to the left, right, top or bottom side of the given side array. 330 auto ref sideLeft(T)(return auto ref inout T sides) 331 if (isSomeSideArray!T) { 332 333 return sides[Style.Side.left]; 334 335 } 336 337 /// ditto 338 auto ref sideRight(T)(return auto ref inout T sides) 339 if (isSomeSideArray!T) { 340 341 return sides[Style.Side.right]; 342 343 } 344 345 /// ditto 346 auto ref sideTop(T)(return auto ref inout T sides) 347 if (isSomeSideArray!T) { 348 349 return sides[Style.Side.top]; 350 351 } 352 353 /// ditto 354 auto ref sideBottom(T)(return auto ref inout T sides) 355 if (isSomeSideArray!T) { 356 357 return sides[Style.Side.bottom]; 358 359 } 360 361 /// 362 unittest { 363 364 float[4] sides = [8, 0, 4, 2]; 365 366 assert(sides.sideRight == 0); 367 368 sides.sideRight = 8; 369 sides.sideBottom = 4; 370 371 assert(sides == [8, 8, 4, 4]); 372 373 } 374 375 /// Get a reference to the X axis for the given side or axis array. 376 ref inout(ElementType!T[2]) sideX(T)(return ref inout T sides) 377 if (isSideArray!T) { 378 379 const start = Style.Side.left; 380 return sides[start .. start + 2]; 381 382 } 383 384 /// ditto 385 auto ref sideX(T)(return auto ref inout T sides) 386 if (isSomeSideArray!T && !isSideArray!T) { 387 388 const start = Style.Side.left; 389 return sides[start .. start + 2]; 390 391 } 392 393 /// ditto 394 ref inout(ElementType!T) sideX(T)(return ref inout T sides) 395 if (isAxisArray!T) { 396 397 return sides[0]; 398 399 } 400 401 /// Get a reference to the Y axis for the given side or axis array. 402 ref inout(ElementType!T[2]) sideY(T)(return ref inout T sides) 403 if (isSideArray!T) { 404 405 const start = Style.Side.top; 406 return sides[start .. start + 2]; 407 408 } 409 410 /// ditto 411 auto ref sideY(T)(return auto ref inout T sides) 412 if (isSomeSideArray!T && !isSideArray!T) { 413 414 const start = Style.Side.top; 415 return sides[start .. start + 2]; 416 417 } 418 419 /// ditto 420 ref inout(ElementType!T) sideY(T)(return ref inout T sides) 421 if (isAxisArray!T) { 422 423 return sides[1]; 424 425 } 426 427 /// Assigning values to an axis of a side array. 428 unittest { 429 430 float[4] sides = [1, 2, 3, 4]; 431 432 assert(sides.sideX == [sides.sideLeft, sides.sideRight]); 433 assert(sides.sideY == [sides.sideTop, sides.sideBottom]); 434 435 sides.sideX = 8; 436 437 assert(sides == [8, 8, 3, 4]); 438 439 sides.sideY = sides.sideBottom; 440 441 assert(sides == [8, 8, 4, 4]); 442 443 } 444 445 /// Operating on an axis array. 446 @("sideX/sideY work on axis arrays") 447 unittest { 448 449 float[2] sides; 450 451 sides.sideX = 1; 452 sides.sideY = 2; 453 454 assert(sides == [1, 2]); 455 456 assert(sides.sideX == 1); 457 assert(sides.sideY == 2); 458 459 } 460 461 /// Returns a side array created from either: another side array like it, a two item array with each representing an 462 /// axis like `[x, y]`, or a single item array or the element type to fill all values with it. 463 T[4] normalizeSideArray(T, size_t n)(T[n] values) { 464 465 // Already a valid side array 466 static if (n == 4) return values; 467 468 // Axis array 469 else static if (n == 2) return [values[0], values[0], values[1], values[1]]; 470 471 // Single item array 472 else static if (n == 1) return [values[0], values[0], values[0], values[0]]; 473 474 else static assert(false, format!"Unsupported static array size %s, expected 1, 2 or 4 elements."(n)); 475 476 477 } 478 479 /// ditto 480 T[4] normalizeSideArray(T)(T value) { 481 482 return [value, value, value, value]; 483 484 } 485 486 /// Shift the side clockwise (if positive) or counter-clockwise (if negative). 487 Style.Side shiftSide(Style.Side side, int shift) { 488 489 // Convert the side to an "angle" — 0 is the top, 1 is right and so on... 490 const angle = side.predSwitch( 491 Style.Side.top, 0, 492 Style.Side.right, 1, 493 Style.Side.bottom, 2, 494 Style.Side.left, 3, 495 ); 496 497 // Perform the shift 498 const shifted = (angle + shift) % 4; 499 500 // And convert it back 501 return shifted.predSwitch( 502 0, Style.Side.top, 503 1, Style.Side.right, 504 2, Style.Side.bottom, 505 3, Style.Side.left, 506 ); 507 508 } 509 510 unittest { 511 512 assert(shiftSide(Style.Side.left, 0) == Style.Side.left); 513 assert(shiftSide(Style.Side.left, 1) == Style.Side.top); 514 assert(shiftSide(Style.Side.left, 2) == Style.Side.right); 515 assert(shiftSide(Style.Side.left, 4) == Style.Side.left); 516 517 assert(shiftSide(Style.Side.top, 1) == Style.Side.right); 518 519 } 520 521 /// Make a style point the other way around 522 Style.Side reverse(Style.Side side) { 523 524 with (Style.Side) 525 return side.predSwitch( 526 left, right, 527 right, left, 528 top, bottom, 529 bottom, top, 530 ); 531 532 } 533 534 /// Get position of a rectangle's side, on the X axis if `left` or `right`, or on the Y axis if `top` or `bottom`. 535 float getSide(Rectangle rectangle, Style.Side side) { 536 537 with (Style.Side) 538 return side.predSwitch( 539 left, rectangle.x, 540 right, rectangle.x + rectangle.width, 541 top, rectangle.y, 542 bottom, rectangle.y + rectangle.height, 543 544 ); 545 546 } 547 548 unittest { 549 550 const rect = Rectangle(0, 5, 10, 15); 551 552 assert(rect.x == rect.getSide(Style.Side.left)); 553 assert(rect.y == rect.getSide(Style.Side.top)); 554 assert(rect.end.x == rect.getSide(Style.Side.right)); 555 assert(rect.end.y == rect.getSide(Style.Side.bottom)); 556 557 } 558 559 unittest { 560 561 import fluid.frame; 562 import fluid.structs; 563 564 auto io = new HeadlessBackend; 565 auto myTheme = nullTheme.derive( 566 rule!Frame( 567 Rule.backgroundColor = color!"fff", 568 Rule.tint = color!"aaaa", 569 ), 570 ); 571 auto root = vframe( 572 layout!(1, "fill"), 573 myTheme, 574 vframe( 575 layout!(1, "fill"), 576 vframe( 577 layout!(1, "fill"), 578 vframe( 579 layout!(1, "fill"), 580 ) 581 ), 582 ), 583 ); 584 585 root.io = io; 586 root.draw(); 587 588 auto rect = Rectangle(0, 0, 800, 600); 589 auto bg = color!"fff"; 590 591 // Background rectangles — all covering the same area, but with fading color and transparency 592 io.assertRectangle(rect, bg = multiply(bg, color!"aaaa")); 593 io.assertRectangle(rect, bg = multiply(bg, color!"aaaa")); 594 io.assertRectangle(rect, bg = multiply(bg, color!"aaaa")); 595 io.assertRectangle(rect, bg = multiply(bg, color!"aaaa")); 596 597 } 598 599 unittest { 600 601 import fluid.frame; 602 import fluid.structs; 603 604 auto io = new HeadlessBackend; 605 auto myTheme = nullTheme.derive( 606 rule!Frame( 607 Rule.backgroundColor = color!"fff", 608 Rule.tint = color!"aaaa", 609 Rule.border.sideRight = 1, 610 Rule.borderStyle = colorBorder(color!"f00"), 611 ) 612 ); 613 auto root = vframe( 614 layout!(1, "fill"), 615 myTheme, 616 vframe( 617 layout!(1, "fill"), 618 vframe( 619 layout!(1, "fill"), 620 vframe( 621 layout!(1, "fill"), 622 ) 623 ), 624 ), 625 ); 626 627 root.io = io; 628 root.draw(); 629 630 auto bg = color!"fff"; 631 632 // Background rectangles — reducing in size every pixel as the border gets added 633 io.assertRectangle(Rectangle(0, 0, 800, 600), bg = multiply(bg, color!"aaaa")); 634 io.assertRectangle(Rectangle(0, 0, 799, 600), bg = multiply(bg, color!"aaaa")); 635 io.assertRectangle(Rectangle(0, 0, 798, 600), bg = multiply(bg, color!"aaaa")); 636 io.assertRectangle(Rectangle(0, 0, 797, 600), bg = multiply(bg, color!"aaaa")); 637 638 auto border = color!"f00"; 639 640 // Border rectangles 641 io.assertRectangle(Rectangle(799, 0, 1, 600), border = multiply(border, color!"aaaa")); 642 io.assertRectangle(Rectangle(798, 0, 1, 600), border = multiply(border, color!"aaaa")); 643 io.assertRectangle(Rectangle(797, 0, 1, 600), border = multiply(border, color!"aaaa")); 644 io.assertRectangle(Rectangle(796, 0, 1, 600), border = multiply(border, color!"aaaa")); 645 646 }