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