1 /// 2 module fluid.structs; 3 4 import std.conv; 5 import std.traits; 6 7 import fluid.node; 8 9 10 @safe: 11 12 13 /// Check if the given type implements node parameter interface. 14 /// 15 /// Node parameters passed at the beginning of a simpleConstructor will not be passed to the node constructor. Instead, 16 /// their `apply` function will be called on the node after the node has been created. This can be used to initialize 17 /// properties at the time of creation. A basic implementation of the interface looks as follows: 18 /// 19 /// --- 20 /// struct MyParameter { 21 /// void apply(Node node) { } 22 /// } 23 /// --- 24 /// 25 /// Params: 26 /// T = Type to check 27 // NodeType = Node to implement. 28 enum isNodeParam(T, NodeType = Node) 29 = __traits(compiles, T.init.apply(NodeType.init)); 30 31 32 enum NodeAlign { 33 34 start, center, end, fill, 35 36 centre = center 37 38 } 39 40 /// Create a new layout 41 /// Params: 42 /// expand = Numerator of the fraction of space this node should occupy in the parent. 43 /// align_ = Align of the node (horizontal and vertical). 44 /// alignX = Horizontal align of the node. 45 /// alignY = Vertical align of the node. 46 Layout layout(uint expand, NodeAlign alignX, NodeAlign alignY) pure { 47 48 return Layout(expand, [alignX, alignY]); 49 50 } 51 52 /// Ditto 53 Layout layout(uint expand, NodeAlign align_) pure { 54 55 return Layout(expand, align_); 56 57 } 58 59 /// Ditto 60 Layout layout(NodeAlign alignX, NodeAlign alignY) pure { 61 62 return Layout(0, [alignX, alignY]); 63 64 } 65 66 /// Ditto 67 Layout layout(NodeAlign align_) pure { 68 69 return Layout(0, align_); 70 71 } 72 73 /// Ditto 74 Layout layout(uint expand) pure { 75 76 return Layout(expand); 77 78 } 79 80 /// CTFE version of the layout constructor, allows using strings instead of enum members, to avoid boilerplate. 81 Layout layout(uint expand, string alignX, string alignY)() pure { 82 83 enum valueX = alignX.to!NodeAlign; 84 enum valueY = alignY.to!NodeAlign; 85 86 return Layout(expand, [valueX, valueY]); 87 88 } 89 90 /// Ditto 91 Layout layout(uint expand, string align_)() pure { 92 93 enum valueXY = align_.to!NodeAlign; 94 95 return Layout(expand, valueXY); 96 97 } 98 99 /// Ditto 100 Layout layout(string alignX, string alignY)() pure { 101 102 enum valueX = alignX.to!NodeAlign; 103 enum valueY = alignY.to!NodeAlign; 104 105 return Layout(0, [valueX, valueY]); 106 107 } 108 109 /// Ditto 110 Layout layout(string align_)() pure { 111 112 enum valueXY = align_.to!NodeAlign; 113 114 return Layout(0, valueXY); 115 116 } 117 118 /// Ditto 119 Layout layout(uint expand)() pure { 120 121 return Layout(expand); 122 123 } 124 125 unittest { 126 127 assert(layout!1 == layout(1)); 128 assert(layout!("fill") == layout(NodeAlign.fill, NodeAlign.fill)); 129 assert(layout!("fill", "fill") == layout(NodeAlign.fill)); 130 131 assert(!__traits(compiles, layout!"expand")); 132 assert(!__traits(compiles, layout!("expand", "noexpand"))); 133 assert(!__traits(compiles, layout!(1, "whatever"))); 134 assert(!__traits(compiles, layout!(2, "foo", "bar"))); 135 136 } 137 138 /// Node parameter for setting the node layout. 139 struct Layout { 140 141 /// Fraction of available space this node should occupy in the node direction. 142 /// 143 /// If set to `0`, the node doesn't have a strict size limit and has size based on content. 144 uint expand; 145 146 /// Align the content box to a side of the occupied space. 147 NodeAlign[2] nodeAlign; 148 149 /// Apply this layout to the given node. Implements the node parameter. 150 void apply(Node node) { 151 152 node.layout = this; 153 154 } 155 156 string toString() const { 157 158 import std.format; 159 160 const equalAlign = nodeAlign[0] == nodeAlign[1]; 161 const startAlign = equalAlign && nodeAlign[0] == NodeAlign.start; 162 163 if (expand) { 164 165 if (startAlign) return format!".layout!%s"(expand); 166 else if (equalAlign) return format!".layout!(%s, %s)"(expand, nodeAlign[0]); 167 else return format!".layout!(%s, %s, %s)"(expand, nodeAlign[0], nodeAlign[1]); 168 169 } 170 171 else { 172 173 if (startAlign) return format!"Layout()"; 174 else if (equalAlign) return format!".layout!%s"(nodeAlign[0]); 175 else return format!".layout!(%s, %s)"(nodeAlign[0], nodeAlign[1]); 176 177 } 178 179 } 180 181 } 182 183 /// Tags are optional "marks" left on nodes that are used to apply matching styles. Tags closely resemble 184 /// [HTML classes](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/class). 185 /// 186 /// Tags have to be explicitly defined before usage by creating an enum and marking it with the `@NodeTag` attribute. 187 /// Such tags can then be applied by passing them to the constructor. 188 enum NodeTag; 189 190 /// 191 unittest { 192 193 import fluid.label; 194 195 @NodeTag 196 enum Tags { 197 myTag, 198 } 199 200 static assert(isNodeTag!(Tags.myTag)); 201 202 auto myLabel = label( 203 .tags!(Tags.myTag), 204 "Hello, World!" 205 ); 206 207 assert(myLabel.tags == .tags!(Tags.myTag)); 208 209 } 210 211 /// Check if the given item is a node tag. 212 template isNodeTag(alias tag) { 213 214 // @NodeTag enum Tag; 215 // enum Tag { @NodeTag tag } 216 enum isDirectTag 217 = isSomeEnum!tag 218 && hasUDA!(tag, NodeTag); 219 220 // @NodeTag enum Enum { tag } 221 static if (isType!tag) 222 enum isTagMember = false; 223 else 224 enum isTagMember 225 = is(typeof(tag)== enum) 226 && hasUDA!(typeof(tag), NodeTag); 227 228 enum isNodeTag = isDirectTag || isTagMember; 229 230 } 231 232 /// Test if the given symbol is an enum, or an enum member. 233 enum isSomeEnum(alias tag) 234 = is(tag == enum) 235 || is(__traits(parent, tag) == enum); 236 237 /// Specify tags for the next node to add. 238 TagList tags(input...)() { 239 240 return TagList.init.add!input; 241 242 } 243 244 /// Node parameter assigning a new set of tags to a node. 245 struct TagList { 246 247 import std.range; 248 import std.algorithm; 249 250 /// A *sorted* array of tags. 251 private SortedRange!(TagID[]) range; 252 253 /// Check if the range is empty. 254 bool empty() { 255 256 return range.empty; 257 258 } 259 260 /// Count all tags. 261 size_t length() { 262 263 return range.length; 264 265 } 266 267 /// Get a list of all tags in the list. 268 const(TagID)[] get() { 269 270 return range.release; 271 272 } 273 274 /// Create a new set of tags expanded by the given set of tags. 275 TagList add(input...)() { 276 277 const originalLength = this.range.length; 278 279 TagID[input.length] newTags; 280 281 // Load the tags 282 static foreach (i, tag; input) { 283 284 newTags[i] = tagID!tag; 285 286 } 287 288 // Allocate output range 289 auto result = new TagID[originalLength + input.length]; 290 auto lhs = result[0..originalLength] = this.range.release; 291 292 // Sort the result 293 completeSort(assumeSorted(lhs), newTags[]); 294 295 // Add the remaining tags 296 result[originalLength..$] = newTags; 297 298 return TagList(assumeSorted(result)); 299 300 } 301 302 /// Remove given tags from the list. 303 TagList remove(input...)() { 304 305 TagID[input.length] targetTags; 306 307 // Load the tags 308 static foreach (i, tag; input) { 309 310 targetTags[i] = tagID!tag; 311 312 } 313 314 // Sort them 315 sort(targetTags[]); 316 317 return TagList( 318 setDifference(this.range, targetTags[]) 319 .array 320 .assumeSorted 321 ); 322 323 } 324 325 unittest { 326 327 @NodeTag 328 enum Foo { a, b, c, d } 329 330 auto myTags = tags!(Foo.a, Foo.b, Foo.c); 331 332 assert(myTags.remove!(Foo.b, Foo.a) == tags!(Foo.c)); 333 assert(myTags.remove!(Foo.d) == myTags); 334 assert(myTags.remove!() == myTags); 335 assert(myTags.remove!(Foo.a, Foo.b, Foo.c) == tags!()); 336 assert(myTags.remove!(Foo.a, Foo.b, Foo.c, Foo.d) == tags!()); 337 338 } 339 340 /// Get the intesection of the two tag lists. 341 /// Returns: A range with tags that are present in both of the lists. 342 auto intersect(TagList tags) { 343 344 return setIntersection(this.range, tags.range); 345 346 } 347 348 /// Assign this list of tags to the given node. 349 void apply(Node node) { 350 351 node.tags = this; 352 353 } 354 355 string toString() { 356 357 // Prevent writeln from clearing the range 358 return text(range.release); 359 360 } 361 362 } 363 364 unittest { 365 366 @NodeTag 367 enum singleEnum; 368 369 assert(isNodeTag!singleEnum); 370 371 @NodeTag 372 enum Tags { a, b, c } 373 374 assert(isNodeTag!(Tags.a)); 375 assert(isNodeTag!(Tags.b)); 376 assert(isNodeTag!(Tags.c)); 377 378 enum NonTags { a, b, c } 379 380 assert(!isNodeTag!(NonTags.a)); 381 assert(!isNodeTag!(NonTags.b)); 382 assert(!isNodeTag!(NonTags.c)); 383 384 enum SomeTags { a, b, @NodeTag tag } 385 386 assert(!isNodeTag!(SomeTags.a)); 387 assert(!isNodeTag!(SomeTags.b)); 388 assert(isNodeTag!(SomeTags.tag)); 389 390 } 391 392 unittest { 393 394 import std.range; 395 import std.algorithm; 396 397 @NodeTag 398 enum MyTags { 399 tag1, tag2 400 } 401 402 auto tags1 = tags!(MyTags.tag1, MyTags.tag2); 403 auto tags2 = tags!(MyTags.tag2, MyTags.tag1); 404 405 assert(tags1.intersect(tags2).walkLength == 2); 406 assert(tags2.intersect(tags1).walkLength == 2); 407 assert(tags1 == tags2); 408 409 auto tags3 = tags!(MyTags.tag1); 410 auto tags4 = tags!(MyTags.tag2); 411 412 assert(tags1.intersect(tags3).equal(tagID!(MyTags.tag1).only)); 413 assert(tags1.intersect(tags4).equal(tagID!(MyTags.tag2).only)); 414 assert(tags3.intersect(tags4).empty); 415 416 } 417 418 TagID tagID(alias tag)() 419 out (r; r.id, "Invalid ID returned for tag " ~ tag.stringof) 420 do { 421 422 enum Tag = TagIDImpl!tag(); 423 424 debug 425 return TagID(cast(long) &Tag._id, fullyQualifiedName!tag); 426 else 427 return TagID(cast(long) &Tag._id); 428 429 } 430 431 /// Unique ID of a node tag. 432 struct TagID { 433 434 /// Unique ID of the tag. 435 long id; 436 437 invariant(id, "Tag ID must not be 0."); 438 439 /// Tag name. Only emitted when debugging. 440 debug string name; 441 442 bool opEqual(TagID other) { 443 444 return id == other.id; 445 446 } 447 448 long opCmp(TagID other) const { 449 450 return id - other.id; 451 452 } 453 454 } 455 456 private struct TagIDImpl(alias nodeTag) 457 if (isNodeTag!nodeTag) { 458 459 alias tag = nodeTag; 460 461 /// Implementation is the same as input action IDs, see fluid.input.InputAction. 462 /// For what's important, the _id field is not the ID; its pointer however, is. 463 private static immutable bool _id; 464 465 } 466 467 @("Members of anonymous enums cannot be NodeTags.") 468 unittest { 469 470 class A { 471 @NodeTag enum { foo } 472 } 473 class B : A { 474 @NodeTag enum { bar } 475 } 476 477 assert(!__traits(compiles, tagID!(B.foo))); 478 assert(!__traits(compiles, tagID!(B.bar))); 479 480 } 481 482 /// This node property will disable mouse input on the given node. 483 /// 484 /// Params: 485 /// value = If set to false, the effect is reversed and mouse input is instead enabled. 486 auto ignoreMouse(bool value = true) { 487 488 static struct IgnoreMouse { 489 490 bool value; 491 492 void apply(Node node) { 493 494 node.ignoreMouse = value; 495 496 } 497 498 } 499 500 return IgnoreMouse(value); 501 502 } 503 504 /// 505 unittest { 506 507 import fluid.label; 508 import fluid.button; 509 510 // Prevents the label from blocking the button 511 vframeButton( 512 label(.ignoreMouse, "Click me!"), 513 delegate { } 514 ); 515 516 } 517 518 @("ignoreMouse property sets Node.ignoreMouse to true") 519 unittest { 520 521 import fluid.space; 522 523 assert(vspace().ignoreMouse == false); 524 assert(vspace(.ignoreMouse).ignoreMouse == true); 525 assert(vspace(.ignoreMouse(false)).ignoreMouse == false); 526 assert(vspace(.ignoreMouse(true)).ignoreMouse == true); 527 528 } 529 530 /// This node property will make the subject hidden, setting the `isHidden` field to true. 531 /// 532 /// Params: 533 /// value = If set to false, the effect is reversed and the node is set to be visible instead. 534 /// See_Also: `Node.isHidden` 535 auto hidden(bool value = true) { 536 537 static struct Hidden { 538 539 bool value; 540 541 void apply(Node node) { 542 543 node.isHidden = value; 544 545 } 546 547 } 548 549 return Hidden(value); 550 551 } 552 553 /// 554 unittest { 555 556 import fluid.label; 557 558 auto myLabel = label(.hidden, "The user will never see this label"); 559 myLabel.draw(); // doesn't draw anything! 560 561 } 562 563 /// This node property will disable the subject, setting the `isHidden` field to true. 564 /// 565 /// Params: 566 /// value = If set to false, the effect is reversed and the node is set to be enabled instead. 567 /// See_Also: `Node.isDisabled` 568 auto disabled(bool value = true) { 569 570 static struct Disabled { 571 572 bool value; 573 574 void apply(Node node) { 575 576 node.isDisabled = value; 577 578 } 579 580 } 581 582 return Disabled(value); 583 584 } 585 586 unittest { 587 588 import fluid.button; 589 590 button( 591 .disabled, 592 "You cannot press this button!", 593 delegate { 594 assert(false); 595 } 596 ); 597 598 599 }