1 /// 2 module glui.style; 3 4 import raylib; 5 6 import std.math; 7 import std.range; 8 import std.string; 9 import std.typecons; 10 import std.algorithm; 11 12 import glui.node; 13 import glui.utils; 14 15 public import glui.border; 16 public import glui.style_macros; 17 18 @safe: 19 20 /// Node theme. 21 alias StyleKeyPtr = immutable(StyleKey)*; 22 alias Theme = Style[StyleKeyPtr]; 23 24 /// Side array is a static array defining a property separately for each side of a box, for example margin and border 25 /// size. Order is as follows: `[left, right, top, bottom]`. You can use `Style.Side` to index this array with an enum. 26 /// 27 /// Because of the default behavior of static arrays, one can set the value for all sides to be equal with a simple 28 /// assignment: `array = 8`. Additionally, to make it easier to manipulate the box, one may use the `sideX` and `sideY` 29 /// functions to get a `uint[2]` array of the values corresponding to the given array (which can also be assigned like 30 /// `array.sideX = 8`) or the `sideLeft`, `sideRight`, `sideTop` and `sideBottom` functions corresponding to the given 31 /// box. 32 enum isSideArray(T) = is(T == X[4], X); 33 34 /// 35 unittest { 36 37 uint[4] sides; 38 static assert(isSideArray!(typeof(sides))); 39 40 sides.sideX = 4; 41 42 assert(sides.sideLeft == sides.sideRight); 43 assert(sides.sideLeft == 4); 44 45 sides = 8; 46 assert(sides == [8, 8, 8, 8]); 47 assert(sides.sideX == sides.sideY); 48 49 } 50 51 /// An empty struct used to create unique style type identifiers. 52 struct StyleKey { } 53 54 /// Create a new style initialized with given D code. 55 /// 56 /// raylib and std.string are accessible inside by default. 57 /// 58 /// Note: It is recommended to create a root style node defining font parameters and then inherit other styles from it. 59 /// 60 /// Params: 61 /// init = D code to use. 62 /// parents = Styles to inherit from. See `Style.this` documentation for more info. 63 /// data = Data to pass to the code as the context. All fields of the struct will be within the style's scope. 64 Style style(string init, Data)(Data data, Style[] parents...) { 65 66 auto result = new Style; 67 68 with (data) with (result) mixin(init); 69 70 return result; 71 72 } 73 74 /// Ditto. 75 Style style(string init)(Style[] parents...) { 76 77 auto result = new Style(parents); 78 result.update!init; 79 80 return result; 81 82 } 83 84 /// Create a color from hex code. 85 Color color(string hexCode)() { 86 87 return color(hexCode); 88 89 } 90 91 /// ditto 92 Color color(string hexCode) pure { 93 94 import std.format; 95 96 // Remove the # if there is any 97 const hex = hexCode.chompPrefix("#"); 98 99 Color result; 100 result.a = 0xff; 101 102 switch (hex.length) { 103 104 // 4 digit RGBA 105 case 4: 106 formattedRead!"%x"(hex[3..4], result.a); 107 result.a *= 17; 108 109 // Parse the rest like RGB 110 goto case; 111 112 // 3 digit RGB 113 case 3: 114 formattedRead!"%x"(hex[0..1], result.r); 115 formattedRead!"%x"(hex[1..2], result.g); 116 formattedRead!"%x"(hex[2..3], result.b); 117 result.r *= 17; 118 result.g *= 17; 119 result.b *= 17; 120 break; 121 122 // 8 digit RGBA 123 case 8: 124 formattedRead!"%x"(hex[6..8], result.a); 125 goto case; 126 127 // 6 digit RGB 128 case 6: 129 formattedRead!"%x"(hex[0..2], result.r); 130 formattedRead!"%x"(hex[2..4], result.g); 131 formattedRead!"%x"(hex[4..6], result.b); 132 break; 133 134 default: 135 assert(false, "Invalid hex code length"); 136 137 } 138 139 return result; 140 141 } 142 143 unittest { 144 145 import std.exception; 146 147 assert(color!"#123" == Color(0x11, 0x22, 0x33, 0xff)); 148 assert(color!"#1234" == Color(0x11, 0x22, 0x33, 0x44)); 149 assert(color!"1234" == Color(0x11, 0x22, 0x33, 0x44)); 150 assert(color!"123456" == Color(0x12, 0x34, 0x56, 0xff)); 151 assert(color!"2a5592f0" == Color(0x2a, 0x55, 0x92, 0xf0)); 152 153 assertThrown(color!"ag5"); 154 155 } 156 157 /// Contains the style for a node. 158 class Style { 159 160 enum Side { 161 162 left, right, top, bottom, 163 164 } 165 166 // Internal use only, can't be private because it's used in mixins. 167 static { 168 169 Theme _currentTheme; 170 Style[] _styleStack; 171 172 } 173 174 // Text options 175 struct { 176 177 /// Font to be used for the text. 178 Font font; 179 180 /// Font size (height) in pixels. 181 float fontSize; 182 183 /// Line height, as a fraction of `fontSize`. 184 float lineHeight; 185 186 /// Space between characters, relative to font size. 187 float charSpacing; 188 189 /// Space between words, relative to the font size. 190 float wordSpacing; 191 192 /// Text color. 193 Color textColor; 194 195 } 196 197 // Background 198 struct { 199 200 /// Background color of the node. 201 Color backgroundColor; 202 203 } 204 205 // Spacing 206 struct { 207 208 /// Margin (outer margin) of the node. `[left, right, top, bottom]`. 209 /// 210 /// See: `isSideArray`. 211 uint[4] margin; 212 213 /// Border size, placed between margin and padding. `[left, right, top, bottom]`. 214 /// 215 /// See: `isSideArray` 216 uint[4] border; 217 218 /// Padding (inner margin) of the node. `[left, right, top, bottom]`. 219 /// 220 /// See: `isSideArray` 221 uint[4] padding; 222 223 /// Border style to use. 224 GluiBorder borderStyle; 225 226 } 227 228 // Misc 229 struct { 230 231 /// Cursor icon to use while this node is hovered. 232 /// 233 /// Custom image cursors are not supported yet. 234 MouseCursor mouseCursor; 235 236 } 237 238 this() { } 239 240 /// Create a style by copying params of others. 241 /// 242 /// Multiple styles can be set, so if one field is set to `typeof(field).init`, it will be taken from the previous 243 /// style from the list — that is, settings from the last style override previous ones. 244 this(Style[] styles...) { 245 246 import std.meta, std.traits; 247 248 // Check each style 249 foreach (i, style; styles) { 250 251 // Inherit each field 252 static foreach (field; FieldNameTuple!(typeof(this))) {{ 253 254 auto inheritedField = mixin("style." ~ field); 255 256 static if (__traits(compiles, inheritedField is null)) { 257 258 const isInit = inheritedField is null; 259 260 } 261 else { 262 263 const isInit = inheritedField == inheritedField.init; 264 265 } 266 267 // Ignore if it's set to init (unless it's the first style) 268 if (i == 0 || !isInit) { 269 270 mixin("this." ~ field) = inheritedField; 271 272 } 273 274 }} 275 276 } 277 278 } 279 280 /// Get the default, empty style. 281 static Style init() { 282 283 static Style val; 284 if (val is null) val = new Style; 285 return val; 286 287 } 288 289 static Font loadFont(string file, int fontSize, dchar[] fontChars = null) @trusted { 290 291 const realSize = fontSize * hidpiScale.y; 292 293 return LoadFontEx(file.toStringz, cast(int) realSize.ceil, cast(int*) fontChars.ptr, 294 cast(int) fontChars.length); 295 296 } 297 298 /// Update the style with given D code. 299 /// 300 /// This allows each init code to have a consistent default scope, featuring `glui`, `raylib` and chosen `std` 301 /// modules. 302 /// 303 /// Params: 304 /// init = Code to update the style with. 305 /// T = An compile-time object to update the scope with. 306 void update(string init)() { 307 308 import glui; 309 310 // Wrap init content in brackets to allow imports 311 // See: https://forum.dlang.org/thread/nl4vse$egk$1@digitalmars.com 312 // The thread mentions mixin templates but it's the same for string mixins too; and a mixin with multiple 313 // statements is annoyingly treated as multiple separate mixins. 314 mixin(init.format!"{ %s }"); 315 316 } 317 318 /// Ditto. 319 void update(string init, T)() { 320 321 import glui; 322 323 with (T) mixin(init.format!"{ %s }"); 324 325 } 326 327 /// Get the current font 328 inout(Font) getFont() inout @trusted { 329 330 return cast(inout) (font.recs ? font : GetFontDefault); 331 332 } 333 334 /// Measure space given text will use. 335 /// 336 /// Params: 337 /// availableSpace = Space available for drawing. 338 /// text = Text to draw. 339 /// wrap = If true (default), the text will be wrapped to match available space, unless the space is 340 /// empty. 341 /// Returns: 342 /// If `availableSpace` is a vector, returns the result as a vector. 343 /// 344 /// If `availableSpace` is a rectangle, returns a rectangle of the size of the result, offset to the position 345 /// of the given rectangle. 346 Vector2 measureText(Vector2 availableSpace, string text, bool wrap = true) const 347 in (availableSpace.x.isFinite && availableSpace.y.isFinite, 348 format!"Text space given must be finite: %s"(availableSpace)) 349 in (fontSize.isFinite, 350 format!"Invalid font size %s"(fontSize)) 351 in (lineHeight.isFinite, 352 format!"Invalid line height %s"(lineHeight)) 353 out (r; r.x.isFinite && r.y.isFinite, 354 format!"Resulting text space must be finite: %s"(r)) 355 do { 356 357 auto wrappedLines = wrapText(availableSpace.x, text, !wrap || availableSpace.x == 0); 358 359 360 return Vector2( 361 wrappedLines.map!"a.width".maxElement, 362 wrappedLines.length * fontSize * lineHeight, 363 ); 364 365 } 366 367 /// Ditto 368 Rectangle measureText(Rectangle availableSpace, string text, bool wrap = true) const 369 do { 370 371 const vec = measureText( 372 Vector2(availableSpace.width, availableSpace.height), 373 text, wrap 374 ); 375 376 return Rectangle( 377 availableSpace.x, availableSpace.y, 378 vec.x, vec.y 379 ); 380 381 } 382 383 /// Draw text using the same params as `measureText`. 384 void drawText(Rectangle rect, string text, bool wrap = true) const { 385 386 // Text position from top, relative to given rect 387 size_t top; 388 389 const totalLineHeight = fontSize * lineHeight; 390 391 // Draw each line 392 foreach (line; wrapText(rect.width, text, !wrap)) { 393 394 scope (success) top += cast(size_t) ceil(lineHeight * fontSize); 395 396 // Stop if drawing below rect 397 if (top > rect.height) break; 398 399 // Text position from left 400 size_t left; 401 402 const margin = (totalLineHeight - fontSize)/2; 403 404 foreach (word; line.words) { 405 406 const position = Vector2(rect.x + left, rect.y + top + margin); 407 408 () @trusted { 409 410 // cast(): raylib doesn't mutate the font. The parameter would probably be defined `const`, but 411 // since it's not transistive in C, and font is a struct with a pointer inside, it only matters 412 // in D. 413 414 DrawTextEx(cast() getFont, word.text.toStringz, position, fontSize, fontSize * charSpacing, 415 textColor); 416 417 }(); 418 419 left += cast(size_t) ceil(word.width + fontSize * wordSpacing); 420 421 } 422 423 } 424 425 } 426 427 /// Split the text into multiple lines in order to fit within given width. 428 /// 429 /// Params: 430 /// width = Container width the text should fit in. 431 /// text = Text to wrap. 432 /// lineFeedsOnly = If true, this should only wrap the text on line feeds. 433 TextLine[] wrapText(double width, string text, bool lineFeedsOnly) const { 434 435 const spaceSize = cast(size_t) ceil(fontSize * wordSpacing); 436 437 auto result = [TextLine()]; 438 439 /// Get width of the given word. 440 float wordWidth(string wordText) @trusted { 441 442 // See drawText for cast() 443 return MeasureTextEx(cast() getFont, wordText.toStringz, fontSize, fontSize * charSpacing).x; 444 445 } 446 447 TextLine.Word[] words; 448 449 auto whitespaceSplit = text[] 450 .splitter!((a, string b) => [' ', '\n'].canFind(a), Yes.keepSeparators)(" "); 451 452 // Pass 1: split on words, calculate minimum size 453 foreach (chunk; whitespaceSplit.chunks(2)) { 454 455 const wordText = chunk.front; 456 const size = cast(size_t) wordWidth(wordText).ceil; 457 458 chunk.popFront; 459 const feed = chunk.empty 460 ? false 461 : chunk.front == "\n"; 462 463 // Push the word 464 words ~= TextLine.Word(wordText, size, feed); 465 466 // Update minimum size 467 if (size > width) width = size; 468 469 } 470 471 // Pass 2: calculate total size 472 foreach (word; words) { 473 474 scope (success) { 475 476 // Start a new line if this words is followed by a line feed 477 if (word.lineFeed) result ~= TextLine(); 478 479 } 480 481 auto lastLine = &result[$-1]; 482 483 // If last line is empty 484 if (lastLine.words == []) { 485 486 // Append to it immediately 487 lastLine.words ~= word; 488 lastLine.width += word.width; 489 continue; 490 491 } 492 493 494 // Check if this word can fit 495 if (lineFeedsOnly || lastLine.width + spaceSize + word.width <= width) { 496 497 // Push it to this line 498 lastLine.words ~= word; 499 lastLine.width += spaceSize + word.width; 500 501 } 502 503 // It can't 504 else { 505 506 // Push it to a new line 507 result ~= TextLine([word], word.width); 508 509 } 510 511 } 512 513 return result; 514 515 } 516 517 /// Draw the background 518 void drawBackground(Rectangle rect) const @trusted { 519 520 DrawRectangleRec(rect, backgroundColor); 521 522 } 523 524 /// Get a side array holding both the regular margin and the border. 525 uint[4] fullMargin() const { 526 527 // No border 528 if (borderStyle is null) return margin; 529 530 return [ 531 margin.sideLeft + border.sideLeft, 532 margin.sideRight + border.sideRight, 533 margin.sideTop + border.sideTop, 534 margin.sideBottom + border.sideBottom, 535 ]; 536 537 } 538 539 /// Remove padding from the vector representing size of a box. 540 Vector2 contentBox(Vector2 size) const { 541 542 return cropBox(size, padding); 543 544 } 545 546 /// Remove padding from the given rect. 547 Rectangle contentBox(Rectangle rect) const { 548 549 return cropBox(rect, padding); 550 551 } 552 553 /// Get a sum of margin, border size and padding. 554 uint[4] totalMargin() const { 555 556 uint[4] ret = margin[] + border[] + padding[]; 557 return ret; 558 559 } 560 561 /// Crop the given box by reducing its size on all sides. 562 static Vector2 cropBox(Vector2 size, uint[4] sides) { 563 564 size.x = max(0, size.x - sides.sideLeft - sides.sideRight); 565 size.y = max(0, size.y - sides.sideTop - sides.sideBottom); 566 567 return size; 568 569 } 570 571 /// ditto 572 static Rectangle cropBox(Rectangle rect, uint[4] sides) { 573 574 rect.x += sides.sideLeft; 575 rect.y += sides.sideTop; 576 577 const size = cropBox(Vector2(rect.w, rect.h), sides); 578 rect.width = size.x; 579 rect.height = size.y; 580 581 return rect; 582 583 } 584 585 } 586 587 /// `wrapText` result. 588 struct TextLine { 589 590 struct Word { 591 592 string text; 593 size_t width; 594 bool lineFeed; // Word is followed by a line feed. 595 596 } 597 598 /// Words on this line. 599 Word[] words; 600 601 /// Width of the line (including spaces). 602 size_t width = 0; 603 604 } 605 606 /// Get a reference to the left, right, top or bottom side of the given side array. 607 ref inout(uint) sideLeft(T)(return ref inout T sides) 608 if (isSideArray!T) { 609 610 return sides[Style.Side.left]; 611 612 } 613 614 /// ditto 615 ref inout(uint) sideRight(T)(return ref inout T sides) 616 if (isSideArray!T) { 617 618 return sides[Style.Side.right]; 619 620 } 621 622 /// ditto 623 ref inout(uint) sideTop(T)(return ref inout T sides) 624 if (isSideArray!T) { 625 626 return sides[Style.Side.top]; 627 628 } 629 630 /// ditto 631 ref inout(uint) sideBottom(T)(return ref inout T sides) 632 if (isSideArray!T) { 633 634 return sides[Style.Side.bottom]; 635 636 } 637 638 /// 639 unittest { 640 641 uint[4] sides = [8, 0, 4, 2]; 642 643 assert(sides.sideRight == 0); 644 645 sides.sideRight = 8; 646 sides.sideBottom = 4; 647 648 assert(sides == [8, 8, 4, 4]); 649 650 } 651 652 /// Get a reference to the X axis for the given side array. 653 ref inout(uint[2]) sideX(T)(return ref inout T sides) 654 if (isSideArray!T) { 655 656 const start = Style.Side.left; 657 return sides[start .. start + 2]; 658 659 } 660 661 ref inout(uint[2]) sideY(T)(return ref inout T sides) 662 if (isSideArray!T) { 663 664 const start = Style.Side.top; 665 return sides[start .. start + 2]; 666 667 } 668 669 /// 670 unittest { 671 672 uint[4] sides = [1, 2, 3, 4]; 673 674 assert(sides.sideX == [sides.sideLeft, sides.sideRight]); 675 assert(sides.sideY == [sides.sideTop, sides.sideBottom]); 676 677 sides.sideX = 8; 678 679 assert(sides == [8, 8, 3, 4]); 680 681 sides.sideY = sides.sideBottom; 682 683 assert(sides == [8, 8, 4, 4]); 684 685 } 686 687 /// Returns a side array created from either: another side array like it, a two item array with each representing an 688 /// axis like `[x, y]`, or a single item array or the element type to fill all values with it. 689 T[4] normalizeSideArray(T, size_t n)(T[n] values) { 690 691 // Already a valid side array 692 static if (n == 4) return values; 693 694 // Axis array 695 else static if (n == 2) return [values[0], values[0], values[1], values[1]]; 696 697 // Single item array 698 else static if (n == 1) return [values[0], values[0], values[0], values[0]]; 699 700 else static assert(false, format!"Unsupported static array size %s, expected 1, 2 or 4 elements."(n)); 701 702 703 } 704 705 /// ditto 706 T[4] normalizeSideArray(T)(T value) { 707 708 return [value, value, value, value]; 709 710 } 711 712 /// Shift the side clockwise (if positive) or counter-clockwise (if negative). 713 Style.Side shiftSide(Style.Side side, int shift) { 714 715 // Conver the side to an "angle" — 0 is the top, 1 is right and so on... 716 const angle = side.predSwitch( 717 Style.Side.top, 0, 718 Style.Side.right, 1, 719 Style.Side.bottom, 2, 720 Style.Side.left, 3, 721 ); 722 723 // Perform the shift 724 const shifted = (angle + shift) % 4; 725 726 // And convert it back 727 return shifted.predSwitch( 728 0, Style.Side.top, 729 1, Style.Side.right, 730 2, Style.Side.bottom, 731 3, Style.Side.left, 732 ); 733 734 } 735 736 unittest { 737 738 assert(shiftSide(Style.Side.left, 0) == Style.Side.left); 739 assert(shiftSide(Style.Side.left, 1) == Style.Side.top); 740 assert(shiftSide(Style.Side.left, 2) == Style.Side.right); 741 assert(shiftSide(Style.Side.left, 4) == Style.Side.left); 742 743 assert(shiftSide(Style.Side.top, 1) == Style.Side.right); 744 745 }