1 /// This tour is a set of examples and tutorials designed to illustrate core features of Fluid and provide a quick 2 /// start guide to developing applications using Fluid. 3 /// 4 /// This module is the central piece of the tour, gluing it together. It loads and parses each module to display 5 /// it as a document. It's not trivial; other modules from this package are designed to offer better guidance on Fluid 6 /// usage, but it might also be useful to people intending to implement similar functionality. 7 /// 8 /// To get started with the tour, use `dub run fluid:tour`, which should compile and run the tour. The program explains 9 /// different parts of the library and provides code examples, but you're free to browse through its files if you 10 /// like! introduction.d might be a good start. I hope this directory proves as a useful learning resource. 11 module fluid.tour; 12 13 import fluid; 14 import dparse.ast; 15 16 import std.string; 17 import std.traits; 18 import std.algorithm; 19 20 21 /// Maximum content width, used for code samples, since they require more space. 22 enum maxContentSize = .sizeLimitX(1000); 23 24 /// Reduced content width, used for document text. 25 enum contentSize = .sizeLimitX(800); 26 27 /// Sidebar width 28 enum sidebarSize = .sizeLimitX(220); 29 30 Theme mainTheme; 31 Theme exampleListTheme; 32 Theme codeTheme; 33 Theme previewWrapperTheme; 34 Theme highlightBoxTheme; 35 36 static this() { 37 38 import fluid.theme; 39 import std.file, std.path; 40 41 auto monospace = Style.loadTypeface(thisExePath.dirName.buildPath("../tour/ibm-plex-mono.ttf")); 42 43 mainTheme = Theme( 44 rule!Frame( 45 margin.sideX = 12, 46 margin.sideY = 16, 47 ), 48 rule!Label( 49 margin.sideX = 12, 50 margin.sideY = 7, 51 ), 52 rule!Button( 53 margin.sideX = 12, 54 margin.sideY = 7, 55 ), 56 rule!GridFrame(margin.sideY = 0), 57 rule!GridRow(margin = 0), 58 rule!ScrollFrame(margin = 0), 59 rule!PopupFrame( 60 padding.sideX = 2, 61 padding.sideY = 4, 62 ), 63 64 /// Code input 65 rule!CodeInput( 66 margin = 0, 67 typeface = monospace, 68 fontSize = 11.pt, 69 backgroundColor = color!"#dedede", 70 padding.sideX = 12, 71 padding.sideY = 16, 72 73 when!"a.isDisabled"( 74 backgroundColor = color!"#dedede", 75 ), 76 77 // TODO These colors are "borrowed" from Tree-sitter CLI, how about making our own? 78 when!`a.token.startsWith("keyword")` (textColor = color("#5f00d7")), 79 when!`a.token.startsWith("attribute")` (textColor = color("#af0000")), 80 when!`a.token.startsWith("property")` (textColor = color("#af0000")), 81 when!`a.token.startsWith("punctuation")`(textColor = color("#4e4e4e")), 82 when!`a.token.startsWith("type")` (textColor = color("#005f5f")), 83 when!`a.token.startsWith("operator")` (textColor = color("#50228a")), 84 when!`a.token.startsWith("comment")` (textColor = color("#8a8a8a")), 85 when!`a.token.startsWith("number")` (textColor = color("#875f00")), 86 when!`a.token.startsWith("string")` (textColor = color("#008700")), 87 when!`a.token.startsWith("constant")` (textColor = color("#875f00")), 88 when!`a.token.startsWith("variable")` (textColor = color("#875f00")), 89 when!`a.token.startsWith("function")` (textColor = color("#005fd7")), 90 when!`a.token.startsWith("module")` (textColor = color("#af8700")), 91 ), 92 93 // Heading 94 rule!(Label, Tags.heading)( 95 typeface = Style.defaultTypeface, 96 fontSize = 20.pt, 97 margin.sideTop = 20, 98 margin.sideBottom = 10, 99 ), 100 rule!(Label, Tags.subheading)( 101 typeface = Style.defaultTypeface, 102 fontSize = 16.pt, 103 margin.sideTop = 16, 104 margin.sideBottom = 8, 105 ), 106 ); 107 108 exampleListTheme = mainTheme.derive( 109 rule!Button( 110 padding.sideX = 8, 111 padding.sideY = 16, 112 margin = 2, 113 ), 114 ); 115 116 highlightBoxTheme = Theme( 117 rule!Node( 118 border = 1, 119 borderStyle = colorBorder(color!"#e62937"), 120 ), 121 ); 122 123 codeTheme = mainTheme.derive( 124 125 rule!Node( 126 typeface = monospace, 127 fontSize = 11.pt, 128 ), 129 rule!Frame( 130 padding = 0, 131 ), 132 rule!Label( 133 margin = 0, 134 backgroundColor = color!"#dedede", 135 padding.sideX = 12, 136 padding.sideY = 16, 137 ), 138 ); 139 140 previewWrapperTheme = mainTheme.derive( 141 rule!Frame( 142 margin = 0, 143 border = 1, 144 padding = 0, 145 borderStyle = colorBorder(color!"#dedede"), 146 ), 147 ); 148 149 } 150 151 enum Chapter { 152 @("Introduction") introduction, 153 @("Frames") frames, 154 @("Buttons & mutability") buttons, 155 @("Node slots") slots, 156 @("Themes") themes, 157 @("Margin, padding and border") margins, 158 @("Writing forms") forms, 159 @("moduleView") module_view, 160 // @("Popups") popups, 161 // @("Drag and drop") drag_and_drop, 162 } 163 164 @NodeTag 165 enum Tags { 166 heading, 167 subheading, 168 } 169 170 /// The entrypoint prepares themes and the window. 171 void main(string[] args) { 172 173 import raylib; 174 175 // Prepare the window 176 SetConfigFlags(ConfigFlags.FLAG_WINDOW_RESIZABLE); 177 SetTraceLogLevel(TraceLogLevel.LOG_WARNING); 178 version (FluidTour_NewIO) 179 InitWindow(1000, 750, "Fluid tour (New I/O)"); 180 else 181 InitWindow(1000, 750, "Fluid tour"); 182 SetTargetFPS(60); 183 SetExitKey(0); 184 scope (exit) CloseWindow(); 185 186 // Create the UI — pass the first argument to load a chapter under the given name 187 auto ui = args.length > 1 188 ? createUI(args[1]) 189 : createUI(); 190 191 // If new I/O is toggled on, disable the default backend 192 // and wrap the UI in the raylibStack root 193 version (FluidTour_NewIO) { 194 auto root = raylibStack.v5_5(ui); 195 root.io = new HeadlessBackend; 196 } 197 else { 198 auto root = ui; 199 } 200 201 // Event loop 202 while (!WindowShouldClose) { 203 204 BeginDrawing(); 205 scope (exit) EndDrawing(); 206 207 ClearBackground(color!"fff"); 208 209 // Fluid is by default configured to work with Raylib, so all you need to make them work together is a single 210 // call 211 root.draw(); 212 213 } 214 215 } 216 217 Space createUI(string initialChapter = null) @safe { 218 219 import std.conv; 220 221 Chapter currentChapter; 222 Frame root; 223 ScrollFrame contentWrapper; 224 Space navigationBar; 225 Label titleLabel; 226 Button leftButton, rightButton; 227 228 auto content = nodeSlot!Space(.layout!(1, "fill")); 229 auto outlineContent = vspace(.layout!"fill"); 230 auto outline = vframe( 231 .layout!"fill", 232 button( 233 .layout!"fill", 234 "Top", 235 delegate { 236 contentWrapper.scrollStart(); 237 } 238 ), 239 outlineContent, 240 ); 241 auto sidebar = sizeLock!switchSlot( 242 .layout!(1, "end", "start"), 243 .sidebarSize, 244 outline, 245 null, 246 ); 247 248 void changeChapter(Chapter chapter) { 249 250 // Change the content root and show the back button 251 currentChapter = chapter; 252 content = render(chapter); 253 titleLabel.text = title(chapter); 254 255 // Show navigation 256 navigationBar.show(); 257 leftButton.isHidden = chapter == 0; 258 rightButton.isHidden = chapter == Chapter.max; 259 260 // Scroll back to top 261 contentWrapper.scrollStart(); 262 263 // Collect all headings and build the outline 264 content.queueAction(new BuildOutline(outlineContent)); 265 266 } 267 268 // All content is scrollable 269 root = onionFrame( 270 .layout!"fill", 271 272 // Content 273 contentWrapper = vscrollFrame( 274 .layout!"fill", 275 .mainTheme, 276 sizeLock!vspace( 277 .layout!(1, "center", "start"), 278 .maxContentSize, 279 280 // Navigation 281 navigationBar = sizeLock!hspace( 282 .layout!"center", 283 .contentSize, 284 285 // Back button 286 button("← Back to navigation", delegate { 287 content = exampleList(&changeChapter); 288 navigationBar.hide(); 289 leftButton.hide(); 290 rightButton.hide(); 291 outlineContent.children = []; 292 }), 293 sidebar.retry( 294 popupButton("Outline", outline), 295 ), 296 titleLabel = label(""), 297 ).hide(), 298 299 // Content 300 content = exampleList(&changeChapter), 301 302 sizeLock!hframe( 303 .layout!"center", 304 .contentSize, 305 306 // Left button 307 leftButton = button("Previous chapter", delegate { 308 changeChapter(to!Chapter(currentChapter-1)); 309 }).hide(), 310 311 // Right button 312 rightButton = button(.layout!(1, "end"), "Next chapter", delegate { 313 changeChapter(to!Chapter(currentChapter+1)); 314 }).hide(), 315 ), 316 ), 317 ), 318 319 // Add sidebar on the left 320 hspace( 321 .layout!"fill", 322 sidebar, 323 324 // Reserve space for content 325 sizeLock!vspace(.maxContentSize), 326 327 // Balance the sidebar to center the content 328 vspace(.layout!1), 329 ), 330 331 332 ); 333 334 if (initialChapter) { 335 changeChapter(to!Chapter(initialChapter)); 336 } 337 338 return root; 339 340 } 341 342 Space exampleList(void delegate(Chapter) @safe changeChapter) @safe { 343 344 import std.meta; 345 import std.array; 346 import std.range; 347 348 auto chapterGrid = gridFrame( 349 .layout!"fill", 350 .segments(3), 351 ); 352 353 // TODO This should be easier 354 auto rows = only(EnumMembers!Chapter) 355 356 // Create a button for each chapter 357 .map!(a => button( 358 .layout!"fill", 359 title(a), 360 delegate { changeChapter(a); } 361 )) 362 363 // Split them into chunks of three 364 .chunks(3); 365 366 foreach (row; rows) { 367 chapterGrid.addRow(row.array); 368 } 369 370 return sizeLock!vspace( 371 .layout!"center", 372 .exampleListTheme, 373 .contentSize, 374 label(.layout!"center", .tags!(Tags.heading), "Hello, World!"), 375 label("Pick a chapter of the tutorial to get started. Start with the first one or browse the chapters that " 376 ~ "interest you! Output previews are shown next to code samples to help you understand the content."), 377 label(.layout!"fill", .tags!(FluidTag.warning), "While this tutorial covers the most important parts of Fluid, " 378 ~ "it's still incomplete. Content will be added in further updates of Fluid. Contributions are welcome."), 379 chapterGrid, 380 hseparator(), 381 imageView(.layout!"center", "logo.png", Vector2(249.5, 120)), 382 ); 383 384 } 385 386 /// Create a code block 387 Space showcaseCode(string code) { 388 389 return vframe( 390 .layout!"fill", 391 .codeTheme, 392 sizeLock!label( 393 .layout!"center", 394 .contentSize, 395 code, 396 ), 397 ); 398 399 } 400 401 /// Showcase code and its result. 402 Space showcaseCode(string code, Node node, Theme theme = Theme.init) { 403 404 CodeInput editor; 405 406 // Make the node inherit the default theme rather than the one we set 407 if (!node.theme) { 408 node.theme = either(theme, fluidDefaultTheme); 409 } 410 411 // Reset code editor text. 412 void reset() { 413 414 editor.value = code; 415 416 } 417 418 scope (success) reset(); 419 420 return hframe( 421 .layout!"fill", 422 423 editor = codeInput( 424 .layout!(1, "fill"), 425 .codeTheme, 426 ), 427 vframe( 428 .layout!(1, "fill"), 429 .previewWrapperTheme, 430 nodeSlot!Node( 431 .layout!(1, "fill"), 432 node, 433 ), 434 ) 435 ); 436 437 } 438 439 /// Get the title of the given chapter. 440 string title(Chapter query) @safe { 441 442 import std.traits; 443 444 switch (query) { 445 446 static foreach (chapter; EnumMembers!Chapter) { 447 448 case chapter: 449 return getUDAs!(chapter, string)[0]; 450 451 } 452 453 default: return null; 454 455 } 456 457 } 458 459 /// Render the given chapter. 460 Space render(Chapter query) @safe { 461 462 switch (query) { 463 464 static foreach (chapter; EnumMembers!Chapter) { 465 466 case chapter: 467 return render!chapter; 468 469 } 470 471 default: return null; 472 473 } 474 475 } 476 477 /// ditto 478 Space render(Chapter chapter)() @trusted { 479 480 import std.file; 481 import std.path; 482 import std.conv; 483 import std.meta; 484 import std.traits; 485 import dparse.lexer; 486 import dparse.parser : parseModule; 487 import dparse.rollback_allocator : RollbackAllocator; 488 489 LexerConfig config; 490 RollbackAllocator rba; 491 492 enum name = chapter.to!string; 493 494 // Import the module 495 mixin("import fluid.tour.", name, ";"); 496 alias mod = mixin("fluid.tour.", name); 497 498 // Get the module filename 499 const sourceDirectory = thisExePath.dirName.buildPath("../tour"); 500 const filename = buildPath(sourceDirectory, name ~ ".d"); 501 502 // Use moduleView for rendering its module 503 if (chapter == Chapter.module_view) { 504 505 import std.path; 506 import fluid.theme; 507 import fluid.module_view; 508 509 auto compiler = DlangCompiler.findAny(); 510 511 return moduleViewFile( 512 .layout!"fill", 513 mainTheme.derive( 514 rule!Frame( 515 padding = 0, 516 margin = 0, 517 gap = 4, 518 ), 519 rule!(Frame, FluidTag.warning)( 520 warningRule, 521 margin.sideX = 12, 522 children!Label( 523 margin = 0, 524 ), 525 ), 526 rule!Button( 527 margin = 0, 528 ), 529 ), 530 compiler, 531 filename, 532 ); 533 534 } 535 536 // Load the file 537 auto sourceCode = readText(filename); 538 auto cache = StringCache(StringCache.defaultBucketCount); 539 auto tokens = getTokensForParser(sourceCode, config, &cache); 540 541 // Parse it 542 auto m = parseModule(tokens, filename, &rba); 543 auto visitor = new FunctionVisitor(sourceCode.splitLines); 544 visitor.visit(m); 545 546 // Begin creating the document 547 auto document = vspace(.layout!"fill"); 548 549 // Check each member 550 static foreach (member; __traits(allMembers, mod)) {{ 551 552 // Limit to memberrs that end with "Example" 553 // Note we cannot properly support overloads 554 static if (member.endsWith("Example")) { 555 556 alias memberSymbol = __traits(getMember, mod, member); 557 558 auto documentation = sizeLock!vspace(.layout!"center", .contentSize); 559 auto code = visitor.functions[member]; 560 auto theme = fluidDefaultTheme; 561 562 // Load documentation attributes 563 static foreach (uda; __traits(getAttributes, memberSymbol)) { 564 565 // Node 566 static if (is(typeof(uda()) : Node)) 567 documentation ~= uda(); 568 569 // Theme 570 else static if (is(typeof(uda()) : Theme)) 571 theme = uda(); 572 573 } 574 575 // Insert the documentation 576 document ~= documentation; 577 578 // Add and run a code example if it returns a node 579 static if (is(ReturnType!memberSymbol : Node)) 580 document ~= showcaseCode(code, memberSymbol(), theme); 581 582 // Otherwise, show just the code 583 else if (code != "") 584 document ~= showcaseCode(code); 585 586 } 587 588 }} 589 590 return document; 591 592 } 593 594 class FunctionVisitor : ASTVisitor { 595 596 int indentLevel; 597 598 /// Source code divided by lines. 599 string[] sourceLines; 600 601 /// Mapping of function names to their bodies. 602 string[string] functions; 603 604 this(string[] sourceLines) { 605 606 this.sourceLines = sourceLines; 607 608 } 609 610 alias visit = ASTVisitor.visit; 611 612 override void visit(const FunctionDeclaration decl) { 613 614 import std.array; 615 import std.range; 616 import std.string; 617 import dparse.lexer; 618 import dparse.formatter; 619 620 static struct Location { 621 size_t line; 622 size_t column; 623 624 this(T)(T t) { 625 this.line = t.line - 1; 626 this.column = t.column - 1; 627 } 628 } 629 630 // Get function boundaries 631 auto content = decl.functionBody.specifiedFunctionBody.blockStatement; 632 auto tokens = content.tokens; 633 634 // Convert to 0-indexing 635 auto start = Location(content.tokens[0]); 636 auto end = Location(content.tokens[$-1]); 637 auto rangeLines = sourceLines[start.line..end.line+1]; 638 639 // Extract the text from original source code to preserve original formatting and comments 640 auto output = rangeLines 641 .enumerate 642 .map!((value) { 643 644 auto i = value[0], line = value[1]; 645 646 // One line code 647 if (rangeLines.length == 1) return line[start.column+1..end.column]; 648 649 // First line, skip past "{" 650 if (i == 0) return line[start.column+1..$]; 651 652 // Middle line, write whole 653 else if (i+1 != rangeLines.length) return line; 654 655 // Last line, end before "}" 656 else return line[0..end.column]; 657 658 }) 659 .join("\n"); 660 661 // Save the result 662 functions[decl.name.text] = output[].outdent.strip; 663 decl.accept(this); 664 665 } 666 667 } 668 669 class BuildOutline : TreeAction { 670 671 Space outline; 672 Children children; 673 674 this(Space outline) @safe { 675 676 this.outline = outline; 677 outline.children = []; 678 679 } 680 681 override void beforeResize(Node node, Vector2) @safe { 682 683 auto headingTags = .tags!(Tags.heading, Tags.subheading); 684 const isHeading = !node.tags.intersect(headingTags).empty; 685 686 // Headings only 687 if (!isHeading) return; 688 689 // Add a button to the outline 690 if (auto label = cast(Label) node) { 691 692 children ~= button( 693 .layout!"fill", 694 label.text, 695 delegate { 696 label.scrollToTop(); 697 } 698 ); 699 700 } 701 702 } 703 704 override void afterTree() @safe { 705 706 super.afterTree(); 707 outline.children = children; 708 outline.updateSize(); 709 710 } 711 712 }