1 /// 2 module fluid.structs; 3 4 import std.conv; 5 import std.traits; 6 7 import fluid.node; 8 import fluid.types; 9 10 11 @safe: 12 13 14 /// Check if the given type implements node parameter interface. 15 /// 16 /// Node parameters passed at the beginning of a simpleConstructor will not be passed to the node constructor. Instead, 17 /// their `apply` function will be called on the node after the node has been created. This can be used to initialize 18 /// properties at the time of creation. A basic implementation of the interface looks as follows: 19 /// 20 /// --- 21 /// struct MyParameter { 22 /// void apply(Node node) { } 23 /// } 24 /// --- 25 /// 26 /// Params: 27 /// T = Type to check 28 // NodeType = Node to implement. 29 enum isNodeParam(T, NodeType = Node) 30 = __traits(compiles, T.init.apply(NodeType.init)); 31 32 33 enum NodeAlign { 34 35 start, center, end, fill, 36 37 centre = center 38 39 } 40 41 /// Create a new layout 42 /// Params: 43 /// expand = Numerator of the fraction of space this node should occupy in the parent. 44 /// align_ = Align of the node (horizontal and vertical). 45 /// alignX = Horizontal align of the node. 46 /// alignY = Vertical align of the node. 47 Layout layout(uint expand, NodeAlign alignX, NodeAlign alignY) pure { 48 49 return Layout(expand, [alignX, alignY]); 50 51 } 52 53 /// Ditto 54 Layout layout(uint expand, NodeAlign align_) pure { 55 56 return Layout(expand, align_); 57 58 } 59 60 /// Ditto 61 Layout layout(NodeAlign alignX, NodeAlign alignY) pure { 62 63 return Layout(0, [alignX, alignY]); 64 65 } 66 67 /// Ditto 68 Layout layout(NodeAlign align_) pure { 69 70 return Layout(0, align_); 71 72 } 73 74 /// Ditto 75 Layout layout(uint expand) pure { 76 77 return Layout(expand); 78 79 } 80 81 /// CTFE version of the layout constructor, allows using strings instead of enum members, to avoid boilerplate. 82 Layout layout(uint expand, string alignX, string alignY)() pure { 83 84 enum valueX = alignX.to!NodeAlign; 85 enum valueY = alignY.to!NodeAlign; 86 87 return Layout(expand, [valueX, valueY]); 88 89 } 90 91 /// Ditto 92 Layout layout(uint expand, string align_)() pure { 93 94 enum valueXY = align_.to!NodeAlign; 95 96 return Layout(expand, valueXY); 97 98 } 99 100 /// Ditto 101 Layout layout(string alignX, string alignY)() pure { 102 103 enum valueX = alignX.to!NodeAlign; 104 enum valueY = alignY.to!NodeAlign; 105 106 return Layout(0, [valueX, valueY]); 107 108 } 109 110 /// Ditto 111 Layout layout(string align_)() pure { 112 113 enum valueXY = align_.to!NodeAlign; 114 115 return Layout(0, valueXY); 116 117 } 118 119 /// Ditto 120 Layout layout(uint expand)() pure { 121 122 return Layout(expand); 123 124 } 125 126 unittest { 127 128 assert(layout!1 == layout(1)); 129 assert(layout!("fill") == layout(NodeAlign.fill, NodeAlign.fill)); 130 assert(layout!("fill", "fill") == layout(NodeAlign.fill)); 131 132 assert(!__traits(compiles, layout!"expand")); 133 assert(!__traits(compiles, layout!("expand", "noexpand"))); 134 assert(!__traits(compiles, layout!(1, "whatever"))); 135 assert(!__traits(compiles, layout!(2, "foo", "bar"))); 136 137 } 138 139 /// Node parameter for setting the node layout. 140 struct Layout { 141 142 /// Fraction of available space this node should occupy in the node direction. 143 /// 144 /// If set to `0`, the node doesn't have a strict size limit and has size based on content. 145 uint expand; 146 147 /// Align the content box to a side of the occupied space. 148 NodeAlign[2] nodeAlign; 149 150 /// Apply this layout to the given node. Implements the node parameter. 151 void apply(Node node) { 152 153 node.layout = this; 154 155 } 156 157 string toString() const { 158 159 import std.format; 160 161 const equalAlign = nodeAlign[0] == nodeAlign[1]; 162 const startAlign = equalAlign && nodeAlign[0] == NodeAlign.start; 163 164 if (expand) { 165 166 if (startAlign) return format!".layout!%s"(expand); 167 else if (equalAlign) return format!".layout!(%s, %s)"(expand, nodeAlign[0]); 168 else return format!".layout!(%s, %s, %s)"(expand, nodeAlign[0], nodeAlign[1]); 169 170 } 171 172 else { 173 174 if (startAlign) return format!"Layout()"; 175 else if (equalAlign) return format!".layout!%s"(nodeAlign[0]); 176 else return format!".layout!(%s, %s)"(nodeAlign[0], nodeAlign[1]); 177 178 } 179 180 } 181 182 } 183 184 /// Place a rectangle inside another based on its specified alignment. This function considers 185 /// `NodeAlign.fill` to be equivalent to `NodeAlign.start`. 186 /// 187 /// This function may commonly be used with `Layout.nodeAlign`. 188 /// 189 /// Params: 190 /// alignment = Alignment to use for the child rectangle. 191 /// space = Rectangle denoting available space (parent). 192 /// size = Size of the rectangle to place (child). 193 /// Returns: 194 /// position = Position assigned to the child rectangle. 195 Vector2 alignRectangle(NodeAlign[2] alignment, Rectangle space, Vector2 size) { 196 197 float impl(NodeAlign align_, lazy float spaceLeft) { 198 with (NodeAlign) 199 final switch (align_) { 200 case start, fill: return 0; 201 case center: return spaceLeft / 2; 202 case end: return spaceLeft; 203 } 204 } 205 206 return Vector2( 207 space.x + impl(alignment[0], space.width - size.x), 208 space.y + impl(alignment[1], space.height - size.y), 209 ); 210 211 } 212 213 /// Tags are optional "marks" left on nodes that are used to apply matching styles. Tags closely resemble 214 /// [HTML classes](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/class). 215 /// 216 /// Tags have to be explicitly defined before usage by creating an enum and marking it with the `@NodeTag` attribute. 217 /// Such tags can then be applied by passing them to the constructor. 218 enum NodeTag; 219 220 /// 221 unittest { 222 223 import fluid.label; 224 225 @NodeTag 226 enum Tags { 227 myTag, 228 } 229 230 static assert(isNodeTag!(Tags.myTag)); 231 232 auto myLabel = label( 233 .tags!(Tags.myTag), 234 "Hello, World!" 235 ); 236 237 assert(myLabel.tags == .tags!(Tags.myTag)); 238 239 } 240 241 /// Check if the given item is a node tag. 242 template isNodeTag(alias tag) { 243 244 // @NodeTag enum Tag; 245 // enum Tag { @NodeTag tag } 246 enum isDirectTag 247 = isSomeEnum!tag 248 && hasUDA!(tag, NodeTag); 249 250 // @NodeTag enum Enum { tag } 251 static if (isType!tag) 252 enum isTagMember = false; 253 else 254 enum isTagMember 255 = is(typeof(tag)== enum) 256 && hasUDA!(typeof(tag), NodeTag); 257 258 enum isNodeTag = isDirectTag || isTagMember; 259 260 } 261 262 /// Test if the given symbol is an enum, or an enum member. 263 enum isSomeEnum(alias tag) 264 = is(tag == enum) 265 || is(__traits(parent, tag) == enum); 266 267 /// Specify tags for the next node to add. 268 TagList tags(input...)() { 269 270 return TagList.init.add!input; 271 272 } 273 274 /// Node parameter assigning a new set of tags to a node. 275 struct TagList { 276 277 import std.range; 278 import std.algorithm; 279 280 /// A *sorted* array of tags. 281 private SortedRange!(TagID[]) range; 282 283 /// Check if the range is empty. 284 bool empty() { 285 286 return range.empty; 287 288 } 289 290 /// Count all tags. 291 size_t length() { 292 293 return range.length; 294 295 } 296 297 /// Get a list of all tags in the list. 298 const(TagID)[] get() { 299 300 return range.release; 301 302 } 303 304 /// Create a new set of tags expanded by the given set of tags. 305 TagList add(input...)() { 306 307 const originalLength = this.range.length; 308 309 TagID[input.length] newTags; 310 311 // Load the tags 312 static foreach (i, tag; input) { 313 314 newTags[i] = tagID!tag; 315 316 } 317 318 // Allocate output range 319 auto result = new TagID[originalLength + input.length]; 320 auto lhs = result[0..originalLength] = this.range.release; 321 322 // Sort the result 323 completeSort(assumeSorted(lhs), newTags[]); 324 325 // Add the remaining tags 326 result[originalLength..$] = newTags; 327 328 return TagList(assumeSorted(result)); 329 330 } 331 332 /// Remove given tags from the list. 333 TagList remove(input...)() { 334 335 TagID[input.length] targetTags; 336 337 // Load the tags 338 static foreach (i, tag; input) { 339 340 targetTags[i] = tagID!tag; 341 342 } 343 344 // Sort them 345 sort(targetTags[]); 346 347 return TagList( 348 setDifference(this.range, targetTags[]) 349 .array 350 .assumeSorted 351 ); 352 353 } 354 355 unittest { 356 357 @NodeTag 358 enum Foo { a, b, c, d } 359 360 auto myTags = tags!(Foo.a, Foo.b, Foo.c); 361 362 assert(myTags.remove!(Foo.b, Foo.a) == tags!(Foo.c)); 363 assert(myTags.remove!(Foo.d) == myTags); 364 assert(myTags.remove!() == myTags); 365 assert(myTags.remove!(Foo.a, Foo.b, Foo.c) == tags!()); 366 assert(myTags.remove!(Foo.a, Foo.b, Foo.c, Foo.d) == tags!()); 367 368 } 369 370 /// Get the intesection of the two tag lists. 371 /// Returns: A range with tags that are present in both of the lists. 372 auto intersect(TagList tags) { 373 374 return setIntersection(this.range, tags.range); 375 376 } 377 378 /// Assign this list of tags to the given node. 379 void apply(Node node) { 380 381 node.tags = this; 382 383 } 384 385 string toString() { 386 387 // Prevent writeln from clearing the range 388 return text(range.release); 389 390 } 391 392 } 393 394 unittest { 395 396 @NodeTag 397 enum singleEnum; 398 399 assert(isNodeTag!singleEnum); 400 401 @NodeTag 402 enum Tags { a, b, c } 403 404 assert(isNodeTag!(Tags.a)); 405 assert(isNodeTag!(Tags.b)); 406 assert(isNodeTag!(Tags.c)); 407 408 enum NonTags { a, b, c } 409 410 assert(!isNodeTag!(NonTags.a)); 411 assert(!isNodeTag!(NonTags.b)); 412 assert(!isNodeTag!(NonTags.c)); 413 414 enum SomeTags { a, b, @NodeTag tag } 415 416 assert(!isNodeTag!(SomeTags.a)); 417 assert(!isNodeTag!(SomeTags.b)); 418 assert(isNodeTag!(SomeTags.tag)); 419 420 } 421 422 unittest { 423 424 import std.range; 425 import std.algorithm; 426 427 @NodeTag 428 enum MyTags { 429 tag1, tag2 430 } 431 432 auto tags1 = tags!(MyTags.tag1, MyTags.tag2); 433 auto tags2 = tags!(MyTags.tag2, MyTags.tag1); 434 435 assert(tags1.intersect(tags2).walkLength == 2); 436 assert(tags2.intersect(tags1).walkLength == 2); 437 assert(tags1 == tags2); 438 439 auto tags3 = tags!(MyTags.tag1); 440 auto tags4 = tags!(MyTags.tag2); 441 442 assert(tags1.intersect(tags3).equal(tagID!(MyTags.tag1).only)); 443 assert(tags1.intersect(tags4).equal(tagID!(MyTags.tag2).only)); 444 assert(tags3.intersect(tags4).empty); 445 446 } 447 448 TagID tagID(alias tag)() 449 out (r; r.id, "Invalid ID returned for tag " ~ tag.stringof) 450 do { 451 452 enum Tag = TagIDImpl!tag(); 453 454 debug 455 return TagID(cast(long) &Tag._id, fullyQualifiedName!tag); 456 else 457 return TagID(cast(long) &Tag._id); 458 459 } 460 461 /// Unique ID of a node tag. 462 struct TagID { 463 464 /// Unique ID of the tag. 465 long id; 466 467 invariant(id, "Tag ID must not be 0."); 468 469 /// Tag name. Only emitted when debugging. 470 debug string name; 471 472 bool opEqual(TagID other) { 473 474 return id == other.id; 475 476 } 477 478 long opCmp(TagID other) const { 479 480 return id - other.id; 481 482 } 483 484 } 485 486 private struct TagIDImpl(alias nodeTag) 487 if (isNodeTag!nodeTag) { 488 489 alias tag = nodeTag; 490 491 /// Implementation is the same as input action IDs, see fluid.input.InputAction. 492 /// For what's important, the _id field is not the ID; its pointer however, is. 493 private static immutable bool _id; 494 495 } 496 497 @("Members of anonymous enums cannot be NodeTags.") 498 unittest { 499 500 class A { 501 @NodeTag enum { foo } 502 } 503 class B : A { 504 @NodeTag enum { bar } 505 } 506 507 assert(!__traits(compiles, tagID!(B.foo))); 508 assert(!__traits(compiles, tagID!(B.bar))); 509 510 } 511 512 /// This node property will disable mouse input on the given node. 513 /// 514 /// Params: 515 /// value = If set to false, the effect is reversed and mouse input is instead enabled. 516 auto ignoreMouse(bool value = true) { 517 518 static struct IgnoreMouse { 519 520 bool value; 521 522 void apply(Node node) { 523 524 node.ignoreMouse = value; 525 526 } 527 528 } 529 530 return IgnoreMouse(value); 531 532 } 533 534 /// 535 unittest { 536 537 import fluid.label; 538 import fluid.button; 539 540 // Prevents the label from blocking the button 541 vframeButton( 542 label(.ignoreMouse, "Click me!"), 543 delegate { } 544 ); 545 546 } 547 548 @("ignoreMouse property sets Node.ignoreMouse to true") 549 unittest { 550 551 import fluid.space; 552 553 assert(vspace().ignoreMouse == false); 554 assert(vspace(.ignoreMouse).ignoreMouse == true); 555 assert(vspace(.ignoreMouse(false)).ignoreMouse == false); 556 assert(vspace(.ignoreMouse(true)).ignoreMouse == true); 557 558 } 559 560 /// This node property will make the subject hidden, setting the `isHidden` field to true. 561 /// 562 /// Params: 563 /// value = If set to false, the effect is reversed and the node is set to be visible instead. 564 /// See_Also: `Node.isHidden` 565 auto hidden(bool value = true) { 566 567 static struct Hidden { 568 569 bool value; 570 571 void apply(Node node) { 572 573 node.isHidden = value; 574 575 } 576 577 } 578 579 return Hidden(value); 580 581 } 582 583 /// 584 unittest { 585 586 import fluid.label; 587 588 auto myLabel = label(.hidden, "The user will never see this label"); 589 myLabel.draw(); // doesn't draw anything! 590 591 } 592 593 /// This node property will disable the subject, setting the `isHidden` field to true. 594 /// 595 /// Params: 596 /// value = If set to false, the effect is reversed and the node is set to be enabled instead. 597 /// See_Also: `Node.isDisabled` 598 auto disabled(bool value = true) { 599 600 static struct Disabled { 601 602 bool value; 603 604 void apply(Node node) { 605 606 node.isDisabled = value; 607 608 } 609 610 } 611 612 return Disabled(value); 613 614 } 615 616 unittest { 617 618 import fluid.button; 619 620 button( 621 .disabled, 622 "You cannot press this button!", 623 delegate { 624 assert(false); 625 } 626 ); 627 628 629 } 630 631 /// `InBounds` is used as a return value of `Node.inBounds`. For most use-cases, 632 /// `InBounds.yes` and `InBounds.no` are the most appropriate, specifying that the point in 633 /// question is, or is not, in the node's bounds. This defines the way nodes interact with 634 /// mouse, touchscreen or other hover events (`fluid.io.hover`). 635 /// 636 /// The node is not normally responsible for the bounds of its children. The node's should 637 /// only specify its own bounds, so neither a `yes` or `no` answer prevent children nodes 638 /// from overriding the answer. 639 /// 640 /// Despite the above, it is sometimes desirable to keep children from occupying space, for 641 /// example to hijack and control mouse input. To specify that children nodes *cannot* be in 642 /// bounds, use `InBounds.notInBranch` (to indicate none of the nodes include the point) or 643 /// `InBounds.onlySelf` (the node captures all events, including of its children). 644 /// 645 /// See_Also: 646 /// `Node.inBounds`. 647 enum IsOpaque : IsOpaqueMask { 648 649 /// The point is in bounds of this node. 650 yes = IsOpaqueMask(0), 651 652 /// The point is *not* in bounds of this node. 653 no = IsOpaqueMask(1), 654 655 /// The point is in bounds, but not in the bounds of any of the children nodes. 656 onlySelf = IsOpaqueMask(2), 657 658 /// Indicates that the point is *not* in bounds of any of the nodes in the branch; neither 659 /// of self, nor any of the children nodes. 660 notInBranch = IsOpaqueMask(3), 661 } 662 663 /// This bitmask defines whether a node contains a point in its boundaries. 664 /// 665 /// To allow this to default to `InBounds.yes` while being 666 /// [zero-initialized](https://dlang.org/spec/traits.html#isZeroInit), each bit is inverted; 667 /// i.e. `0` means *yes, in bounds* and `1` means, *no, not in bounds*. 668 /// 669 /// See_Also: 670 /// `InBounds` for all possible values of this bitmask. 671 /// `Node.inBounds` for a function returning this value. 672 struct IsOpaqueMask { 673 int bitmask; 674 675 /// Returns: 676 /// True if the queried point can be found in the node itself. 677 bool inSelf() const { 678 return (bitmask & 1) == 0; 679 } 680 681 /// Returns: 682 /// True if the queried point may or may not be found in the children of the node. 683 /// A false value indicates that the point will not be in any of the children nodes, 684 /// and that children nodes should not be tested. 685 bool inChildren() const { 686 return (bitmask & 2) == 0; 687 } 688 689 /// Create a value that combines the restrictions of both masks. It can be said that either 690 /// of the masks acts as a "filter", hence the name. 691 /// 692 /// For example, combining `IsOpaque.yes` with `IsOpaque.no` returns `IsOpaque.no`. 693 /// Combining `IsOpaque.no` with `IsOpaque.onlySelf` returns `IsOpaque.notInBranch`. 694 /// 695 /// Params: 696 /// other = Mask to combine with. 697 /// Returns: 698 /// A mask with `inSelf == false` if false for either of the masks, 699 /// and similarly `inChildren == false` if false for either of the values. 700 IsOpaque filter(IsOpaqueMask other) const { 701 return cast(IsOpaque) IsOpaqueMask((bitmask | other.bitmask) & 3); 702 } 703 704 /// Set the node's opacity filter. This can be used as a node property — an opacity mask 705 /// can be passed to a node builder. 706 /// Params: 707 /// node = Node to change. 708 void apply(Node node) { 709 node.isOpaque = cast(IsOpaque) IsOpaqueMask(bitmask & 3); 710 } 711 } 712 713 static assert(IsOpaque.init == IsOpaque.yes); 714 static assert(!IsOpaque.no.inSelf); 715 static assert( IsOpaque.no.inChildren); 716 static assert( IsOpaque.yes.inSelf); 717 static assert( IsOpaque.yes.inChildren); 718 static assert(!IsOpaque.notInBranch.inSelf); 719 static assert(!IsOpaque.notInBranch.inChildren); 720 static assert( IsOpaque.onlySelf.inSelf); 721 static assert(!IsOpaque.onlySelf.inChildren); 722 static assert(IsOpaque.yes.filter(IsOpaque.no) == IsOpaque.no); 723 static assert(IsOpaque.no.filter(IsOpaque.onlySelf) == IsOpaque.notInBranch);