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.utils; 11 import fluid.backend; 12 import fluid.typeface; 13 14 public import fluid.border; 15 public import fluid.style_macros; 16 public import fluid.default_theme; 17 public import fluid.backend : color; 18 19 20 @safe: 21 22 23 alias StyleKeyPtr = immutable(StyleKey)*; 24 25 /// Node theme. 26 alias Theme = Style[StyleKeyPtr]; 27 28 /// Side array is a static array defining a property separately for each side of a box, for example margin and border 29 /// size. Order is as follows: `[left, right, top, bottom]`. You can use `Style.Side` to index this array with an enum. 30 /// 31 /// Because of the default behavior of static arrays, one can set the value for all sides to be equal with a simple 32 /// assignment: `array = 8`. Additionally, to make it easier to manipulate the box, one may use the `sideX` and `sideY` 33 /// functions to get a `uint[2]` array of the values corresponding to the given axis (which can also be assigned like 34 /// `array.sideX = 8`) or the `sideLeft`, `sideRight`, `sideTop` and `sideBottom` functions corresponding to the given 35 /// sides. 36 enum isSideArray(T) = is(T == X[4], X); 37 38 /// 39 unittest { 40 41 uint[4] sides; 42 static assert(isSideArray!(uint[4])); 43 44 sides.sideX = 4; 45 46 assert(sides.sideLeft == sides.sideRight); 47 assert(sides.sideLeft == 4); 48 49 sides = 8; 50 assert(sides == [8, 8, 8, 8]); 51 assert(sides.sideX == sides.sideY); 52 53 } 54 55 /// An empty struct used to create unique style type identifiers. 56 struct StyleKey { } 57 58 /// Create a new style initialized with given D code. 59 /// 60 /// raylib and std.string are accessible inside by default. 61 /// 62 /// Note: It is recommended to create a root style node defining font parameters and then inherit other styles from it. 63 /// 64 /// Params: 65 /// init = D code to use. 66 /// parents = Styles to inherit from. See `Style.this` documentation for more info. 67 /// data = Data to pass to the code as the context. All fields of the struct will be within the style's scope. 68 Style style(string init, Data)(Data data, Style[] parents...) { 69 70 auto result = new Style; 71 72 with (data) with (result) mixin(init); 73 74 return result; 75 76 } 77 78 /// Ditto. 79 Style style(string init)(Style[] parents...) { 80 81 auto result = new Style(parents); 82 result.update!init; 83 84 return result; 85 86 } 87 88 /// Contains the style for a node. 89 class Style { 90 91 enum Side { 92 93 left, right, top, bottom, 94 95 } 96 97 // Internal use only, can't be private because it's used in mixins. 98 static { 99 100 Theme _currentTheme; 101 Style[] _styleStack; 102 103 } 104 105 // Text options 106 struct { 107 108 /// Main typeface to be used for text. 109 Typeface typeface; 110 111 alias font = typeface; 112 113 deprecated("Set font parameters in the typeface. These will be removed in 0.7.0") { 114 115 /// Font size (height) in pixels. 116 float fontSize; 117 118 /// Line height, as a fraction of `fontSize`. 119 float lineHeight; 120 121 /// Space between characters, relative to font size. 122 float charSpacing; 123 124 /// Space between words, relative to the font size. 125 float wordSpacing; 126 127 } 128 129 /// Text color. 130 Color textColor; 131 132 } 133 134 // Background 135 struct { 136 137 /// Background color of the node. 138 Color backgroundColor; 139 140 } 141 142 // Spacing 143 struct { 144 145 /// Margin (outer margin) of the node. `[left, right, top, bottom]`. 146 /// 147 /// See: `isSideArray`. 148 uint[4] margin; 149 150 /// Border size, placed between margin and padding. `[left, right, top, bottom]`. 151 /// 152 /// See: `isSideArray` 153 uint[4] border; 154 155 /// Padding (inner margin) of the node. `[left, right, top, bottom]`. 156 /// 157 /// See: `isSideArray` 158 uint[4] padding; 159 160 /// Border style to use. 161 FluidBorder borderStyle; 162 163 } 164 165 // Misc 166 struct { 167 168 /// Cursor icon to use while this node is hovered. 169 /// 170 /// Custom image cursors are not supported yet. 171 FluidMouseCursor mouseCursor; 172 173 } 174 175 this() { 176 177 this.font = Typeface.defaultTypeface; 178 179 } 180 181 /// Create a style by copying params of others. 182 /// 183 /// Multiple styles can be set, so if one field is set to `typeof(field).init`, it will be taken from the previous 184 /// style from the list — that is, settings from the last style override previous ones. 185 this(Style[] styles...) { 186 187 // Check each style 188 foreach (i, style; styles) { 189 190 // Inherit each field 191 static foreach (j; 0..this.tupleof.length) {{ 192 193 auto inheritedField = style.tupleof[j]; 194 195 static if (__traits(compiles, inheritedField is null)) { 196 197 const isInit = inheritedField is null; 198 199 } 200 else { 201 202 const isInit = inheritedField == inheritedField.init; 203 204 } 205 206 // Ignore if it's set to init (unless it's the first style) 207 if (i == 0 || !isInit) { 208 209 this.tupleof[j] = inheritedField; 210 211 } 212 213 }} 214 215 } 216 217 } 218 219 /// Get the default, empty style. 220 static Style init() { 221 222 static Style val; 223 if (val is null) val = new Style; 224 return val; 225 226 } 227 228 static Typeface loadTypeface(string file, int fontSize) @trusted { 229 230 return new FreetypeTypeface(file, fontSize); 231 232 } 233 234 static Typeface loadTypeface(int fontSize) @trusted { 235 236 return new FreetypeTypeface(fontSize); 237 238 } 239 240 alias loadFont = loadTypeface; 241 242 /// Update the style with given D code. 243 /// 244 /// This allows each init code to have a consistent default scope, featuring `fluid`, `raylib` and chosen `std` 245 /// modules. 246 /// 247 /// Params: 248 /// init = Code to update the style with. 249 /// T = An compile-time object to update the scope with. 250 void update(string init)() { 251 252 import fluid; 253 254 // Wrap init content in brackets to allow imports 255 // See: https://forum.dlang.org/thread/nl4vse$egk$1@digitalmars.com 256 // The thread mentions mixin templates but it's the same for string mixins too; and a mixin with multiple 257 // statements is annoyingly treated as multiple separate mixins. 258 mixin(init.format!"{ %s }"); 259 260 } 261 262 /// Ditto. 263 void update(string init, T)() { 264 265 import fluid; 266 267 with (T) mixin(init.format!"{ %s }"); 268 269 } 270 271 /// Set current DPI. 272 void setDPI(Vector2 dpi) { 273 274 // Update the typeface 275 if (typeface) { 276 277 typeface.dpi = dpi; 278 279 } 280 281 } 282 283 deprecated("Use Typeface or Text instead. To be removed in 0.7.0.") { 284 285 /// Measure space given text will use. 286 /// 287 /// Params: 288 /// availableSpace = Space available for drawing. 289 /// text = Text to draw. 290 /// wrap = If true (default), the text will be wrapped to match available space, unless the space is 291 /// empty. 292 /// Returns: 293 /// If `availableSpace` is a vector, returns the result as a vector. 294 /// 295 /// If `availableSpace` is a rectangle, returns a rectangle of the size of the result, offset to the position 296 /// of the given rectangle. 297 Vector2 measureText(Vector2 availableSpace, string text, bool wrap = true) const 298 in (availableSpace.x.isFinite && availableSpace.y.isFinite, 299 format!"Text space given must be finite: %s"(availableSpace)) 300 out (r; r.x.isFinite && r.y.isFinite, 301 format!"Resulting text space must be finite: %s"(r)) 302 do { 303 304 return typeface.measure(availableSpace, text, wrap); 305 306 } 307 308 /// Ditto 309 Rectangle measureText(Rectangle availableSpace, string text, bool wrap = true) const 310 do { 311 312 const vec = measureText( 313 Vector2(availableSpace.width, availableSpace.height), 314 text, wrap 315 ); 316 317 return Rectangle( 318 availableSpace.x, availableSpace.y, 319 vec.x, vec.y 320 ); 321 322 } 323 324 /// Draw text using the same params as `measureText`. 325 void drawText(ref Image image, Rectangle rect, string text, bool wrap = true) const { 326 327 typeface.draw(image, rect, text, textColor, wrap); 328 329 } 330 331 /// ditto 332 void drawText(ref Image image, Rectangle rect, string text, Color color, bool wrap = true) const { 333 334 typeface.draw(image, rect, text, color, wrap); 335 336 } 337 338 } 339 340 /// Draw the background 341 void drawBackground(FluidBackend backend, Rectangle rect) const @trusted { 342 343 backend.drawRectangle(rect, backgroundColor); 344 345 } 346 347 /// Get a side array holding both the regular margin and the border. 348 uint[4] fullMargin() const { 349 350 return [ 351 margin.sideLeft + border.sideLeft, 352 margin.sideRight + border.sideRight, 353 margin.sideTop + border.sideTop, 354 margin.sideBottom + border.sideBottom, 355 ]; 356 357 } 358 359 /// Remove padding from the vector representing size of a box. 360 Vector2 contentBox(Vector2 size) const { 361 362 return cropBox(size, padding); 363 364 } 365 366 /// Remove padding from the given rect. 367 Rectangle contentBox(Rectangle rect) const { 368 369 return cropBox(rect, padding); 370 371 } 372 373 /// Get a sum of margin, border size and padding. 374 uint[4] totalMargin() const { 375 376 uint[4] ret = margin[] + border[] + padding[]; 377 return ret; 378 379 } 380 381 /// Crop the given box by reducing its size on all sides. 382 static Vector2 cropBox(Vector2 size, uint[4] sides) { 383 384 size.x = max(0, size.x - sides.sideLeft - sides.sideRight); 385 size.y = max(0, size.y - sides.sideTop - sides.sideBottom); 386 387 return size; 388 389 } 390 391 /// ditto 392 static Rectangle cropBox(Rectangle rect, uint[4] sides) { 393 394 rect.x += sides.sideLeft; 395 rect.y += sides.sideTop; 396 397 const size = cropBox(Vector2(rect.w, rect.h), sides); 398 rect.width = size.x; 399 rect.height = size.y; 400 401 return rect; 402 403 } 404 405 } 406 407 /// `wrapText` result. 408 struct TextLine { 409 410 struct Word { 411 412 string text; 413 size_t width; 414 bool lineFeed; // Word is followed by a line feed. 415 416 } 417 418 /// Index of the line within the original text. This is the start of the text. 419 size_t index; 420 421 /// Text on this line. 422 string text; 423 424 /// Width of the line (including spaces). 425 size_t width = 0; 426 427 /// If true, the line is explicitly terminated with a line feed. 428 bool lineFeed; 429 430 } 431 432 /// Get a reference to the left, right, top or bottom side of the given side array. 433 ref inout(ElementType!T) sideLeft(T)(return ref inout T sides) 434 if (isSideArray!T) { 435 436 return sides[Style.Side.left]; 437 438 } 439 440 /// ditto 441 ref inout(ElementType!T) sideRight(T)(return ref inout T sides) 442 if (isSideArray!T) { 443 444 return sides[Style.Side.right]; 445 446 } 447 448 /// ditto 449 ref inout(ElementType!T) sideTop(T)(return ref inout T sides) 450 if (isSideArray!T) { 451 452 return sides[Style.Side.top]; 453 454 } 455 456 /// ditto 457 ref inout(ElementType!T) sideBottom(T)(return ref inout T sides) 458 if (isSideArray!T) { 459 460 return sides[Style.Side.bottom]; 461 462 } 463 464 /// 465 unittest { 466 467 uint[4] sides = [8, 0, 4, 2]; 468 469 assert(sides.sideRight == 0); 470 471 sides.sideRight = 8; 472 sides.sideBottom = 4; 473 474 assert(sides == [8, 8, 4, 4]); 475 476 } 477 478 /// Get a reference to the X axis for the given side array. 479 ref inout(uint[2]) sideX(T)(return ref inout T sides) 480 if (isSideArray!T) { 481 482 const start = Style.Side.left; 483 return sides[start .. start + 2]; 484 485 } 486 487 ref inout(uint[2]) sideY(T)(return ref inout T sides) 488 if (isSideArray!T) { 489 490 const start = Style.Side.top; 491 return sides[start .. start + 2]; 492 493 } 494 495 /// 496 unittest { 497 498 uint[4] sides = [1, 2, 3, 4]; 499 500 assert(sides.sideX == [sides.sideLeft, sides.sideRight]); 501 assert(sides.sideY == [sides.sideTop, sides.sideBottom]); 502 503 sides.sideX = 8; 504 505 assert(sides == [8, 8, 3, 4]); 506 507 sides.sideY = sides.sideBottom; 508 509 assert(sides == [8, 8, 4, 4]); 510 511 } 512 513 /// Returns a side array created from either: another side array like it, a two item array with each representing an 514 /// axis like `[x, y]`, or a single item array or the element type to fill all values with it. 515 T[4] normalizeSideArray(T, size_t n)(T[n] values) { 516 517 // Already a valid side array 518 static if (n == 4) return values; 519 520 // Axis array 521 else static if (n == 2) return [values[0], values[0], values[1], values[1]]; 522 523 // Single item array 524 else static if (n == 1) return [values[0], values[0], values[0], values[0]]; 525 526 else static assert(false, format!"Unsupported static array size %s, expected 1, 2 or 4 elements."(n)); 527 528 529 } 530 531 /// ditto 532 T[4] normalizeSideArray(T)(T value) { 533 534 return [value, value, value, value]; 535 536 } 537 538 /// Shift the side clockwise (if positive) or counter-clockwise (if negative). 539 Style.Side shiftSide(Style.Side side, int shift) { 540 541 // Convert the side to an "angle" — 0 is the top, 1 is right and so on... 542 const angle = side.predSwitch( 543 Style.Side.top, 0, 544 Style.Side.right, 1, 545 Style.Side.bottom, 2, 546 Style.Side.left, 3, 547 ); 548 549 // Perform the shift 550 const shifted = (angle + shift) % 4; 551 552 // And convert it back 553 return shifted.predSwitch( 554 0, Style.Side.top, 555 1, Style.Side.right, 556 2, Style.Side.bottom, 557 3, Style.Side.left, 558 ); 559 560 } 561 562 unittest { 563 564 assert(shiftSide(Style.Side.left, 0) == Style.Side.left); 565 assert(shiftSide(Style.Side.left, 1) == Style.Side.top); 566 assert(shiftSide(Style.Side.left, 2) == Style.Side.right); 567 assert(shiftSide(Style.Side.left, 4) == Style.Side.left); 568 569 assert(shiftSide(Style.Side.top, 1) == Style.Side.right); 570 571 } 572 573 /// Make a style point the other way around 574 Style.Side reverse(Style.Side side) { 575 576 with (Style.Side) 577 return side.predSwitch( 578 left, right, 579 right, left, 580 top, bottom, 581 bottom, top, 582 ); 583 584 } 585 586 /// Get position of a rectangle's side, on the X axis if `left` or `right`, or on the Y axis if `top` or `bottom`. 587 float getSide(Rectangle rectangle, Style.Side side) { 588 589 with (Style.Side) 590 return side.predSwitch( 591 left, rectangle.x, 592 right, rectangle.x + rectangle.width, 593 top, rectangle.y, 594 bottom, rectangle.y + rectangle.height, 595 596 ); 597 598 } 599 600 unittest { 601 602 const rect = Rectangle(0, 5, 10, 15); 603 604 assert(rect.x == rect.getSide(Style.Side.left)); 605 assert(rect.y == rect.getSide(Style.Side.top)); 606 assert(rect.end.x == rect.getSide(Style.Side.right)); 607 assert(rect.end.y == rect.getSide(Style.Side.bottom)); 608 609 }