1 ///2 modulefluid.structs;
3 4 importstd.conv;
5 importstd.traits;
6 7 importfluid.node;
8 importfluid.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 initialize18 /// 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 check28 // NodeType = Node to implement.29 enumisNodeParam(T, NodeType = Node)
30 = __traits(compiles, T.init.apply(NodeType.init));
31 32 33 enumNodeAlign {
34 35 start, center, end, fill,
36 37 centre = center38 39 }
40 41 /// Create a new layout42 /// 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 Layoutlayout(uintexpand, NodeAlignalignX, NodeAlignalignY) pure {
48 49 returnLayout(expand, [alignX, alignY]);
50 51 }
52 53 /// Ditto54 Layoutlayout(uintexpand, NodeAlignalign_) pure {
55 56 returnLayout(expand, align_);
57 58 }
59 60 /// Ditto61 Layoutlayout(NodeAlignalignX, NodeAlignalignY) pure {
62 63 returnLayout(0, [alignX, alignY]);
64 65 }
66 67 /// Ditto68 Layoutlayout(NodeAlignalign_) pure {
69 70 returnLayout(0, align_);
71 72 }
73 74 /// Ditto75 Layoutlayout(uintexpand) pure {
76 77 returnLayout(expand);
78 79 }
80 81 /// CTFE version of the layout constructor, allows using strings instead of enum members, to avoid boilerplate.82 Layoutlayout(uintexpand, stringalignX, stringalignY)() pure {
83 84 enumvalueX = alignX.to!NodeAlign;
85 enumvalueY = alignY.to!NodeAlign;
86 87 returnLayout(expand, [valueX, valueY]);
88 89 }
90 91 /// Ditto92 Layoutlayout(uintexpand, stringalign_)() pure {
93 94 enumvalueXY = align_.to!NodeAlign;
95 96 returnLayout(expand, valueXY);
97 98 }
99 100 /// Ditto101 Layoutlayout(stringalignX, stringalignY)() pure {
102 103 enumvalueX = alignX.to!NodeAlign;
104 enumvalueY = alignY.to!NodeAlign;
105 106 returnLayout(0, [valueX, valueY]);
107 108 }
109 110 /// Ditto111 Layoutlayout(stringalign_)() pure {
112 113 enumvalueXY = align_.to!NodeAlign;
114 115 returnLayout(0, valueXY);
116 117 }
118 119 /// Ditto120 Layoutlayout(uintexpand)() pure {
121 122 returnLayout(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 structLayout {
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 uintexpand;
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 voidapply(Nodenode) {
152 153 node.layout = this;
154 155 }
156 157 stringtoString() const {
158 159 importstd.format;
160 161 constequalAlign = nodeAlign[0] == nodeAlign[1];
162 conststartAlign = equalAlign && nodeAlign[0] == NodeAlign.start;
163 164 if (expand) {
165 166 if (startAlign) returnformat!".layout!%s"(expand);
167 elseif (equalAlign) returnformat!".layout!(%s, %s)"(expand, nodeAlign[0]);
168 elsereturnformat!".layout!(%s, %s, %s)"(expand, nodeAlign[0], nodeAlign[1]);
169 170 }
171 172 else {
173 174 if (startAlign) returnformat!"Layout()";
175 elseif (equalAlign) returnformat!".layout!%s"(nodeAlign[0]);
176 elsereturnformat!".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 considers185 /// `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 Vector2alignRectangle(NodeAlign[2] alignment, Rectanglespace, Vector2size) {
196 197 floatimpl(NodeAlignalign_, lazyfloatspaceLeft) {
198 with (NodeAlign)
199 finalswitch (align_) {
200 casestart, fill: return0;
201 casecenter: returnspaceLeft / 2;
202 caseend: returnspaceLeft;
203 }
204 }
205 206 returnVector2(
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 resemble214 /// [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 enumNodeTag;
219 220 ///221 unittest {
222 223 importfluid.label;
224 225 @NodeTag226 enumTags {
227 myTag,
228 }
229 230 staticassert(isNodeTag!(Tags.myTag));
231 232 automyLabel = 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 templateisNodeTag(aliastag) {
243 244 // @NodeTag enum Tag;245 // enum Tag { @NodeTag tag }246 enumisDirectTag247 = isSomeEnum!tag248 && hasUDA!(tag, NodeTag);
249 250 // @NodeTag enum Enum { tag }251 staticif (isType!tag)
252 enumisTagMember = false;
253 else254 enumisTagMember255 = is(typeof(tag)== enum)
256 && hasUDA!(typeof(tag), NodeTag);
257 258 enumisNodeTag = isDirectTag || isTagMember;
259 260 }
261 262 /// Test if the given symbol is an enum, or an enum member.263 enumisSomeEnum(aliastag)
264 = is(tag == enum)
265 || is(__traits(parent, tag) == enum);
266 267 /// Specify tags for the next node to add.268 TagListtags(input...)() {
269 270 returnTagList.init.add!input;
271 272 }
273 274 /// Node parameter assigning a new set of tags to a node.275 structTagList {
276 277 importstd.range;
278 importstd.algorithm;
279 280 /// A *sorted* array of tags.281 privateSortedRange!(TagID[]) range;
282 283 /// Check if the range is empty.284 boolempty() {
285 286 returnrange.empty;
287 288 }
289 290 /// Count all tags.291 size_tlength() {
292 293 returnrange.length;
294 295 }
296 297 /// Get a list of all tags in the list.298 const(TagID)[] get() {
299 300 returnrange.release;
301 302 }
303 304 /// Create a new set of tags expanded by the given set of tags.305 TagListadd(input...)() {
306 307 constoriginalLength = this.range.length;
308 309 TagID[input.length] newTags;
310 311 // Load the tags312 staticforeach (i, tag; input) {
313 314 newTags[i] = tagID!tag;
315 316 }
317 318 // Allocate output range319 autoresult = newTagID[originalLength + input.length];
320 autolhs = result[0..originalLength] = this.range.release;
321 322 // Sort the result323 completeSort(assumeSorted(lhs), newTags[]);
324 325 // Add the remaining tags326 result[originalLength..$] = newTags;
327 328 returnTagList(assumeSorted(result));
329 330 }
331 332 /// Remove given tags from the list.333 TagListremove(input...)() {
334 335 TagID[input.length] targetTags;
336 337 // Load the tags338 staticforeach (i, tag; input) {
339 340 targetTags[i] = tagID!tag;
341 342 }
343 344 // Sort them345 sort(targetTags[]);
346 347 returnTagList(
348 setDifference(this.range, targetTags[])
349 .array350 .assumeSorted351 );
352 353 }
354 355 unittest {
356 357 @NodeTag358 enumFoo { a, b, c, d }
359 360 automyTags = 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 autointersect(TagListtags) {
373 374 returnsetIntersection(this.range, tags.range);
375 376 }
377 378 /// Assign this list of tags to the given node.379 voidapply(Nodenode) {
380 381 node.tags = this;
382 383 }
384 385 stringtoString() {
386 387 // Prevent writeln from clearing the range388 returntext(range.release);
389 390 }
391 392 }
393 394 unittest {
395 396 @NodeTag397 enumsingleEnum;
398 399 assert(isNodeTag!singleEnum);
400 401 @NodeTag402 enumTags { a, b, c }
403 404 assert(isNodeTag!(Tags.a));
405 assert(isNodeTag!(Tags.b));
406 assert(isNodeTag!(Tags.c));
407 408 enumNonTags { a, b, c }
409 410 assert(!isNodeTag!(NonTags.a));
411 assert(!isNodeTag!(NonTags.b));
412 assert(!isNodeTag!(NonTags.c));
413 414 enumSomeTags { a, b, @NodeTagtag }
415 416 assert(!isNodeTag!(SomeTags.a));
417 assert(!isNodeTag!(SomeTags.b));
418 assert(isNodeTag!(SomeTags.tag));
419 420 }
421 422 unittest {
423 424 importstd.range;
425 importstd.algorithm;
426 427 @NodeTag428 enumMyTags {
429 tag1, tag2430 }
431 432 autotags1 = tags!(MyTags.tag1, MyTags.tag2);
433 autotags2 = 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 autotags3 = tags!(MyTags.tag1);
440 autotags4 = 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 TagIDtagID(aliastag)()
449 out (r; r.id, "Invalid ID returned for tag " ~ tag.stringof)
450 do {
451 452 enumTag = TagIDImpl!tag();
453 454 debug455 returnTagID(cast(long) &Tag._id, fullyQualifiedName!tag);
456 else457 returnTagID(cast(long) &Tag._id);
458 459 }
460 461 /// Unique ID of a node tag.462 structTagID {
463 464 /// Unique ID of the tag.465 longid;
466 467 invariant(id, "Tag ID must not be 0.");
468 469 /// Tag name. Only emitted when debugging.470 debugstringname;
471 472 boolopEqual(TagIDother) {
473 474 returnid == other.id;
475 476 }
477 478 longopCmp(TagIDother) const {
479 480 returnid - other.id;
481 482 }
483 484 }
485 486 privatestructTagIDImpl(aliasnodeTag)
487 if (isNodeTag!nodeTag) {
488 489 aliastag = 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 privatestaticimmutablebool_id;
494 495 }
496 497 @("Members of anonymous enums cannot be NodeTags.")
498 unittest {
499 500 classA {
501 @NodeTagenum { foo }
502 }
503 classB : A {
504 @NodeTagenum { 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 autoignoreMouse(boolvalue = true) {
517 518 staticstructIgnoreMouse {
519 520 boolvalue;
521 522 voidapply(Nodenode) {
523 524 node.ignoreMouse = value;
525 526 }
527 528 }
529 530 returnIgnoreMouse(value);
531 532 }
533 534 ///535 unittest {
536 537 importfluid.label;
538 importfluid.button;
539 540 // Prevents the label from blocking the button541 vframeButton(
542 label(.ignoreMouse, "Click me!"),
543 delegate { }
544 );
545 546 }
547 548 @("ignoreMouse property sets Node.ignoreMouse to true")
549 unittest {
550 551 importfluid.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 autohidden(boolvalue = true) {
566 567 staticstructHidden {
568 569 boolvalue;
570 571 voidapply(Nodenode) {
572 573 node.isHidden = value;
574 575 }
576 577 }
578 579 returnHidden(value);
580 581 }
582 583 ///584 unittest {
585 586 importfluid.label;
587 588 automyLabel = 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 autodisabled(boolvalue = true) {
599 600 staticstructDisabled {
601 602 boolvalue;
603 604 voidapply(Nodenode) {
605 606 node.isDisabled = value;
607 608 }
609 610 }
611 612 returnDisabled(value);
613 614 }
615 616 unittest {
617 618 importfluid.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 in633 /// question is, or is not, in the node's bounds. This defines the way nodes interact with634 /// 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 should637 /// only specify its own bounds, so neither a `yes` or `no` answer prevent children nodes638 /// from overriding the answer.639 ///640 /// Despite the above, it is sometimes desirable to keep children from occupying space, for641 /// example to hijack and control mouse input. To specify that children nodes *cannot* be in642 /// bounds, use `InBounds.notInBranch` (to indicate none of the nodes include the point) or643 /// `InBounds.onlySelf` (the node captures all events, including of its children).644 ///645 /// See_Also:646 /// `Node.inBounds`.647 enumIsOpaque : 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; neither659 /// 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 being666 /// [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 structIsOpaqueMask {
673 intbitmask;
674 675 /// Returns:676 /// True if the queried point can be found in the node itself.677 boolinSelf() 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 boolinChildren() const {
686 return (bitmask & 2) == 0;
687 }
688 689 /// Create a value that combines the restrictions of both masks. It can be said that either690 /// 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 IsOpaquefilter(IsOpaqueMaskother) const {
701 returncast(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 mask705 /// can be passed to a node builder.706 /// Params:707 /// node = Node to change.708 voidapply(Nodenode) {
709 node.isOpaque = cast(IsOpaque) IsOpaqueMask(bitmask & 3);
710 }
711 }
712 713 staticassert(IsOpaque.init == IsOpaque.yes);
714 staticassert(!IsOpaque.no.inSelf);
715 staticassert( IsOpaque.no.inChildren);
716 staticassert( IsOpaque.yes.inSelf);
717 staticassert( IsOpaque.yes.inChildren);
718 staticassert(!IsOpaque.notInBranch.inSelf);
719 staticassert(!IsOpaque.notInBranch.inChildren);
720 staticassert( IsOpaque.onlySelf.inSelf);
721 staticassert(!IsOpaque.onlySelf.inChildren);
722 staticassert(IsOpaque.yes.filter(IsOpaque.no) == IsOpaque.no);
723 staticassert(IsOpaque.no.filter(IsOpaque.onlySelf) == IsOpaque.notInBranch);