1 module styles.theme; 2 3 import fluid; 4 import fluid.theme; 5 6 @safe: 7 8 @("Themes can be applied to Node") 9 unittest { 10 11 Theme theme; 12 13 with (Rule) 14 theme.add( 15 rule!Label( 16 textColor = color!"#abc", 17 ), 18 ); 19 20 auto content = label("placeholder"); 21 auto root = testSpace(theme, content); 22 23 root.draw(); 24 assert(content.text.texture.chunks[0].image.palette[0] == color("#abc")); 25 root.drawAndAssert( 26 content.drawsImage().ofColor("#fff"), 27 ); 28 29 } 30 31 @("Theme rules can be applied to Style") 32 @system unittest { 33 34 auto frameRule = rule!Frame( 35 Rule.margin.sideX = 8, 36 Rule.margin.sideY = 4, 37 ); 38 auto theme = nullTheme.derive(frameRule); 39 40 // Test selector 41 assert(frameRule.selector.type == typeid(Frame)); 42 assert(frameRule.selector.tags.empty); 43 44 // Test fields 45 auto style = Style.init; 46 frameRule.apply(vframe(), style); 47 assert(style.margin == [8, 8, 4, 4]); 48 assert(style.padding == style.init.padding); 49 assert(style.textColor == style.textColor.init); 50 51 // No dynamic rules 52 assert(rule.styleDelegate is null); 53 54 auto root = vframe(theme); 55 root.draw(); 56 57 assert(root.style == style); 58 59 } 60 61 @("Themes respect inheritance") 62 unittest { 63 64 auto myTheme = nullTheme.derive( 65 rule!Node( 66 Rule.margin.sideX = 8, 67 ), 68 rule!Label( 69 Rule.margin.sideTop = 6, 70 ), 71 rule!Button( 72 Rule.margin.sideBottom = 4, 73 ), 74 ); 75 76 auto style = Style.init; 77 myTheme.apply(button("", delegate { }), style); 78 79 assert(style.margin == [8, 8, 6, 4]); 80 81 } 82 83 @("Rules can set properties dynamically") 84 unittest { 85 86 auto myRule = rule!Label( 87 Rule.textColor = color!"011", 88 Rule.backgroundColor = color!"faf", 89 (Label node) => node.isDisabled 90 ? rule(Rule.tint = color!"000a") 91 : rule() 92 ); 93 94 auto myTheme = Theme( 95 rule!Label( 96 myRule, 97 ), 98 rule!Button( 99 myRule, 100 Rule.textColor = color!"012", 101 ), 102 ); 103 104 auto style = Style.init; 105 auto myLabel = label(""); 106 107 // Apply the style, including dynamic rules 108 auto cbs = myTheme.apply(myLabel, style); 109 assert(cbs.length == 1); 110 cbs[0](myLabel).apply(myLabel, style); 111 112 assert(style.textColor == color!"011"); 113 assert(style.backgroundColor == color!"faf"); 114 assert(style.tint == Style.init.tint); 115 116 // Disable the node and apply again, it should change nothing 117 myLabel.disable(); 118 myTheme.apply(myLabel, style); 119 assert(style.tint == Style.init.tint); 120 121 // Apply the callback, tint should change 122 cbs[0](myLabel).apply(myLabel, style); 123 assert(style.tint == color!"000a"); 124 125 } 126 127 @("Rules using tags from different enums do not collide") 128 unittest { 129 130 @NodeTag enum Foo { tag } 131 @NodeTag enum Bar { tag } 132 133 auto theme = nullTheme.derive( 134 rule!Label( 135 textColor = color("#f00"), 136 ), 137 rule!(Label, Foo.tag)( 138 textColor = color("#0f0"), 139 ), 140 ); 141 142 Label fooLabel, barLabel; 143 144 auto root = vspace( 145 theme, 146 fooLabel = label(.tags!(Foo.tag), "foo"), 147 barLabel = label(.tags!(Bar.tag), "bar"), 148 ); 149 150 root.draw(); 151 152 assert(fooLabel.pickStyle().textColor == color("#0f0")); 153 assert(barLabel.pickStyle().textColor == color("#f00")); 154 155 } 156 157 @("Margins can be defined through methods, and combined") 158 unittest { 159 160 import fluid.label; 161 162 auto myLabel = label(""); 163 164 void testMargin(Rule rule, float[4] margin) { 165 auto style = Style.init; 166 rule.apply(myLabel, style); 167 assert(style.margin == margin); 168 } 169 170 with (Rule) { 171 172 testMargin(rule(margin = 2), [2, 2, 2, 2]); 173 testMargin(rule(margin.sideX = 2), [2, 2, 0, 0]); 174 testMargin(rule(margin.sideY = 2), [0, 0, 2, 2]); 175 testMargin(rule(margin.sideTop = 2), [0, 0, 2, 0]); 176 testMargin(rule(margin.sideBottom = 2), [0, 0, 0, 2]); 177 testMargin(rule(margin.sideX = 2, margin.sideY = 4), [2, 2, 4, 4]); 178 testMargin(rule(margin = [1, 2, 3, 4]), [1, 2, 3, 4]); 179 testMargin(rule(margin.sideX = [1, 2]), [1, 2, 0, 0]); 180 181 } 182 183 } 184 185 @("Rule.opacity can be used to change tint's alpha") 186 unittest { 187 188 import std.math; 189 190 auto myRule = rule( 191 Rule.opacity = 0.5, 192 ); 193 auto style = Style.init; 194 195 myRule.apply(label(""), style); 196 197 assert(style.opacity.isClose(127/255f)); 198 assert(style.tint == color!"ffffff7f"); 199 200 auto secondRule = rule( 201 Rule.tint = color!"abc", 202 Rule.opacity = 0.6, 203 ); 204 205 style = Style.init; 206 secondRule.apply(label(""), style); 207 208 assert(style.opacity.isClose(153/255f)); 209 assert(style.tint == color!"abc9"); 210 211 } 212 213 @("Rule copying tests class ancestry") 214 @trusted 215 unittest { 216 217 import std.exception; 218 import core.exception : AssertError; 219 220 auto generalRule = rule( 221 Rule.textColor = color!"#001", 222 ); 223 auto buttonRule = rule!Button( 224 Rule.backgroundColor = color!"#002", 225 generalRule, 226 ); 227 assertThrown!AssertError( 228 rule!Label(buttonRule), 229 "Label rule cannot inherit from a Button rule." 230 ); 231 232 assertNotThrown(rule!Button(buttonRule)); 233 assertNotThrown(rule!Button(rule!Label())); 234 235 } 236 237 @("Dynamic rules cannot inherit from mismatched rules") 238 unittest { 239 240 import fluid.space; 241 import fluid.frame; 242 243 auto theme = nullTheme.derive( 244 rule!Space( 245 (Space _) => rule!Frame( 246 backgroundColor = color("#123"), 247 ), 248 ), 249 ); 250 251 auto root = vspace(theme); 252 root.draw(); 253 254 assert(root.pickStyle.backgroundColor == Color.init); 255 256 } 257 258 @("WhenRule immediately responds to changes") 259 unittest { 260 261 import fluid.label; 262 263 auto myTheme = Theme( 264 rule!Label( 265 Rule.textColor = color!"100", 266 Rule.backgroundColor = color!"aaa", 267 268 when!"a.isEmpty"(Rule.textColor = color!"200"), 269 when!"a.text == `two`"(Rule.backgroundColor = color!"010") 270 .otherwise(Rule.backgroundColor = color!"020"), 271 ), 272 ); 273 274 auto myLabel = label(myTheme, "one"); 275 myLabel.draw(); 276 277 assert(myLabel.pickStyle().textColor == color!"100"); 278 assert(myLabel.pickStyle().backgroundColor == color!"020"); 279 assert(myLabel.style.backgroundColor == color!"aaa"); 280 281 myLabel.text = ""; 282 283 assert(myLabel.pickStyle().textColor == color!"200"); 284 assert(myLabel.pickStyle().backgroundColor == color!"020"); 285 assert(myLabel.style.backgroundColor == color!"aaa"); 286 287 myLabel.text = "two"; 288 289 assert(myLabel.pickStyle().textColor == color!"100"); 290 assert(myLabel.pickStyle().backgroundColor == color!"010"); 291 assert(myLabel.style.backgroundColor == color!"aaa"); 292 293 } 294 295 @("Basic children rules work") 296 unittest { 297 298 import fluid.theme; 299 import std.algorithm; 300 301 auto theme = nullTheme.derive( 302 303 // Labels are red by default 304 rule!Label( 305 textColor = color("#f00"), 306 ), 307 // Labels inside frames turn green 308 rule!Frame( 309 children!Label( 310 textColor = color("#0f0"), 311 ), 312 ), 313 314 ); 315 316 Label[2] greenLabels; 317 Label[2] redLabels; 318 319 auto root = vspace( 320 theme, 321 redLabels[0] = label("red"), 322 vframe( 323 greenLabels[0] = label("green"), 324 hspace( 325 greenLabels[1] = label("green"), 326 ), 327 ), 328 redLabels[1] = label("red"), 329 ); 330 331 root.draw(); 332 333 assert(redLabels[] .all!(a => a.pickStyle.textColor == color("#f00")), "All red labels are red"); 334 assert(greenLabels[].all!(a => a.pickStyle.textColor == color("#0f0")), "All green labels are green"); 335 336 } 337 338 @("Children rules can be nested") 339 unittest { 340 341 import std.algorithm; 342 343 auto theme = nullTheme.derive( 344 345 // Labels are red by default 346 rule!Label( 347 textColor = color("#f00"), 348 ), 349 rule!Frame( 350 // Labels inside frames turn blue 351 children!Label( 352 textColor = color("#00f"), 353 ), 354 // But if nested further, they turn green 355 children!Frame( 356 textColor = color("#000"), 357 children!Label( 358 textColor = color("#0f0"), 359 ), 360 ), 361 ), 362 363 ); 364 365 Label[2] redLabels; 366 Label[3] blueLabels; 367 Label[4] greenLabels; 368 369 auto root = vspace( 370 theme, 371 redLabels[0] = label("Red"), 372 vframe( 373 blueLabels[0] = label("Blue"), 374 vframe( 375 greenLabels[0] = label("Green"), 376 vframe( 377 greenLabels[1] = label("Green"), 378 ), 379 ), 380 blueLabels[1] = label("Blue"), 381 vframe( 382 greenLabels[2] = label("Green"), 383 ) 384 ), 385 vspace( 386 vframe( 387 blueLabels[2] = label("Blue"), 388 vspace( 389 vframe( 390 greenLabels[3] = label("Green") 391 ), 392 ), 393 ), 394 redLabels[1] = label("Red"), 395 ), 396 ); 397 398 root.draw(); 399 400 assert(redLabels[] .all!(a => a.pickStyle.textColor == color("#f00")), "All red labels must be red"); 401 assert(blueLabels[] .all!(a => a.pickStyle.textColor == color("#00f")), "All blue labels must be blue"); 402 assert(greenLabels[].all!(a => a.pickStyle.textColor == color("#0f0")), "All green labels must be green"); 403 404 } 405 406 @("`children` rules work inside of `when`") 407 unittest { 408 409 auto theme = nullTheme.derive( 410 rule!FrameButton( 411 children!Label( 412 textColor = color("#f00"), 413 ), 414 when!"a.isFocused"( 415 children!Label( 416 textColor = color("#0f0"), 417 ), 418 ), 419 ), 420 ); 421 422 FrameButton first, second; 423 Label firstLabel, secondLabel; 424 425 auto root = vframe( 426 theme, 427 first = vframeButton( 428 firstLabel = label("Hello"), 429 delegate { } 430 ), 431 second = vframeButton( 432 secondLabel = label("Hello"), 433 delegate { } 434 ), 435 ); 436 437 root.draw(); 438 439 assert(firstLabel.pickStyle.textColor == color("#f00")); 440 assert(secondLabel.pickStyle.textColor == color("#f00")); 441 442 first.focus(); 443 root.draw(); 444 445 assert(firstLabel.pickStyle.textColor == color("#0f0")); 446 assert(secondLabel.pickStyle.textColor == color("#f00")); 447 448 second.focus(); 449 root.draw(); 450 451 assert(firstLabel.pickStyle.textColor == color("#f00")); 452 assert(secondLabel.pickStyle.textColor == color("#0f0")); 453 454 } 455 456 @("`children` rules work inside of delegates") 457 unittest { 458 459 // Note: This is impractical; in reality this will allocate memory excessively. 460 // This could be avoided by allocating all breadcrumbs on a stack. 461 class ColorFrame : Frame { 462 463 Color color; 464 465 this(Color color, Node[] nodes...) { 466 this.color = color; 467 super(nodes); 468 } 469 470 } 471 472 auto theme = nullTheme.derive( 473 rule!Label( 474 textColor = color("#000"), 475 ), 476 rule!ColorFrame( 477 (ColorFrame a) => rule( 478 children!Label( 479 textColor = a.color, 480 ) 481 ) 482 ), 483 ); 484 485 ColorFrame frame; 486 Label target; 487 Label sample; 488 489 auto root = vframe( 490 theme, 491 frame = new ColorFrame( 492 color("#00f"), 493 target = label("Colorful label"), 494 ), 495 sample = label("Never affected"), 496 ); 497 498 root.draw(); 499 500 assert(target.pickStyle.textColor == color("#00f")); 501 assert(sample.pickStyle.textColor == color("#000")); 502 503 frame.color = color("#0f0"), 504 root.draw(); 505 506 assert(target.pickStyle.textColor == color("#0f0")); 507 assert(sample.pickStyle.textColor == color("#000")); 508 509 } 510 511 @("Children rules can contain `when` clauses and delegates") 512 unittest { 513 514 // Focused button turns red, or green if inside of a frame 515 auto theme = nullTheme.derive( 516 rule!Frame( 517 children!Button( 518 when!"a.isFocused"( 519 textColor = color("#0f0"), 520 ), 521 (Node b) => rule( 522 backgroundColor = color("#123"), 523 ), 524 ), 525 ), 526 rule!Button( 527 textColor = color("#000"), 528 backgroundColor = color("#000"), 529 when!"a.isFocused"( 530 textColor = color("#f00"), 531 ), 532 ), 533 ); 534 535 Button greenButton; 536 Button redButton; 537 538 auto root = vspace( 539 theme, 540 vframe( 541 greenButton = button("Green", delegate { }), 542 ), 543 redButton = button("Red", delegate { }), 544 ); 545 546 root.draw(); 547 548 assert(greenButton.pickStyle.textColor == color("#000")); 549 assert(greenButton.pickStyle.backgroundColor == color("#123")); 550 assert(redButton.pickStyle.textColor == color("#000")); 551 assert(redButton.pickStyle.backgroundColor == color("#000")); 552 553 greenButton.focus(); 554 root.draw(); 555 556 assert(greenButton.isFocused); 557 assert(greenButton.pickStyle.textColor == color("#0f0")); 558 assert(greenButton.pickStyle.backgroundColor == color("#123")); 559 assert(redButton.pickStyle.textColor == color("#000")); 560 assert(redButton.pickStyle.backgroundColor == color("#000")); 561 562 redButton.focus(); 563 root.draw(); 564 565 assert(greenButton.pickStyle.textColor == color("#000")); 566 assert(greenButton.pickStyle.backgroundColor == color("#123")); 567 assert(redButton.pickStyle.textColor == color("#f00")); 568 assert(redButton.pickStyle.backgroundColor == color("#000")); 569 570 } 571 572 @("Rule.typeface can change typeface") 573 unittest { 574 575 import fluid.typeface; 576 577 auto typeface = new FreetypeTypeface; 578 auto sample = Rule.typeface = typeface; 579 580 assert(sample.name == "typeface"); 581 582 auto target = Style.defaultTypeface; 583 assert(target !is typeface); 584 sample.value.apply(target); 585 586 assert(target is typeface); 587 assert(typeface is typeface); 588 589 } 590 591 @("Plain Rule.margin assignment overrides the entire margin") 592 unittest { 593 594 auto sampleField = Rule.margin = 4; 595 assert(sampleField.name == "margin"); 596 597 float[4] field = [1, 1, 1, 1]; 598 sampleField.value.apply(field); 599 600 assert(field == [4, 4, 4, 4]); 601 602 } 603 604 @("Rule.margin supports partial changes") 605 unittest { 606 607 auto sampleField = Rule.margin.sideX = 8; 608 assert(sampleField.name == "margin"); 609 610 float[4] field = [1, 1, 1, 1]; 611 sampleField.value.apply(field); 612 613 assert(field == [8, 8, 1, 1]); 614 615 }