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 auto myLabel = label( 201 .tags!(Tags.myTag), 202 "Hello, World!" 203 ); 204 205 assert(myLabel.tags == .tags!(Tags.myTag)); 206 207 } 208 209 /// Check if the given item is a node tag. 210 enum isNodeTag(alias tag) 211 // @NodeTag enum Tag; 212 // enum Tag { @NodeTag tag } 213 = (isSomeEnum!tag 214 && hasUDA!(tag, NodeTag)) 215 216 // @NodeTag enum Enum { tag } 217 || (!isType!tag 218 && is(__traits(parent, tag) == enum) 219 && hasUDA!(typeof(tag), NodeTag)); 220 221 /// Test if the given symbol is an enum, or an enum member. 222 enum isSomeEnum(alias tag) 223 = is(tag == enum) 224 || is(__traits(parent, tag) == enum); 225 226 /// Specify tags for the next node to add. 227 TagList tags(input...)() { 228 229 return TagList.init.add!input; 230 231 } 232 233 /// Node parameter assigning a new set of tags to a node. 234 struct TagList { 235 236 import std.range; 237 import std.algorithm; 238 239 /// A *sorted* array of tags. 240 private SortedRange!(TagID[]) range; 241 242 /// Check if the range is empty. 243 bool empty() { 244 245 return range.empty; 246 247 } 248 249 /// Count all tags. 250 size_t length() { 251 252 return range.length; 253 254 } 255 256 /// Get a list of all tags in the list. 257 const(TagID)[] get() { 258 259 return range.release; 260 261 } 262 263 /// Create a new set of tags expanded by the given set of tags. 264 TagList add(input...)() { 265 266 const originalLength = this.range.length; 267 268 TagID[input.length] newTags; 269 270 // Load the tags 271 static foreach (i, tag; input) { 272 273 newTags[i] = tagID!tag; 274 275 } 276 277 // Allocate output range 278 auto result = new TagID[originalLength + input.length]; 279 auto lhs = result[0..originalLength] = this.range.release; 280 281 // Sort the result 282 completeSort(assumeSorted(lhs), newTags[]); 283 284 // Add the remaining tags 285 result[originalLength..$] = newTags; 286 287 return TagList(assumeSorted(result)); 288 289 } 290 291 /// Remove given tags from the list. 292 TagList remove(input...)() { 293 294 TagID[input.length] targetTags; 295 296 // Load the tags 297 static foreach (i, tag; input) { 298 299 targetTags[i] = tagID!tag; 300 301 } 302 303 // Sort them 304 sort(targetTags[]); 305 306 return TagList( 307 setDifference(this.range, targetTags[]) 308 .array 309 .assumeSorted 310 ); 311 312 } 313 314 unittest { 315 316 @NodeTag 317 enum Foo { a, b, c, d } 318 319 auto myTags = tags!(Foo.a, Foo.b, Foo.c); 320 321 assert(myTags.remove!(Foo.b, Foo.a) == tags!(Foo.c)); 322 assert(myTags.remove!(Foo.d) == myTags); 323 assert(myTags.remove!() == myTags); 324 assert(myTags.remove!(Foo.a, Foo.b, Foo.c) == tags!()); 325 assert(myTags.remove!(Foo.a, Foo.b, Foo.c, Foo.d) == tags!()); 326 327 } 328 329 /// Get the intesection of the two tag lists. 330 /// Returns: A range with tags that are present in both of the lists. 331 auto intersect(TagList tags) { 332 333 return setIntersection(this.range, tags.range); 334 335 } 336 337 /// Assign this list of tags to the given node. 338 void apply(Node node) { 339 340 node.tags = this; 341 342 } 343 344 string toString() { 345 346 // Prevent writeln from clearing the range 347 return text(range.release); 348 349 } 350 351 } 352 353 unittest { 354 355 @NodeTag 356 enum singleEnum; 357 358 assert(isNodeTag!singleEnum); 359 360 @NodeTag 361 enum Tags { a, b, c } 362 363 assert(isNodeTag!(Tags.a)); 364 assert(isNodeTag!(Tags.b)); 365 assert(isNodeTag!(Tags.c)); 366 367 enum NonTags { a, b, c } 368 369 assert(!isNodeTag!(NonTags.a)); 370 assert(!isNodeTag!(NonTags.b)); 371 assert(!isNodeTag!(NonTags.c)); 372 373 enum SomeTags { a, b, @NodeTag tag } 374 375 assert(!isNodeTag!(SomeTags.a)); 376 assert(!isNodeTag!(SomeTags.b)); 377 assert(isNodeTag!(SomeTags.tag)); 378 379 } 380 381 unittest { 382 383 import std.range; 384 import std.algorithm; 385 386 @NodeTag 387 enum MyTags { 388 tag1, tag2 389 } 390 391 auto tags1 = tags!(MyTags.tag1, MyTags.tag2); 392 auto tags2 = tags!(MyTags.tag2, MyTags.tag1); 393 394 assert(tags1.intersect(tags2).walkLength == 2); 395 assert(tags2.intersect(tags1).walkLength == 2); 396 assert(tags1 == tags2); 397 398 auto tags3 = tags!(MyTags.tag1); 399 auto tags4 = tags!(MyTags.tag2); 400 401 assert(tags1.intersect(tags3).equal(tagID!(MyTags.tag1).only)); 402 assert(tags1.intersect(tags4).equal(tagID!(MyTags.tag2).only)); 403 assert(tags3.intersect(tags4).empty); 404 405 } 406 407 TagID tagID(alias tag)() 408 out (r; r.id, "Invalid ID returned for tag " ~ tag.stringof) 409 do { 410 411 enum Tag = TagIDImpl!tag(); 412 413 debug 414 return TagID(cast(long) &Tag._id, fullyQualifiedName!tag); 415 else 416 return TagID(cast(long) &Tag._id); 417 418 } 419 420 /// Unique ID of a node tag. 421 struct TagID { 422 423 /// Unique ID of the tag. 424 long id; 425 426 invariant(id, "Tag ID must not be 0."); 427 428 /// Tag name. Only emitted when debugging. 429 debug string name; 430 431 bool opEqual(TagID other) { 432 433 return id == other.id; 434 435 } 436 437 long opCmp(TagID other) const { 438 439 return id - other.id; 440 441 } 442 443 } 444 445 private struct TagIDImpl(alias nodeTag) 446 if (isNodeTag!nodeTag) { 447 448 alias tag = nodeTag; 449 450 /// Implementation is the same as input action IDs, see fluid.input.InputAction. 451 /// For what's important, the _id field is not the ID; its pointer however, is. 452 private static immutable bool _id; 453 454 } 455 456 @("Members of anonymous enums cannot be NodeTags.") 457 unittest { 458 459 class A { 460 @NodeTag enum { foo } 461 } 462 class B : A { 463 @NodeTag enum { bar } 464 } 465 466 assert(!__traits(compiles, tagID!(B.foo))); 467 assert(!__traits(compiles, tagID!(B.bar))); 468 469 } 470 471 /// This node property will disable mouse input on the given node. 472 /// 473 /// Params: 474 /// value = If set to false, the effect is reversed and mouse input is instead enabled. 475 auto ignoreMouse(bool value = true) { 476 477 static struct IgnoreMouse { 478 479 bool value; 480 481 void apply(Node node) { 482 483 node.ignoreMouse = value; 484 485 } 486 487 } 488 489 return IgnoreMouse(value); 490 491 } 492 493 /// 494 unittest { 495 496 import fluid.label; 497 import fluid.button; 498 499 // Prevents the label from blocking the button 500 vframeButton( 501 label(.ignoreMouse, "Click me!"), 502 delegate { } 503 ); 504 505 } 506 507 @("ignoreMouse property sets Node.ignoreMouse to true") 508 unittest { 509 510 import fluid.space; 511 512 assert(vspace().ignoreMouse == false); 513 assert(vspace(.ignoreMouse).ignoreMouse == true); 514 assert(vspace(.ignoreMouse(false)).ignoreMouse == false); 515 assert(vspace(.ignoreMouse(true)).ignoreMouse == true); 516 517 } 518 519 /// This node property will make the subject hidden, setting the `isHidden` field to true. 520 /// 521 /// Params: 522 /// value = If set to false, the effect is reversed and the node is set to be visible instead. 523 /// See_Also: `Node.isHidden` 524 auto hidden(bool value = true) { 525 526 static struct Hidden { 527 528 bool value; 529 530 void apply(Node node) { 531 532 node.isHidden = value; 533 534 } 535 536 } 537 538 return Hidden(value); 539 540 } 541 542 /// 543 unittest { 544 545 import fluid.label; 546 547 auto myLabel = label(.hidden, "The user will never see this label"); 548 myLabel.draw(); // doesn't draw anything! 549 550 } 551 552 /// This node property will disable the subject, setting the `isHidden` field to true. 553 /// 554 /// Params: 555 /// value = If set to false, the effect is reversed and the node is set to be enabled instead. 556 /// See_Also: `Node.isDisabled` 557 auto disabled(bool value = true) { 558 559 static struct Disabled { 560 561 bool value; 562 563 void apply(Node node) { 564 565 node.isDisabled = value; 566 567 } 568 569 } 570 571 return Disabled(value); 572 573 } 574 575 unittest { 576 577 import fluid.button; 578 579 button( 580 .disabled, 581 "You cannot press this button!", 582 delegate { 583 assert(false); 584 } 585 ); 586 587 588 }