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