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